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
@@ -541,6 +541,7 @@
541
541
  system,
542
542
  groups: [],
543
543
  relations: [],
544
+ events: [],
544
545
  objects
545
546
  };
546
547
  }
@@ -920,12 +921,16 @@
920
921
  const height = frame.height;
921
922
  const padding = frame.padding;
922
923
  const layoutPreset = resolveLayoutPreset(document2);
923
- const projection = resolveProjection(document2, options.projection);
924
+ const schemaProjection = resolveProjection(document2, options.projection);
925
+ const camera = normalizeViewCamera(options.camera ?? null);
926
+ const renderProjection = resolveRenderProjection(schemaProjection, camera);
924
927
  const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
925
928
  const spacingFactor = layoutPresetSpacing(layoutPreset);
926
929
  const systemId = document2.system?.id ?? null;
927
- const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
928
- const relationships = buildSceneRelationships(document2.objects, objectMap);
930
+ const activeEventId = options.activeEventId ?? null;
931
+ const effectiveObjects = createEffectiveObjects(document2.objects, document2.events ?? [], activeEventId);
932
+ const objectMap = new Map(effectiveObjects.map((object) => [object.id, object]));
933
+ const relationships = buildSceneRelationships(effectiveObjects, objectMap);
929
934
  const positions = /* @__PURE__ */ new Map();
930
935
  const orbitDrafts = [];
931
936
  const leaderDrafts = [];
@@ -934,7 +939,7 @@
934
939
  const atObjects = [];
935
940
  const surfaceChildren = /* @__PURE__ */ new Map();
936
941
  const orbitChildren = /* @__PURE__ */ new Map();
937
- for (const object of document2.objects) {
942
+ for (const object of effectiveObjects) {
938
943
  const placement = object.placement;
939
944
  if (!placement) {
940
945
  rootObjects.push(object);
@@ -961,7 +966,7 @@
961
966
  surfaceChildren,
962
967
  objectMap,
963
968
  spacingFactor,
964
- projection,
969
+ projection: renderProjection,
965
970
  scaleModel
966
971
  };
967
972
  const primaryRoot = rootObjects.find((object) => object.type === "star") ?? rootObjects[0] ?? null;
@@ -973,7 +978,7 @@
973
978
  const rootRingRadius = Math.min(width, height) * 0.28 * spacingFactor * scaleModel.orbitDistanceMultiplier;
974
979
  secondaryRoots.forEach((object, index) => {
975
980
  const angle = angleForIndex(index, secondaryRoots.length, -Math.PI / 2);
976
- const offset = projectPolarOffset(angle, rootRingRadius, projection, 1);
981
+ const offset = projectPolarOffset(angle, rootRingRadius, renderProjection, 1);
977
982
  placeObject(object, centerX + offset.x, centerY + offset.y, 0, positions, orbitDrafts, leaderDrafts, context);
978
983
  });
979
984
  }
@@ -1029,38 +1034,48 @@
1029
1034
  const objects = [...positions.values()].map((position) => createSceneObject(position, scaleModel, relationships));
1030
1035
  const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
1031
1036
  const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
1032
- const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
1037
+ const labels = createSceneLabels(objects, width, height, scaleModel.labelMultiplier);
1033
1038
  const relations = createSceneRelations(document2, objects);
1034
- const layers = createSceneLayers(orbitVisuals, relations, leaders, objects, labels);
1035
- const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
1039
+ const events = createSceneEvents(document2.events ?? [], objects, activeEventId);
1040
+ const layers = createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels);
1041
+ const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, scaleModel.labelMultiplier);
1036
1042
  const semanticGroups = createSceneSemanticGroups(document2, objects);
1037
- const viewpoints = createSceneViewpoints(document2, projection, frame.preset, relationships, objectMap);
1038
- const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
1043
+ const viewpoints = createSceneViewpoints(document2, schemaProjection, frame.preset, relationships, objectMap);
1044
+ const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, scaleModel.labelMultiplier);
1039
1045
  return {
1040
1046
  width,
1041
1047
  height,
1042
1048
  padding,
1043
1049
  renderPreset: frame.preset,
1044
- projection,
1050
+ projection: schemaProjection,
1051
+ renderProjection,
1052
+ camera,
1045
1053
  scaleModel,
1046
1054
  title: String(document2.system?.title ?? document2.system?.properties.title ?? document2.system?.id ?? "WorldOrbit") || "WorldOrbit",
1047
- subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
1055
+ subtitle: buildSceneSubtitle(schemaProjection, renderProjection, layoutPreset, camera),
1048
1056
  systemId,
1049
- viewMode: projection,
1057
+ viewMode: schemaProjection,
1050
1058
  layoutPreset,
1051
1059
  metadata: {
1052
1060
  format: document2.format,
1053
1061
  version: document2.version,
1054
- view: projection,
1062
+ view: schemaProjection,
1063
+ renderProjection,
1055
1064
  scale: String(document2.system?.properties.scale ?? layoutPreset),
1056
1065
  units: String(document2.system?.properties.units ?? "mixed"),
1057
- preset: frame.preset ?? "custom"
1066
+ preset: frame.preset ?? "custom",
1067
+ ...camera?.azimuth !== null ? { "camera.azimuth": String(camera?.azimuth) } : {},
1068
+ ...camera?.elevation !== null ? { "camera.elevation": String(camera?.elevation) } : {},
1069
+ ...camera?.roll !== null ? { "camera.roll": String(camera?.roll) } : {},
1070
+ ...camera?.distance !== null ? { "camera.distance": String(camera?.distance) } : {}
1058
1071
  },
1059
1072
  contentBounds,
1060
1073
  layers,
1061
1074
  groups,
1062
1075
  semanticGroups,
1063
1076
  viewpoints,
1077
+ events,
1078
+ activeEventId,
1064
1079
  objects,
1065
1080
  orbitVisuals,
1066
1081
  relations,
@@ -1068,6 +1083,56 @@
1068
1083
  labels
1069
1084
  };
1070
1085
  }
1086
+ function createEffectiveObjects(objects, events, activeEventId) {
1087
+ const cloned = objects.map((object) => structuredClone(object));
1088
+ if (!activeEventId) {
1089
+ return cloned;
1090
+ }
1091
+ const activeEvent = events.find((event) => event.id === activeEventId);
1092
+ if (!activeEvent) {
1093
+ return cloned;
1094
+ }
1095
+ const objectMap = new Map(cloned.map((object) => [object.id, object]));
1096
+ const referencedIds = /* @__PURE__ */ new Set([
1097
+ ...activeEvent.targetObjectId ? [activeEvent.targetObjectId] : [],
1098
+ ...activeEvent.participantObjectIds,
1099
+ ...activeEvent.positions.map((pose) => pose.objectId)
1100
+ ]);
1101
+ for (const objectId of referencedIds) {
1102
+ const object = objectMap.get(objectId);
1103
+ if (!object) {
1104
+ continue;
1105
+ }
1106
+ if (activeEvent.epoch) {
1107
+ object.epoch = activeEvent.epoch;
1108
+ }
1109
+ if (activeEvent.referencePlane) {
1110
+ object.referencePlane = activeEvent.referencePlane;
1111
+ }
1112
+ }
1113
+ for (const pose of activeEvent.positions) {
1114
+ const object = objectMap.get(pose.objectId);
1115
+ if (!object) {
1116
+ continue;
1117
+ }
1118
+ if (pose.placement) {
1119
+ object.placement = structuredClone(pose.placement);
1120
+ }
1121
+ if (pose.inner) {
1122
+ object.properties.inner = { ...pose.inner };
1123
+ }
1124
+ if (pose.outer) {
1125
+ object.properties.outer = { ...pose.outer };
1126
+ }
1127
+ if (pose.epoch) {
1128
+ object.epoch = pose.epoch;
1129
+ }
1130
+ if (pose.referencePlane) {
1131
+ object.referencePlane = pose.referencePlane;
1132
+ }
1133
+ }
1134
+ return cloned;
1135
+ }
1071
1136
  function resolveLayoutPreset(document2) {
1072
1137
  const rawScale = String(document2.system?.properties.scale ?? "balanced").toLowerCase();
1073
1138
  switch (rawScale) {
@@ -1104,10 +1169,59 @@
1104
1169
  }
1105
1170
  }
1106
1171
  function resolveProjection(document2, projection) {
1107
- if (projection === "topdown" || projection === "isometric") {
1172
+ if (projection === "topdown" || projection === "isometric" || projection === "orthographic" || projection === "perspective") {
1108
1173
  return projection;
1109
1174
  }
1110
- return String(document2.system?.properties.view ?? "topdown").toLowerCase() === "isometric" ? "isometric" : "topdown";
1175
+ const documentView = String(document2.system?.properties.view ?? "topdown").toLowerCase();
1176
+ return parseViewProjection(documentView) ?? "topdown";
1177
+ }
1178
+ function resolveRenderProjection(projection, camera) {
1179
+ switch (projection) {
1180
+ case "topdown":
1181
+ return "topdown";
1182
+ case "isometric":
1183
+ return "isometric";
1184
+ case "orthographic":
1185
+ return camera && (camera.azimuth !== null || camera.elevation !== null || camera.roll !== null) ? "isometric" : "topdown";
1186
+ case "perspective":
1187
+ return "isometric";
1188
+ }
1189
+ }
1190
+ function normalizeViewCamera(camera) {
1191
+ if (!camera) {
1192
+ return null;
1193
+ }
1194
+ const normalized = {
1195
+ azimuth: normalizeFiniteCameraValue(camera.azimuth),
1196
+ elevation: normalizeFiniteCameraValue(camera.elevation),
1197
+ roll: normalizeFiniteCameraValue(camera.roll),
1198
+ distance: normalizePositiveCameraDistance(camera.distance)
1199
+ };
1200
+ return normalized.azimuth !== null || normalized.elevation !== null || normalized.roll !== null || normalized.distance !== null ? normalized : null;
1201
+ }
1202
+ function normalizeFiniteCameraValue(value) {
1203
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1204
+ }
1205
+ function normalizePositiveCameraDistance(value) {
1206
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
1207
+ }
1208
+ function buildSceneSubtitle(projection, renderProjection, layoutPreset, camera) {
1209
+ const parts = [`${capitalizeLabel(projection)} view`, `${capitalizeLabel(layoutPreset)} layout`];
1210
+ if (projection !== renderProjection) {
1211
+ parts.push(`2D ${renderProjection} fallback`);
1212
+ }
1213
+ if (camera) {
1214
+ const cameraParts = [
1215
+ camera.azimuth !== null ? `az ${camera.azimuth}` : null,
1216
+ camera.elevation !== null ? `el ${camera.elevation}` : null,
1217
+ camera.roll !== null ? `roll ${camera.roll}` : null,
1218
+ camera.distance !== null ? `dist ${camera.distance}` : null
1219
+ ].filter(Boolean);
1220
+ if (cameraParts.length > 0) {
1221
+ parts.push(`camera ${cameraParts.join(" / ")}`);
1222
+ }
1223
+ }
1224
+ return parts.join(" - ");
1111
1225
  }
1112
1226
  function resolveScaleModel(layoutPreset, overrides) {
1113
1227
  const defaults = defaultScaleModel(layoutPreset);
@@ -1223,24 +1337,14 @@
1223
1337
  hidden: draft.object.properties.hidden === true
1224
1338
  };
1225
1339
  }
1226
- function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1340
+ function createSceneLabels(objects, sceneWidth, sceneHeight, labelMultiplier) {
1227
1341
  const labels = [];
1228
1342
  const occupied = [];
1229
- const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort((left, right) => left.sortKey - right.sortKey);
1343
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1344
+ const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort(compareLabelPlacementOrder);
1230
1345
  for (const object of visibleObjects) {
1231
- const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1232
- const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
1233
- let labelY = object.y + direction * (object.radius + 18 * labelMultiplier);
1234
- let secondaryY = labelY + direction * (16 * labelMultiplier);
1235
- let bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1236
- let attempts = 0;
1237
- while (occupied.some((entry) => rectsOverlap(entry, bounds)) && attempts < 10) {
1238
- labelY += direction * 14 * labelMultiplier;
1239
- secondaryY += direction * 14 * labelMultiplier;
1240
- bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1241
- attempts += 1;
1242
- }
1243
- occupied.push(bounds);
1346
+ const placement = selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) ?? createLabelPlacement(object, defaultVerticalDirection(object, objectMap.get(object.parentId ?? "") ?? null, sceneHeight), 0, labelMultiplier);
1347
+ occupied.push(createLabelRect(object, placement, labelMultiplier));
1244
1348
  labels.push({
1245
1349
  renderId: `${object.renderId}-label`,
1246
1350
  objectId: object.objectId,
@@ -1249,17 +1353,128 @@
1249
1353
  semanticGroupIds: [...object.semanticGroupIds],
1250
1354
  label: object.label,
1251
1355
  secondaryLabel: object.secondaryLabel,
1252
- x: object.x,
1253
- y: labelY,
1254
- secondaryY,
1255
- textAnchor: "middle",
1256
- direction: direction < 0 ? "above" : "below",
1356
+ x: placement.x,
1357
+ y: placement.labelY,
1358
+ secondaryY: placement.secondaryY,
1359
+ textAnchor: placement.textAnchor,
1360
+ direction: placement.direction,
1257
1361
  hidden: object.hidden
1258
1362
  });
1259
1363
  }
1260
1364
  return labels;
1261
1365
  }
1262
- function createSceneLayers(orbitVisuals, relations, leaders, objects, labels) {
1366
+ function compareLabelPlacementOrder(left, right) {
1367
+ const priorityDiff = labelPlacementPriority(left) - labelPlacementPriority(right);
1368
+ if (priorityDiff !== 0) {
1369
+ return priorityDiff;
1370
+ }
1371
+ const renderPriorityDiff = (right.object.renderHints?.renderPriority ?? 0) - (left.object.renderHints?.renderPriority ?? 0);
1372
+ if (renderPriorityDiff !== 0) {
1373
+ return renderPriorityDiff;
1374
+ }
1375
+ return left.sortKey - right.sortKey;
1376
+ }
1377
+ function labelPlacementPriority(object) {
1378
+ switch (object.object.type) {
1379
+ case "star":
1380
+ return 0;
1381
+ case "planet":
1382
+ return 1;
1383
+ case "moon":
1384
+ return 2;
1385
+ case "belt":
1386
+ case "ring":
1387
+ return 3;
1388
+ case "asteroid":
1389
+ case "comet":
1390
+ return 4;
1391
+ case "structure":
1392
+ case "phenomenon":
1393
+ return 5;
1394
+ }
1395
+ }
1396
+ function selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) {
1397
+ for (const direction of preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight)) {
1398
+ const maxAttempts = direction === "left" || direction === "right" ? 4 : 6;
1399
+ for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
1400
+ const placement = createLabelPlacement(object, direction, attempt, labelMultiplier);
1401
+ const rect = createLabelRect(object, placement, labelMultiplier);
1402
+ if (!occupied.some((entry) => rectsOverlap(entry, rect))) {
1403
+ return placement;
1404
+ }
1405
+ }
1406
+ }
1407
+ return null;
1408
+ }
1409
+ function preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight) {
1410
+ const parent = object.parentId ? objectMap.get(object.parentId) ?? null : null;
1411
+ const vertical = defaultVerticalDirection(object, parent, sceneHeight);
1412
+ const oppositeVertical = vertical === "below" ? "above" : "below";
1413
+ const horizontal = defaultHorizontalDirection(object, parent, sceneWidth);
1414
+ const oppositeHorizontal = horizontal === "right" ? "left" : "right";
1415
+ 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";
1416
+ return preferHorizontal ? [horizontal, vertical, oppositeHorizontal, oppositeVertical] : [vertical, horizontal, oppositeVertical, oppositeHorizontal];
1417
+ }
1418
+ function defaultVerticalDirection(object, parent, sceneHeight) {
1419
+ if (parent && Math.abs(object.y - parent.y) > 6) {
1420
+ return object.y >= parent.y ? "below" : "above";
1421
+ }
1422
+ return object.y > sceneHeight * 0.62 ? "above" : "below";
1423
+ }
1424
+ function defaultHorizontalDirection(object, parent, sceneWidth) {
1425
+ if (parent && Math.abs(object.x - parent.x) > 6) {
1426
+ return object.x >= parent.x ? "right" : "left";
1427
+ }
1428
+ return object.x >= sceneWidth / 2 ? "right" : "left";
1429
+ }
1430
+ function createLabelPlacement(object, direction, attempt, labelMultiplier) {
1431
+ const step = 14 * labelMultiplier;
1432
+ switch (direction) {
1433
+ case "above": {
1434
+ const labelY = object.y - (object.radius + 18 * labelMultiplier + attempt * step);
1435
+ return {
1436
+ x: object.x,
1437
+ labelY,
1438
+ secondaryY: labelY - 16 * labelMultiplier,
1439
+ textAnchor: "middle",
1440
+ direction
1441
+ };
1442
+ }
1443
+ case "below": {
1444
+ const labelY = object.y + object.radius + 18 * labelMultiplier + attempt * step;
1445
+ return {
1446
+ x: object.x,
1447
+ labelY,
1448
+ secondaryY: labelY + 16 * labelMultiplier,
1449
+ textAnchor: "middle",
1450
+ direction
1451
+ };
1452
+ }
1453
+ case "left": {
1454
+ const x = object.x - (object.visualRadius + 16 * labelMultiplier + attempt * step);
1455
+ const labelY = object.y - 4 * labelMultiplier;
1456
+ return {
1457
+ x,
1458
+ labelY,
1459
+ secondaryY: labelY + 16 * labelMultiplier,
1460
+ textAnchor: "end",
1461
+ direction
1462
+ };
1463
+ }
1464
+ case "right": {
1465
+ const x = object.x + object.visualRadius + 16 * labelMultiplier + attempt * step;
1466
+ const labelY = object.y - 4 * labelMultiplier;
1467
+ return {
1468
+ x,
1469
+ labelY,
1470
+ secondaryY: labelY + 16 * labelMultiplier,
1471
+ textAnchor: "start",
1472
+ direction
1473
+ };
1474
+ }
1475
+ }
1476
+ }
1477
+ function createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels) {
1263
1478
  const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1264
1479
  const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1265
1480
  return [
@@ -1274,6 +1489,10 @@
1274
1489
  id: "relations",
1275
1490
  renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId)
1276
1491
  },
1492
+ {
1493
+ id: "events",
1494
+ renderIds: events.filter((event) => !event.hidden).map((event) => event.renderId)
1495
+ },
1277
1496
  {
1278
1497
  id: "objects",
1279
1498
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
@@ -1285,7 +1504,7 @@
1285
1504
  { id: "metadata", renderIds: ["wo-title", "wo-subtitle", "wo-meta"] }
1286
1505
  ];
1287
1506
  }
1288
- function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships) {
1507
+ function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, labelMultiplier) {
1289
1508
  const groups = /* @__PURE__ */ new Map();
1290
1509
  const ensureGroup = (groupId) => {
1291
1510
  if (!groupId) {
@@ -1334,7 +1553,7 @@
1334
1553
  }
1335
1554
  }
1336
1555
  for (const group of groups.values()) {
1337
- group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels);
1556
+ group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier);
1338
1557
  }
1339
1558
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1340
1559
  }
@@ -1368,6 +1587,29 @@
1368
1587
  };
1369
1588
  }).sort((left, right) => left.relation.id.localeCompare(right.relation.id));
1370
1589
  }
1590
+ function createSceneEvents(events, objects, activeEventId) {
1591
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1592
+ return events.map((event) => {
1593
+ const objectIds = [.../* @__PURE__ */ new Set([
1594
+ ...event.targetObjectId ? [event.targetObjectId] : [],
1595
+ ...event.participantObjectIds
1596
+ ])];
1597
+ const positions = objectIds.map((objectId) => objectMap.get(objectId)).filter(Boolean);
1598
+ const centroidX = positions.length > 0 ? positions.reduce((sum, object) => sum + object.x, 0) / positions.length : 0;
1599
+ const centroidY = positions.length > 0 ? positions.reduce((sum, object) => sum + object.y, 0) / positions.length : 0;
1600
+ return {
1601
+ renderId: `${createRenderId(event.id)}-event`,
1602
+ eventId: event.id,
1603
+ event,
1604
+ objectIds,
1605
+ participantIds: [...event.participantObjectIds],
1606
+ targetObjectId: event.targetObjectId,
1607
+ x: centroidX,
1608
+ y: centroidY,
1609
+ hidden: event.hidden || positions.length === 0 || positions.every((object) => object.hidden) || activeEventId !== null && event.id !== activeEventId
1610
+ };
1611
+ }).sort((left, right) => left.event.id.localeCompare(right.event.id));
1612
+ }
1371
1613
  function createSceneViewpoints(document2, projection, preset, relationships, objectMap) {
1372
1614
  const generatedOverview = createGeneratedOverviewViewpoint(document2, projection, preset);
1373
1615
  const drafts = /* @__PURE__ */ new Map();
@@ -1415,13 +1657,18 @@
1415
1657
  function createGeneratedOverviewViewpoint(document2, projection, preset) {
1416
1658
  const title = document2.system?.title ?? document2.system?.properties.title;
1417
1659
  const label = title ? `${String(title)} Overview` : "Overview";
1660
+ const camera = normalizeViewCamera(null);
1661
+ const renderProjection = resolveRenderProjection(projection, camera);
1418
1662
  return {
1419
1663
  id: "overview",
1420
1664
  label,
1421
1665
  summary: "Fit the whole system with the current atlas defaults.",
1422
1666
  objectId: null,
1423
1667
  selectedObjectId: null,
1668
+ eventIds: [],
1424
1669
  projection,
1670
+ renderProjection,
1671
+ camera,
1425
1672
  preset,
1426
1673
  rotationDeg: 0,
1427
1674
  scale: null,
@@ -1457,6 +1704,9 @@
1457
1704
  draft.select = normalizedValue;
1458
1705
  }
1459
1706
  return;
1707
+ case "events":
1708
+ draft.eventIds = splitListValue(normalizedValue);
1709
+ return;
1460
1710
  case "projection":
1461
1711
  case "view":
1462
1712
  draft.projection = parseViewProjection(normalizedValue) ?? projection;
@@ -1468,6 +1718,30 @@
1468
1718
  case "angle":
1469
1719
  draft.rotationDeg = parseFiniteNumber(normalizedValue) ?? draft.rotationDeg ?? 0;
1470
1720
  return;
1721
+ case "camera.azimuth":
1722
+ draft.camera = {
1723
+ ...draft.camera ?? createEmptyViewCamera(),
1724
+ azimuth: parseFiniteNumber(normalizedValue)
1725
+ };
1726
+ return;
1727
+ case "camera.elevation":
1728
+ draft.camera = {
1729
+ ...draft.camera ?? createEmptyViewCamera(),
1730
+ elevation: parseFiniteNumber(normalizedValue)
1731
+ };
1732
+ return;
1733
+ case "camera.roll":
1734
+ draft.camera = {
1735
+ ...draft.camera ?? createEmptyViewCamera(),
1736
+ roll: parseFiniteNumber(normalizedValue)
1737
+ };
1738
+ return;
1739
+ case "camera.distance":
1740
+ draft.camera = {
1741
+ ...draft.camera ?? createEmptyViewCamera(),
1742
+ distance: parsePositiveNumber(normalizedValue)
1743
+ };
1744
+ return;
1471
1745
  case "zoom":
1472
1746
  case "scale":
1473
1747
  draft.scale = parsePositiveNumber(normalizedValue);
@@ -1507,13 +1781,19 @@
1507
1781
  const selectedObjectId = draft.select && objectMap.has(draft.select) ? draft.select : objectId;
1508
1782
  const filter = normalizeViewpointFilter(draft.filter);
1509
1783
  const label = draft.label?.trim() || humanizeIdentifier(draft.id);
1784
+ const resolvedProjection = draft.projection ?? projection;
1785
+ const camera = normalizeViewCamera(draft.camera ?? null);
1786
+ const renderProjection = resolveRenderProjection(resolvedProjection, camera);
1510
1787
  return {
1511
1788
  id: draft.id,
1512
1789
  label,
1513
1790
  summary: draft.summary?.trim() || createViewpointSummary(label, objectId, filter),
1514
1791
  objectId,
1515
1792
  selectedObjectId,
1516
- projection: draft.projection ?? projection,
1793
+ eventIds: [...new Set(draft.eventIds ?? [])],
1794
+ projection: resolvedProjection,
1795
+ renderProjection,
1796
+ camera,
1517
1797
  preset: draft.preset ?? preset,
1518
1798
  rotationDeg: draft.rotationDeg ?? 0,
1519
1799
  scale: draft.scale ?? null,
@@ -1530,6 +1810,14 @@
1530
1810
  groupIds: []
1531
1811
  };
1532
1812
  }
1813
+ function createEmptyViewCamera() {
1814
+ return {
1815
+ azimuth: null,
1816
+ elevation: null,
1817
+ roll: null,
1818
+ distance: null
1819
+ };
1820
+ }
1533
1821
  function normalizeViewpointFilter(filter) {
1534
1822
  if (!filter) {
1535
1823
  return null;
@@ -1543,7 +1831,18 @@
1543
1831
  return normalized.query || normalized.objectTypes.length > 0 || normalized.tags.length > 0 || normalized.groupIds.length > 0 ? normalized : null;
1544
1832
  }
1545
1833
  function parseViewProjection(value) {
1546
- return value.toLowerCase() === "isometric" ? "isometric" : value.toLowerCase() === "topdown" ? "topdown" : null;
1834
+ switch (value.toLowerCase()) {
1835
+ case "topdown":
1836
+ return "topdown";
1837
+ case "isometric":
1838
+ return "isometric";
1839
+ case "orthographic":
1840
+ return "orthographic";
1841
+ case "perspective":
1842
+ return "perspective";
1843
+ default:
1844
+ return null;
1845
+ }
1547
1846
  }
1548
1847
  function parseRenderPreset(value) {
1549
1848
  const normalized = value.toLowerCase();
@@ -1570,7 +1869,7 @@
1570
1869
  next["orbits-front"] = enabled;
1571
1870
  continue;
1572
1871
  }
1573
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1872
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "events" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1574
1873
  next[rawLayer] = enabled;
1575
1874
  }
1576
1875
  }
@@ -1581,7 +1880,7 @@
1581
1880
  }
1582
1881
  function parseViewpointGroups(value, document2, relationships, objectMap) {
1583
1882
  return splitListValue(value).map((entry) => {
1584
- if (document2.schemaVersion === "2.1" || document2.groups.some((group) => group.id === entry)) {
1883
+ if (document2.schemaVersion === "2.1" || document2.schemaVersion === "2.5" || document2.groups.some((group) => group.id === entry)) {
1585
1884
  return entry;
1586
1885
  }
1587
1886
  if (entry.startsWith("wo-") && entry.endsWith("-group")) {
@@ -1618,7 +1917,7 @@
1618
1917
  }
1619
1918
  return parts.join(" - ");
1620
1919
  }
1621
- function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels) {
1920
+ function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, labelMultiplier) {
1622
1921
  let minX = Number.POSITIVE_INFINITY;
1623
1922
  let minY = Number.POSITIVE_INFINITY;
1624
1923
  let maxX = Number.NEGATIVE_INFINITY;
@@ -1648,7 +1947,7 @@
1648
1947
  for (const label of labels) {
1649
1948
  if (label.hidden)
1650
1949
  continue;
1651
- includeLabelBounds(label, include);
1950
+ includeLabelBounds(label, include, labelMultiplier);
1652
1951
  }
1653
1952
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
1654
1953
  return createBounds(0, 0, width, height);
@@ -1686,13 +1985,10 @@
1686
1985
  include(object.x - object.visualRadius - 24, object.y - object.visualRadius - 16);
1687
1986
  include(object.x + object.visualRadius + 24, object.y + object.visualRadius + 36);
1688
1987
  }
1689
- function includeLabelBounds(label, include) {
1690
- const labelScale = 1;
1691
- const labelHalfWidth = estimateLabelHalfWidthFromText(label.label, label.secondaryLabel, labelScale);
1692
- include(label.x - labelHalfWidth, label.y - 18);
1693
- include(label.x + labelHalfWidth, label.y + 8);
1694
- include(label.x - labelHalfWidth, label.secondaryY - 14);
1695
- include(label.x + labelHalfWidth, label.secondaryY + 8);
1988
+ function includeLabelBounds(label, include, labelMultiplier) {
1989
+ const bounds = createLabelRectFromText(label.x, label.y, label.secondaryY, label.textAnchor, label.direction, label.label, label.secondaryLabel, labelMultiplier);
1990
+ include(bounds.left, bounds.top);
1991
+ include(bounds.right, bounds.bottom);
1696
1992
  }
1697
1993
  function placeObject(object, x, y, depth, positions, orbitDrafts, leaderDrafts, context) {
1698
1994
  if (positions.has(object.id)) {
@@ -2082,7 +2378,7 @@
2082
2378
  return null;
2083
2379
  }
2084
2380
  }
2085
- function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels) {
2381
+ function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier) {
2086
2382
  let minX = Number.POSITIVE_INFINITY;
2087
2383
  let minY = Number.POSITIVE_INFINITY;
2088
2384
  let maxX = Number.NEGATIVE_INFINITY;
@@ -2111,7 +2407,7 @@
2111
2407
  }
2112
2408
  for (const label of labels) {
2113
2409
  if (!label.hidden && group.labelIds.includes(label.objectId)) {
2114
- includeLabelBounds(label, include);
2410
+ includeLabelBounds(label, include, labelMultiplier);
2115
2411
  }
2116
2412
  }
2117
2413
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
@@ -2136,12 +2432,28 @@
2136
2432
  }
2137
2433
  return current.id;
2138
2434
  }
2139
- function createLabelRect(x, labelY, secondaryY, labelHalfWidth, direction) {
2435
+ function createLabelRect(object, placement, labelMultiplier) {
2436
+ return createLabelRectFromText(placement.x, placement.labelY, placement.secondaryY, placement.textAnchor, placement.direction, object.label, object.secondaryLabel, labelMultiplier);
2437
+ }
2438
+ function createLabelRectFromText(x, labelY, secondaryY, textAnchor, direction, label, secondaryLabel, labelMultiplier) {
2439
+ const labelHalfWidth = estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier);
2440
+ const labelWidth = labelHalfWidth * 2;
2441
+ const topPadding = direction === "above" ? 18 : 12;
2442
+ const bottomPadding = direction === "above" ? 8 : 12;
2443
+ let left = x - labelHalfWidth;
2444
+ let right = x + labelHalfWidth;
2445
+ if (textAnchor === "start") {
2446
+ left = x;
2447
+ right = x + labelWidth;
2448
+ } else if (textAnchor === "end") {
2449
+ left = x - labelWidth;
2450
+ right = x;
2451
+ }
2140
2452
  return {
2141
- left: x - labelHalfWidth,
2142
- right: x + labelHalfWidth,
2143
- top: Math.min(labelY, secondaryY) - (direction < 0 ? 18 : 12),
2144
- bottom: Math.max(labelY, secondaryY) + (direction < 0 ? 8 : 12)
2453
+ left,
2454
+ right,
2455
+ top: Math.min(labelY, secondaryY) - topPadding,
2456
+ bottom: Math.max(labelY, secondaryY) + bottomPadding
2145
2457
  };
2146
2458
  }
2147
2459
  function rectsOverlap(left, right) {
@@ -2328,11 +2640,6 @@
2328
2640
  function customColorFor(value) {
2329
2641
  return typeof value === "string" && value.trim() ? value : void 0;
2330
2642
  }
2331
- function estimateLabelHalfWidth(object, labelMultiplier) {
2332
- const primaryWidth = object.label.length * 4.6 * labelMultiplier + 18;
2333
- const secondaryWidth = object.secondaryLabel.length * 3.9 * labelMultiplier + 18;
2334
- return Math.max(primaryWidth, secondaryWidth, object.visualRadius + 18);
2335
- }
2336
2643
  function estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier) {
2337
2644
  const primaryWidth = label.length * 4.6 * labelMultiplier + 18;
2338
2645
  const secondaryWidth = secondaryLabel.length * 3.9 * labelMultiplier + 18;
@@ -2349,7 +2656,7 @@
2349
2656
  }
2350
2657
 
2351
2658
  // packages/core/dist/draft.js
2352
- function materializeAtlasDocument(document2) {
2659
+ function materializeAtlasDocument(document2, options = {}) {
2353
2660
  const system = document2.system ? {
2354
2661
  type: "system",
2355
2662
  id: document2.system.id,
@@ -2360,6 +2667,8 @@
2360
2667
  properties: materializeDraftSystemProperties(document2.system),
2361
2668
  info: materializeDraftSystemInfo(document2.system)
2362
2669
  } : null;
2670
+ const objects = document2.objects.map(cloneWorldOrbitObject);
2671
+ applyEventPoseOverrides(objects, document2.events ?? [], options.activeEventId ?? null);
2363
2672
  return {
2364
2673
  format: "worldorbit",
2365
2674
  version: "1.0",
@@ -2367,7 +2676,8 @@
2367
2676
  system,
2368
2677
  groups: structuredClone(document2.groups ?? []),
2369
2678
  relations: structuredClone(document2.relations ?? []),
2370
- objects: document2.objects.map(cloneWorldOrbitObject)
2679
+ events: document2.events.map(cloneWorldOrbitEvent),
2680
+ objects
2371
2681
  };
2372
2682
  }
2373
2683
  function cloneWorldOrbitObject(object) {
@@ -2389,6 +2699,75 @@
2389
2699
  info: { ...object.info }
2390
2700
  };
2391
2701
  }
2702
+ function cloneWorldOrbitEvent(event) {
2703
+ return {
2704
+ ...event,
2705
+ participantObjectIds: [...event.participantObjectIds],
2706
+ tags: [...event.tags],
2707
+ positions: event.positions.map(cloneWorldOrbitEventPose)
2708
+ };
2709
+ }
2710
+ function cloneWorldOrbitEventPose(pose) {
2711
+ return {
2712
+ objectId: pose.objectId,
2713
+ placement: clonePlacement(pose.placement),
2714
+ inner: pose.inner ? { ...pose.inner } : void 0,
2715
+ outer: pose.outer ? { ...pose.outer } : void 0,
2716
+ epoch: pose.epoch ?? null,
2717
+ referencePlane: pose.referencePlane ?? null
2718
+ };
2719
+ }
2720
+ function clonePlacement(placement) {
2721
+ return placement ? structuredClone(placement) : null;
2722
+ }
2723
+ function applyEventPoseOverrides(objects, events, activeEventId) {
2724
+ if (!activeEventId) {
2725
+ return;
2726
+ }
2727
+ const event = events.find((entry) => entry.id === activeEventId);
2728
+ if (!event) {
2729
+ return;
2730
+ }
2731
+ const objectMap = new Map(objects.map((object) => [object.id, object]));
2732
+ const referencedIds = /* @__PURE__ */ new Set([
2733
+ ...event.targetObjectId ? [event.targetObjectId] : [],
2734
+ ...event.participantObjectIds,
2735
+ ...event.positions.map((pose) => pose.objectId)
2736
+ ]);
2737
+ for (const objectId of referencedIds) {
2738
+ const object = objectMap.get(objectId);
2739
+ if (!object) {
2740
+ continue;
2741
+ }
2742
+ if (event.epoch) {
2743
+ object.epoch = event.epoch;
2744
+ }
2745
+ if (event.referencePlane) {
2746
+ object.referencePlane = event.referencePlane;
2747
+ }
2748
+ }
2749
+ for (const pose of event.positions) {
2750
+ const object = objectMap.get(pose.objectId);
2751
+ if (!object) {
2752
+ continue;
2753
+ }
2754
+ if (pose.placement) {
2755
+ object.placement = clonePlacement(pose.placement);
2756
+ }
2757
+ if (pose.inner) {
2758
+ object.properties.inner = { ...pose.inner };
2759
+ }
2760
+ if (pose.outer) {
2761
+ object.properties.outer = { ...pose.outer };
2762
+ }
2763
+ if (pose.epoch) {
2764
+ object.epoch = pose.epoch;
2765
+ }
2766
+ if (pose.referencePlane) {
2767
+ object.referencePlane = pose.referencePlane;
2768
+ }
2769
+ }
2770
+ }
2392
2771
  function cloneProperties(properties) {
2393
2772
  const next = {};
2394
2773
  for (const [key, value] of Object.entries(properties)) {
@@ -2461,6 +2840,18 @@
2461
2840
  if (viewpoint.rotationDeg !== 0) {
2462
2841
  info2[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2463
2842
  }
2843
+ if (viewpoint.camera?.azimuth !== null) {
2844
+ info2[`${prefix}.camera.azimuth`] = String(viewpoint.camera?.azimuth);
2845
+ }
2846
+ if (viewpoint.camera?.elevation !== null) {
2847
+ info2[`${prefix}.camera.elevation`] = String(viewpoint.camera?.elevation);
2848
+ }
2849
+ if (viewpoint.camera?.roll !== null) {
2850
+ info2[`${prefix}.camera.roll`] = String(viewpoint.camera?.roll);
2851
+ }
2852
+ if (viewpoint.camera?.distance !== null) {
2853
+ info2[`${prefix}.camera.distance`] = String(viewpoint.camera?.distance);
2854
+ }
2464
2855
  const serializedLayers = serializeViewpointLayers(viewpoint.layers);
2465
2856
  if (serializedLayers) {
2466
2857
  info2[`${prefix}.layers`] = serializedLayers;
@@ -2477,6 +2868,9 @@
2477
2868
  if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2478
2869
  info2[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2479
2870
  }
2871
+ if (viewpoint.events.length > 0) {
2872
+ info2[`${prefix}.events`] = viewpoint.events.join(" ");
2873
+ }
2480
2874
  }
2481
2875
  for (const annotation of system.annotations) {
2482
2876
  const prefix = `annotation.${annotation.id}`;
@@ -2501,7 +2895,7 @@
2501
2895
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2502
2896
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2503
2897
  }
2504
- for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
2898
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
2505
2899
  if (layers[key] !== void 0) {
2506
2900
  tokens.push(layers[key] ? key : `-${key}`);
2507
2901
  }
@@ -2675,6 +3069,7 @@
2675
3069
  const diagnostics = [];
2676
3070
  const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
2677
3071
  const groupIds = new Set(document2.groups.map((group) => group.id));
3072
+ const eventIds = new Set(document2.events.map((event) => event.id));
2678
3073
  if (!document2.system) {
2679
3074
  diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
2680
3075
  }
@@ -2684,6 +3079,7 @@
2684
3079
  ["viewpoint", document2.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
2685
3080
  ["annotation", document2.system?.annotations.map((annotation) => annotation.id) ?? []],
2686
3081
  ["relation", document2.relations.map((relation) => relation.id)],
3082
+ ["event", document2.events.map((event) => event.id)],
2687
3083
  ["object", document2.objects.map((object) => object.id)]
2688
3084
  ]) {
2689
3085
  for (const id of ids) {
@@ -2699,11 +3095,14 @@
2699
3095
  validateRelation(relation, objectMap, diagnostics);
2700
3096
  }
2701
3097
  for (const viewpoint of document2.system?.viewpoints ?? []) {
2702
- validateViewpointFilter(viewpoint.filter, groupIds, sourceSchemaVersion, diagnostics, viewpoint.id);
3098
+ validateViewpoint(viewpoint, groupIds, eventIds, sourceSchemaVersion, diagnostics, objectMap);
2703
3099
  }
2704
3100
  for (const object of document2.objects) {
2705
3101
  validateObject(object, document2.system, objectMap, groupIds, diagnostics);
2706
3102
  }
3103
+ for (const event of document2.events) {
3104
+ validateEvent(event, document2.system, objectMap, diagnostics);
3105
+ }
2707
3106
  return diagnostics;
2708
3107
  }
2709
3108
  function validateRelation(relation, objectMap, diagnostics) {
@@ -2721,15 +3120,24 @@
2721
3120
  diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
2722
3121
  }
2723
3122
  }
2724
- function validateViewpointFilter(filter, groupIds, sourceSchemaVersion, diagnostics, viewpointId) {
2725
- if (!filter || sourceSchemaVersion !== "2.1") {
2726
- return;
2727
- }
2728
- for (const groupId of filter.groupIds) {
2729
- if (!groupIds.has(groupId)) {
2730
- diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`));
3123
+ function validateViewpoint(viewpoint, groupIds, eventIds, sourceSchemaVersion, diagnostics, objectMap) {
3124
+ const filter = viewpoint.filter;
3125
+ if (sourceSchemaVersion === "2.1" || sourceSchemaVersion === "2.5") {
3126
+ if (filter) {
3127
+ for (const groupId of filter.groupIds) {
3128
+ if (!groupIds.has(groupId)) {
3129
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpoint.id}".`, void 0, `viewpoint.${viewpoint.id}.groups`));
3130
+ }
3131
+ }
3132
+ }
3133
+ for (const eventId of viewpoint.events ?? []) {
3134
+ if (!eventIds.has(eventId)) {
3135
+ diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpoint.id}".`, void 0, `viewpoint.${viewpoint.id}.events`));
3136
+ }
2731
3137
  }
2732
3138
  }
3139
+ validateProjection(viewpoint.projection, diagnostics, `viewpoint.${viewpoint.id}.projection`, viewpoint.id);
3140
+ validateCamera(viewpoint.camera, viewpoint.projection, viewpoint.rotationDeg, diagnostics, viewpoint.id, viewpoint.focusObjectId, viewpoint.selectedObjectId, filter, objectMap);
2733
3141
  }
2734
3142
  function validateObject(object, system, objectMap, groupIds, diagnostics) {
2735
3143
  const placement = object.placement;
@@ -2742,6 +3150,12 @@
2742
3150
  }
2743
3151
  }
2744
3152
  }
3153
+ if (typeof object.epoch === "string" && !object.epoch.trim()) {
3154
+ diagnostics.push(warn("validate.epoch.empty", `Object "${object.id}" defines an empty epoch string.`, object.id, "epoch"));
3155
+ }
3156
+ if (typeof object.referencePlane === "string" && !object.referencePlane.trim()) {
3157
+ diagnostics.push(warn("validate.referencePlane.empty", `Object "${object.id}" defines an empty reference plane string.`, object.id, "referencePlane"));
3158
+ }
2745
3159
  if (orbitPlacement) {
2746
3160
  if (!objectMap.has(orbitPlacement.target)) {
2747
3161
  diagnostics.push(error("validate.orbit.target.unknown", `Unknown placement target "${orbitPlacement.target}" on "${object.id}".`, object.id, "orbit"));
@@ -2813,6 +3227,122 @@
2813
3227
  }
2814
3228
  }
2815
3229
  }
3230
+ function validateEvent(event, system, objectMap, diagnostics) {
3231
+ const fieldPrefix = `event.${event.id}`;
3232
+ const referencedIds = /* @__PURE__ */ new Set();
3233
+ if (!event.kind.trim()) {
3234
+ diagnostics.push(error("validate.event.kind.required", `Event "${event.id}" is missing a "kind" value.`, void 0, `${fieldPrefix}.kind`));
3235
+ }
3236
+ if (typeof event.epoch === "string" && !event.epoch.trim()) {
3237
+ diagnostics.push(warn("validate.event.epoch.empty", `Event "${event.id}" defines an empty epoch string.`, void 0, `${fieldPrefix}.epoch`));
3238
+ }
3239
+ if (typeof event.referencePlane === "string" && !event.referencePlane.trim()) {
3240
+ diagnostics.push(warn("validate.event.referencePlane.empty", `Event "${event.id}" defines an empty reference plane string.`, void 0, `${fieldPrefix}.referencePlane`));
3241
+ }
3242
+ if (!event.targetObjectId && event.participantObjectIds.length === 0) {
3243
+ diagnostics.push(error("validate.event.references.required", `Event "${event.id}" must define a "target" or at least one participant.`, void 0, `${fieldPrefix}.participants`));
3244
+ }
3245
+ if (event.targetObjectId) {
3246
+ referencedIds.add(event.targetObjectId);
3247
+ if (!objectMap.has(event.targetObjectId)) {
3248
+ diagnostics.push(error("validate.event.target.unknown", `Unknown event target "${event.targetObjectId}" on "${event.id}".`, void 0, `${fieldPrefix}.target`));
3249
+ }
3250
+ }
3251
+ const seenParticipants = /* @__PURE__ */ new Set();
3252
+ for (const participantId of event.participantObjectIds) {
3253
+ referencedIds.add(participantId);
3254
+ if (seenParticipants.has(participantId)) {
3255
+ diagnostics.push(warn("validate.event.participants.duplicate", `Event "${event.id}" repeats participant "${participantId}".`, void 0, `${fieldPrefix}.participants`));
3256
+ continue;
3257
+ }
3258
+ seenParticipants.add(participantId);
3259
+ if (!objectMap.has(participantId)) {
3260
+ diagnostics.push(error("validate.event.participants.unknown", `Unknown event participant "${participantId}" on "${event.id}".`, void 0, `${fieldPrefix}.participants`));
3261
+ }
3262
+ }
3263
+ if (event.targetObjectId && event.participantObjectIds.length > 0 && !event.participantObjectIds.includes(event.targetObjectId)) {
3264
+ diagnostics.push(warn("validate.event.target.notParticipant", `Event "${event.id}" defines a target outside its participants list.`, void 0, `${fieldPrefix}.target`));
3265
+ }
3266
+ if (event.positions.length === 0) {
3267
+ diagnostics.push(warn("validate.event.positions.missing", `Event "${event.id}" has no positions block and cannot drive a scene snapshot.`, void 0, `${fieldPrefix}.positions`));
3268
+ }
3269
+ if (/(?:^|[-_])(solar-eclipse|lunar-eclipse|transit|occultation)(?:$|[-_])/.test(event.kind) && referencedIds.size < 3) {
3270
+ 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`));
3271
+ }
3272
+ const poseIds = /* @__PURE__ */ new Set();
3273
+ for (const pose of event.positions) {
3274
+ const poseFieldPrefix = `${fieldPrefix}.pose.${pose.objectId}`;
3275
+ if (poseIds.has(pose.objectId)) {
3276
+ diagnostics.push(error("validate.event.pose.duplicate", `Event "${event.id}" defines "${pose.objectId}" more than once in positions.`, void 0, poseFieldPrefix));
3277
+ continue;
3278
+ }
3279
+ poseIds.add(pose.objectId);
3280
+ const object = objectMap.get(pose.objectId);
3281
+ if (!object) {
3282
+ diagnostics.push(error("validate.event.pose.object.unknown", `Unknown event pose object "${pose.objectId}" on "${event.id}".`, void 0, poseFieldPrefix));
3283
+ continue;
3284
+ }
3285
+ if (!referencedIds.has(pose.objectId)) {
3286
+ diagnostics.push(warn("validate.event.pose.unreferenced", `Event pose "${pose.objectId}" on "${event.id}" is not listed in target/participants.`, void 0, poseFieldPrefix));
3287
+ }
3288
+ validateEventPose(pose, object, event, system, objectMap, diagnostics, poseFieldPrefix, event.id);
3289
+ }
3290
+ const missingPoseIds = [...referencedIds].filter((objectId) => !poseIds.has(objectId));
3291
+ if (event.positions.length > 0 && missingPoseIds.length > 0) {
3292
+ diagnostics.push(warn("validate.event.positions.partial", `Event "${event.id}" leaves ${missingPoseIds.length} referenced object(s) on their base placement.`, void 0, `${fieldPrefix}.positions`));
3293
+ }
3294
+ }
3295
+ function validateEventPose(pose, object, event, system, objectMap, diagnostics, fieldPrefix, eventId) {
3296
+ const placement = pose.placement;
3297
+ if (!placement) {
3298
+ diagnostics.push(error("validate.event.pose.placement.required", `Event "${eventId}" pose "${pose.objectId}" is missing a placement mode.`, void 0, fieldPrefix));
3299
+ return;
3300
+ }
3301
+ if (placement.mode === "orbit") {
3302
+ if (!objectMap.has(placement.target)) {
3303
+ diagnostics.push(error("validate.event.pose.orbit.target.unknown", `Unknown event orbit target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.orbit`));
3304
+ }
3305
+ if (placement.distance && placement.semiMajor) {
3306
+ diagnostics.push(error("validate.event.pose.orbit.distanceConflict", `Event "${eventId}" pose "${pose.objectId}" cannot declare both "distance" and "semiMajor".`, void 0, `${fieldPrefix}.distance`));
3307
+ }
3308
+ if (placement.phase && !resolveEffectiveEpoch(system, object, event, pose)) {
3309
+ diagnostics.push(warn("validate.event.pose.phase.epochMissing", `Event "${eventId}" pose "${pose.objectId}" sets "phase" without an effective epoch.`, void 0, `${fieldPrefix}.phase`));
3310
+ }
3311
+ if (placement.inclination && !resolveEffectiveReferencePlane(system, object, event, pose)) {
3312
+ diagnostics.push(warn("validate.event.pose.inclination.referencePlaneMissing", `Event "${eventId}" pose "${pose.objectId}" sets "inclination" without an effective reference plane.`, void 0, `${fieldPrefix}.inclination`));
3313
+ }
3314
+ if (placement.period && !massInSolar(objectMap.get(placement.target)?.properties.mass)) {
3315
+ 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`));
3316
+ }
3317
+ return;
3318
+ }
3319
+ if (placement.mode === "surface") {
3320
+ const target = objectMap.get(placement.target);
3321
+ if (!target) {
3322
+ diagnostics.push(error("validate.event.pose.surface.target.unknown", `Unknown event surface target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.surface`));
3323
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
3324
+ 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`));
3325
+ }
3326
+ return;
3327
+ }
3328
+ if (placement.mode === "at") {
3329
+ if (object.type !== "structure" && object.type !== "phenomenon") {
3330
+ 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`));
3331
+ }
3332
+ const reference = placement.reference;
3333
+ if (reference.kind === "named" && !objectMap.has(reference.name)) {
3334
+ diagnostics.push(error("validate.event.pose.at.target.unknown", `Unknown event at-reference target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3335
+ } else if (reference.kind === "anchor" && !objectMap.has(reference.objectId)) {
3336
+ diagnostics.push(error("validate.event.pose.anchor.target.unknown", `Unknown event anchor target "${reference.objectId}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3337
+ } else if (reference.kind === "lagrange") {
3338
+ if (!objectMap.has(reference.primary)) {
3339
+ diagnostics.push(error("validate.event.pose.lagrange.primary.unknown", `Unknown event Lagrange target "${reference.primary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3340
+ } else if (reference.secondary && !objectMap.has(reference.secondary)) {
3341
+ diagnostics.push(error("validate.event.pose.lagrange.secondary.unknown", `Unknown event Lagrange target "${reference.secondary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3342
+ }
3343
+ }
3344
+ }
3345
+ }
2816
3346
  function validateAtTarget(object, objectMap, diagnostics) {
2817
3347
  const reference = object.placement?.mode === "at" ? object.placement.reference : null;
2818
3348
  if (!reference) {
@@ -2918,6 +3448,52 @@
2918
3448
  return null;
2919
3449
  }
2920
3450
  }
3451
+ function validateProjection(projection, diagnostics, field, viewpointId) {
3452
+ if (projection !== "topdown" && projection !== "isometric" && projection !== "orthographic" && projection !== "perspective") {
3453
+ diagnostics.push(error("validate.viewpoint.projection.invalid", `Unknown projection "${String(projection)}" in viewpoint "${viewpointId}".`, void 0, field));
3454
+ }
3455
+ }
3456
+ function validateCamera(camera, projection, rotationDeg, diagnostics, viewpointId, focusObjectId, selectedObjectId, filter, objectMap) {
3457
+ if (!camera) {
3458
+ return;
3459
+ }
3460
+ const prefix = `viewpoint.${viewpointId}.camera`;
3461
+ for (const [key, value] of [
3462
+ ["azimuth", camera.azimuth],
3463
+ ["elevation", camera.elevation],
3464
+ ["roll", camera.roll],
3465
+ ["distance", camera.distance]
3466
+ ]) {
3467
+ if (value !== null && (!Number.isFinite(value) || key === "distance" && value <= 0)) {
3468
+ diagnostics.push(error("validate.viewpoint.camera.invalid", `Invalid camera ${key} "${String(value)}" in viewpoint "${viewpointId}".`, void 0, `${prefix}.${key}`));
3469
+ }
3470
+ }
3471
+ if (camera.distance !== null && projection !== "perspective") {
3472
+ 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`));
3473
+ }
3474
+ if (projection === "topdown" && (camera.elevation !== null || camera.roll !== null)) {
3475
+ 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));
3476
+ }
3477
+ if (projection === "isometric" && camera.elevation !== null) {
3478
+ diagnostics.push(info("validate.viewpoint.camera.isometricStored", `Camera elevation on isometric viewpoint "${viewpointId}" is preserved semantically for future 3D rendering.`, void 0, `${prefix}.elevation`));
3479
+ }
3480
+ if (camera.azimuth !== null && camera.azimuth !== 0 && rotationDeg !== 0) {
3481
+ 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`));
3482
+ }
3483
+ const hasAnchor = focusObjectId !== null && objectMap.has(focusObjectId) || selectedObjectId !== null && objectMap.has(selectedObjectId) || !!filter;
3484
+ if (!hasAnchor) {
3485
+ diagnostics.push(info("validate.viewpoint.camera.anchorMissing", `Viewpoint "${viewpointId}" stores camera settings without a focus object, selection, or filter anchor.`, void 0, prefix));
3486
+ }
3487
+ }
3488
+ function resolveEffectiveEpoch(system, object, event, pose) {
3489
+ return normalizeOptionalContextString(pose?.epoch) ?? normalizeOptionalContextString(event?.epoch) ?? normalizeOptionalContextString(object.epoch) ?? normalizeOptionalContextString(system?.epoch) ?? null;
3490
+ }
3491
+ function resolveEffectiveReferencePlane(system, object, event, pose) {
3492
+ return normalizeOptionalContextString(pose?.referencePlane) ?? normalizeOptionalContextString(event?.referencePlane) ?? normalizeOptionalContextString(object.referencePlane) ?? normalizeOptionalContextString(system?.referencePlane) ?? null;
3493
+ }
3494
+ function normalizeOptionalContextString(value) {
3495
+ return typeof value === "string" && value.trim() ? value.trim() : null;
3496
+ }
2921
3497
  function toleranceForField(object, field) {
2922
3498
  const tolerance = object.tolerances?.find((entry) => entry.field === field)?.value;
2923
3499
  if (typeof tolerance === "number") {
@@ -3013,6 +3589,23 @@
3013
3589
  });
3014
3590
  }
3015
3591
  var DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
3592
+ var EVENT_POSE_FIELD_KEYS = /* @__PURE__ */ new Set([
3593
+ "orbit",
3594
+ "distance",
3595
+ "semiMajor",
3596
+ "eccentricity",
3597
+ "period",
3598
+ "angle",
3599
+ "inclination",
3600
+ "phase",
3601
+ "at",
3602
+ "surface",
3603
+ "free",
3604
+ "inner",
3605
+ "outer",
3606
+ "epoch",
3607
+ "referencePlane"
3608
+ ]);
3016
3609
  function parseWorldOrbitAtlas(source) {
3017
3610
  return parseAtlasSource(source);
3018
3611
  }
@@ -3027,12 +3620,15 @@
3027
3620
  const objectNodes = [];
3028
3621
  const groups = [];
3029
3622
  const relations = [];
3623
+ const events = [];
3624
+ const eventPoseNodes = /* @__PURE__ */ new Map();
3030
3625
  let sawDefaults = false;
3031
3626
  let sawAtlas = false;
3032
3627
  const viewpointIds = /* @__PURE__ */ new Set();
3033
3628
  const annotationIds = /* @__PURE__ */ new Set();
3034
3629
  const groupIds = /* @__PURE__ */ new Set();
3035
3630
  const relationIds = /* @__PURE__ */ new Set();
3631
+ const eventIds = /* @__PURE__ */ new Set();
3036
3632
  for (let index = 0; index < lines.length; index++) {
3037
3633
  const rawLine = lines[index];
3038
3634
  const lineNumber = index + 1;
@@ -3050,7 +3646,7 @@
3050
3646
  if (!sawSchemaHeader) {
3051
3647
  sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
3052
3648
  sawSchemaHeader = true;
3053
- if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
3649
+ if (prepared.comments.length > 0 && isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
3054
3650
  diagnostics.push({
3055
3651
  code: "parse.schema21.commentCompatibility",
3056
3652
  severity: "warning",
@@ -3063,7 +3659,7 @@
3063
3659
  continue;
3064
3660
  }
3065
3661
  if (indent === 0) {
3066
- section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, { sawDefaults, sawAtlas });
3662
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, { sawDefaults, sawAtlas });
3067
3663
  if (section.kind === "system") {
3068
3664
  system = section.system;
3069
3665
  } else if (section.kind === "defaults") {
@@ -3082,6 +3678,7 @@
3082
3678
  throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
3083
3679
  }
3084
3680
  const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
3681
+ const normalizedEvents = events.map((event) => normalizeDraftEvent(event, eventPoseNodes.get(event.id) ?? []));
3085
3682
  const outputVersion = forcedOutputVersion ?? (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
3086
3683
  const baseDocument = {
3087
3684
  format: "worldorbit",
@@ -3089,6 +3686,7 @@
3089
3686
  system,
3090
3687
  groups,
3091
3688
  relations,
3689
+ events: normalizedEvents,
3092
3690
  objects,
3093
3691
  diagnostics
3094
3692
  };
@@ -3118,13 +3716,13 @@
3118
3716
  return document2;
3119
3717
  }
3120
3718
  function assertDraftSchemaHeader(tokens, line) {
3121
- if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
3122
- throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
3719
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1", "2.5"].includes(tokens[1].value.toLowerCase())) {
3720
+ 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);
3123
3721
  }
3124
3722
  const version = tokens[1].value.toLowerCase();
3125
- return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
3723
+ return version === "2.5" ? "2.5" : version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
3126
3724
  }
3127
- function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, flags) {
3725
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, flags) {
3128
3726
  const keyword = tokens[0]?.value.toLowerCase();
3129
3727
  switch (keyword) {
3130
3728
  case "system":
@@ -3142,6 +3740,8 @@
3142
3740
  return {
3143
3741
  kind: "defaults",
3144
3742
  system,
3743
+ sourceSchemaVersion,
3744
+ diagnostics,
3145
3745
  seenFields: /* @__PURE__ */ new Set()
3146
3746
  };
3147
3747
  case "atlas":
@@ -3161,7 +3761,7 @@
3161
3761
  if (!system) {
3162
3762
  throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
3163
3763
  }
3164
- return startViewpointSection(tokens, line, system, viewpointIds);
3764
+ return startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics);
3165
3765
  case "annotation":
3166
3766
  if (!system) {
3167
3767
  throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
@@ -3173,6 +3773,9 @@
3173
3773
  case "relation":
3174
3774
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
3175
3775
  return startRelationSection(tokens, line, relations, relationIds);
3776
+ case "event":
3777
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "event", { line, column: tokens[0].column });
3778
+ return startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics);
3176
3779
  case "object":
3177
3780
  return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
3178
3781
  default:
@@ -3209,7 +3812,7 @@
3209
3812
  seenFields: /* @__PURE__ */ new Set()
3210
3813
  };
3211
3814
  }
3212
- function startViewpointSection(tokens, line, system, viewpointIds) {
3815
+ function startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics) {
3213
3816
  if (tokens.length !== 2) {
3214
3817
  throw new WorldOrbitError("Invalid viewpoint declaration", line, tokens[0]?.column ?? 1);
3215
3818
  }
@@ -3226,10 +3829,12 @@
3226
3829
  summary: "",
3227
3830
  focusObjectId: null,
3228
3831
  selectedObjectId: null,
3832
+ events: [],
3229
3833
  projection: system.defaults.view,
3230
3834
  preset: system.defaults.preset,
3231
3835
  zoom: null,
3232
3836
  rotationDeg: 0,
3837
+ camera: null,
3233
3838
  layers: {},
3234
3839
  filter: null
3235
3840
  };
@@ -3238,10 +3843,15 @@
3238
3843
  return {
3239
3844
  kind: "viewpoint",
3240
3845
  viewpoint,
3846
+ sourceSchemaVersion,
3847
+ diagnostics,
3241
3848
  seenFields: /* @__PURE__ */ new Set(),
3242
3849
  inFilter: false,
3243
3850
  filterIndent: null,
3244
- seenFilterFields: /* @__PURE__ */ new Set()
3851
+ seenFilterFields: /* @__PURE__ */ new Set(),
3852
+ inCamera: false,
3853
+ cameraIndent: null,
3854
+ seenCameraFields: /* @__PURE__ */ new Set()
3245
3855
  };
3246
3856
  }
3247
3857
  function startAnnotationSection(tokens, line, system, annotationIds) {
@@ -3328,6 +3938,51 @@
3328
3938
  seenFields: /* @__PURE__ */ new Set()
3329
3939
  };
3330
3940
  }
3941
+ function startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics) {
3942
+ if (tokens.length !== 2) {
3943
+ throw new WorldOrbitError("Invalid event declaration", line, tokens[0]?.column ?? 1);
3944
+ }
3945
+ const id = normalizeIdentifier(tokens[1].value);
3946
+ if (!id) {
3947
+ throw new WorldOrbitError("Event id must not be empty", line, tokens[1].column);
3948
+ }
3949
+ if (eventIds.has(id)) {
3950
+ throw new WorldOrbitError(`Duplicate event id "${id}"`, line, tokens[1].column);
3951
+ }
3952
+ const event = {
3953
+ id,
3954
+ kind: "",
3955
+ label: humanizeIdentifier2(id),
3956
+ summary: null,
3957
+ targetObjectId: null,
3958
+ participantObjectIds: [],
3959
+ timing: null,
3960
+ visibility: null,
3961
+ epoch: null,
3962
+ referencePlane: null,
3963
+ tags: [],
3964
+ color: null,
3965
+ hidden: false,
3966
+ positions: []
3967
+ };
3968
+ const rawPoses = [];
3969
+ events.push(event);
3970
+ eventPoseNodes.set(id, rawPoses);
3971
+ eventIds.add(id);
3972
+ return {
3973
+ kind: "event",
3974
+ event,
3975
+ sourceSchemaVersion,
3976
+ diagnostics,
3977
+ seenFields: /* @__PURE__ */ new Set(),
3978
+ rawPoses,
3979
+ inPositions: false,
3980
+ positionsIndent: null,
3981
+ activePose: null,
3982
+ poseIndent: null,
3983
+ activePoseSeenFields: /* @__PURE__ */ new Set()
3984
+ };
3985
+ }
3331
3986
  function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
3332
3987
  if (tokens.length < 3) {
3333
3988
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
@@ -3384,6 +4039,9 @@
3384
4039
  case "relation":
3385
4040
  applyRelationField(section, tokens, line);
3386
4041
  return;
4042
+ case "event":
4043
+ applyEventField(section, indent, tokens, line);
4044
+ return;
3387
4045
  case "object":
3388
4046
  applyObjectField(section, indent, tokens, line);
3389
4047
  return;
@@ -3426,6 +4084,12 @@
3426
4084
  const value = joinFieldValue(tokens, line);
3427
4085
  switch (key) {
3428
4086
  case "view":
4087
+ if (isSchema25Projection(value)) {
4088
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "defaults.view", {
4089
+ line,
4090
+ column: tokens[0].column
4091
+ });
4092
+ }
3429
4093
  section.system.defaults.view = parseProjectionValue(value, line, tokens[0].column);
3430
4094
  return;
3431
4095
  case "scale":
@@ -3465,14 +4129,36 @@
3465
4129
  throw new WorldOrbitError(`Unknown atlas field "${tokens[0].value}"`, line, tokens[0].column);
3466
4130
  }
3467
4131
  function applyViewpointField2(section, indent, tokens, line) {
4132
+ if (section.inCamera && indent <= (section.cameraIndent ?? 0)) {
4133
+ section.inCamera = false;
4134
+ section.cameraIndent = null;
4135
+ }
3468
4136
  if (section.inFilter && indent <= (section.filterIndent ?? 0)) {
3469
4137
  section.inFilter = false;
3470
4138
  section.filterIndent = null;
3471
4139
  }
4140
+ if (section.inCamera) {
4141
+ applyViewpointCameraField(section, tokens, line);
4142
+ return;
4143
+ }
3472
4144
  if (section.inFilter) {
3473
4145
  applyViewpointFilterField(section, tokens, line);
3474
4146
  return;
3475
4147
  }
4148
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "camera") {
4149
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.camera", {
4150
+ line,
4151
+ column: tokens[0].column
4152
+ });
4153
+ if (section.seenFields.has("camera")) {
4154
+ throw new WorldOrbitError('Duplicate viewpoint field "camera"', line, tokens[0].column);
4155
+ }
4156
+ section.seenFields.add("camera");
4157
+ section.inCamera = true;
4158
+ section.cameraIndent = indent;
4159
+ section.viewpoint.camera = section.viewpoint.camera ?? createEmptyViewCamera2();
4160
+ return;
4161
+ }
3476
4162
  if (tokens.length === 1 && tokens[0].value.toLowerCase() === "filter") {
3477
4163
  if (section.seenFields.has("filter")) {
3478
4164
  throw new WorldOrbitError('Duplicate viewpoint field "filter"', line, tokens[0].column);
@@ -3498,6 +4184,12 @@
3498
4184
  section.viewpoint.selectedObjectId = value;
3499
4185
  return;
3500
4186
  case "projection":
4187
+ if (isSchema25Projection(value)) {
4188
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "projection", {
4189
+ line,
4190
+ column: tokens[0].column
4191
+ });
4192
+ }
3501
4193
  section.viewpoint.projection = parseProjectionValue(value, line, tokens[0].column);
3502
4194
  return;
3503
4195
  case "preset":
@@ -3509,13 +4201,49 @@
3509
4201
  case "rotation":
3510
4202
  section.viewpoint.rotationDeg = parseFiniteNumber2(value, line, tokens[0].column, "rotation");
3511
4203
  return;
4204
+ case "camera":
4205
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.camera", {
4206
+ line,
4207
+ column: tokens[0].column
4208
+ });
4209
+ section.viewpoint.camera = parseInlineViewCamera(tokens.slice(1), line, section.viewpoint.camera);
4210
+ return;
3512
4211
  case "layers":
3513
- section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line);
4212
+ section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line, section.sourceSchemaVersion, section.diagnostics);
4213
+ return;
4214
+ case "events":
4215
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.events", {
4216
+ line,
4217
+ column: tokens[0].column
4218
+ });
4219
+ section.viewpoint.events = parseTokenList(tokens.slice(1), line, "events");
3514
4220
  return;
3515
4221
  default:
3516
4222
  throw new WorldOrbitError(`Unknown viewpoint field "${tokens[0].value}"`, line, tokens[0].column);
3517
4223
  }
3518
4224
  }
4225
+ function applyViewpointCameraField(section, tokens, line) {
4226
+ const key = requireUniqueField(tokens, section.seenCameraFields, line);
4227
+ const value = joinFieldValue(tokens, line);
4228
+ const camera = section.viewpoint.camera ?? createEmptyViewCamera2();
4229
+ switch (key) {
4230
+ case "azimuth":
4231
+ camera.azimuth = parseFiniteNumber2(value, line, tokens[0].column, "camera.azimuth");
4232
+ break;
4233
+ case "elevation":
4234
+ camera.elevation = parseFiniteNumber2(value, line, tokens[0].column, "camera.elevation");
4235
+ break;
4236
+ case "roll":
4237
+ camera.roll = parseFiniteNumber2(value, line, tokens[0].column, "camera.roll");
4238
+ break;
4239
+ case "distance":
4240
+ camera.distance = parsePositiveNumber2(value, line, tokens[0].column, "camera.distance");
4241
+ break;
4242
+ default:
4243
+ throw new WorldOrbitError(`Unknown viewpoint camera field "${tokens[0].value}"`, line, tokens[0].column);
4244
+ }
4245
+ section.viewpoint.camera = camera;
4246
+ }
3519
4247
  function applyViewpointFilterField(section, tokens, line) {
3520
4248
  const key = requireUniqueField(tokens, section.seenFilterFields, line);
3521
4249
  const filter = section.viewpoint.filter ?? createEmptyViewpointFilter2();
@@ -3615,6 +4343,126 @@
3615
4343
  throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
3616
4344
  }
3617
4345
  }
4346
+ function applyEventField(section, indent, tokens, line) {
4347
+ if (section.activePose && indent <= (section.poseIndent ?? 0)) {
4348
+ section.activePose = null;
4349
+ section.poseIndent = null;
4350
+ section.activePoseSeenFields.clear();
4351
+ }
4352
+ if (!section.activePose && section.inPositions && indent <= (section.positionsIndent ?? 0)) {
4353
+ section.inPositions = false;
4354
+ section.positionsIndent = null;
4355
+ }
4356
+ if (section.activePose) {
4357
+ if (tokens[0]?.value === "epoch" || tokens[0]?.value === "referencePlane") {
4358
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, `pose.${tokens[0].value}`, {
4359
+ line,
4360
+ column: tokens[0]?.column ?? 1
4361
+ });
4362
+ }
4363
+ section.activePose.fields.push(parseEventPoseField(tokens, line, section.activePoseSeenFields));
4364
+ return;
4365
+ }
4366
+ if (section.inPositions) {
4367
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "pose") {
4368
+ throw new WorldOrbitError(`Unknown event positions field "${tokens[0].value}"`, line, tokens[0]?.column ?? 1);
4369
+ }
4370
+ const objectId = tokens[1].value;
4371
+ if (!objectId.trim()) {
4372
+ throw new WorldOrbitError("Event pose object id must not be empty", line, tokens[1].column);
4373
+ }
4374
+ const rawPose = {
4375
+ objectId,
4376
+ fields: [],
4377
+ location: { line, column: tokens[0].column }
4378
+ };
4379
+ section.rawPoses.push(rawPose);
4380
+ section.activePose = rawPose;
4381
+ section.poseIndent = indent;
4382
+ section.activePoseSeenFields = /* @__PURE__ */ new Set();
4383
+ return;
4384
+ }
4385
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "positions") {
4386
+ if (section.seenFields.has("positions")) {
4387
+ throw new WorldOrbitError('Duplicate event field "positions"', line, tokens[0].column);
4388
+ }
4389
+ section.seenFields.add("positions");
4390
+ section.inPositions = true;
4391
+ section.positionsIndent = indent;
4392
+ return;
4393
+ }
4394
+ const key = requireUniqueField(tokens, section.seenFields, line);
4395
+ switch (key) {
4396
+ case "kind":
4397
+ section.event.kind = joinFieldValue(tokens, line);
4398
+ return;
4399
+ case "label":
4400
+ section.event.label = joinFieldValue(tokens, line);
4401
+ return;
4402
+ case "summary":
4403
+ section.event.summary = joinFieldValue(tokens, line);
4404
+ return;
4405
+ case "target":
4406
+ section.event.targetObjectId = joinFieldValue(tokens, line);
4407
+ return;
4408
+ case "participants":
4409
+ section.event.participantObjectIds = parseTokenList(tokens.slice(1), line, "participants");
4410
+ return;
4411
+ case "timing":
4412
+ section.event.timing = joinFieldValue(tokens, line);
4413
+ return;
4414
+ case "visibility":
4415
+ section.event.visibility = joinFieldValue(tokens, line);
4416
+ return;
4417
+ case "epoch":
4418
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "event.epoch", {
4419
+ line,
4420
+ column: tokens[0].column
4421
+ });
4422
+ section.event.epoch = joinFieldValue(tokens, line);
4423
+ return;
4424
+ case "referenceplane":
4425
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "event.referencePlane", {
4426
+ line,
4427
+ column: tokens[0].column
4428
+ });
4429
+ section.event.referencePlane = joinFieldValue(tokens, line);
4430
+ return;
4431
+ case "tags":
4432
+ section.event.tags = parseTokenList(tokens.slice(1), line, "tags");
4433
+ return;
4434
+ case "color":
4435
+ section.event.color = joinFieldValue(tokens, line);
4436
+ return;
4437
+ case "hidden":
4438
+ section.event.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4439
+ line,
4440
+ column: tokens[0].column
4441
+ });
4442
+ return;
4443
+ default:
4444
+ throw new WorldOrbitError(`Unknown event field "${tokens[0].value}"`, line, tokens[0].column);
4445
+ }
4446
+ }
4447
+ function parseEventPoseField(tokens, line, seenFields) {
4448
+ if (tokens.length < 2) {
4449
+ throw new WorldOrbitError("Invalid event pose field line", line, tokens[0]?.column ?? 1);
4450
+ }
4451
+ const key = tokens[0].value;
4452
+ if (!EVENT_POSE_FIELD_KEYS.has(key)) {
4453
+ throw new WorldOrbitError(`Unknown event pose field "${key}"`, line, tokens[0].column);
4454
+ }
4455
+ if (seenFields.has(key)) {
4456
+ throw new WorldOrbitError(`Duplicate event pose field "${key}"`, line, tokens[0].column);
4457
+ }
4458
+ seenFields.add(key);
4459
+ return {
4460
+ type: "field",
4461
+ key,
4462
+ values: tokens.slice(1).map((token) => token.value),
4463
+ location: { line, column: tokens[0].column }
4464
+ };
4465
+ }
3618
4466
  function applyObjectField(section, indent, tokens, line) {
3619
4467
  if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
3620
4468
  section.activeBlock = null;
@@ -3673,7 +4521,7 @@
3673
4521
  function parseObjectTypeTokens(tokens, line) {
3674
4522
  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");
3675
4523
  }
3676
- function parseLayerTokens(tokens, line) {
4524
+ function parseLayerTokens(tokens, line, sourceSchemaVersion, diagnostics) {
3677
4525
  const layers = {};
3678
4526
  for (const token of parseTokenList(tokens, line, "layers")) {
3679
4527
  const enabled = !token.startsWith("-") && !token.startsWith("!");
@@ -3683,7 +4531,13 @@
3683
4531
  layers["orbits-front"] = enabled;
3684
4532
  continue;
3685
4533
  }
3686
- if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "objects" || raw === "labels" || raw === "metadata") {
4534
+ if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "events" || raw === "objects" || raw === "labels" || raw === "metadata") {
4535
+ if (raw === "events" && sourceSchemaVersion && diagnostics) {
4536
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "layers.events", {
4537
+ line,
4538
+ column: tokens[0]?.column ?? 1
4539
+ });
4540
+ }
3687
4541
  layers[raw] = enabled;
3688
4542
  }
3689
4543
  }
@@ -3701,11 +4555,15 @@
3701
4555
  }
3702
4556
  function parseProjectionValue(value, line, column) {
3703
4557
  const normalized = value.toLowerCase();
3704
- if (normalized !== "topdown" && normalized !== "isometric") {
4558
+ if (normalized !== "topdown" && normalized !== "isometric" && normalized !== "orthographic" && normalized !== "perspective") {
3705
4559
  throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
3706
4560
  }
3707
4561
  return normalized;
3708
4562
  }
4563
+ function isSchema25Projection(value) {
4564
+ const normalized = value.toLowerCase();
4565
+ return normalized === "orthographic" || normalized === "perspective";
4566
+ }
3709
4567
  function parsePresetValue(value, line, column) {
3710
4568
  const normalized = value.toLowerCase();
3711
4569
  if (normalized === "diagram" || normalized === "presentation" || normalized === "atlas-card" || normalized === "markdown") {
@@ -3735,6 +4593,48 @@
3735
4593
  groupIds: []
3736
4594
  };
3737
4595
  }
4596
+ function createEmptyViewCamera2() {
4597
+ return {
4598
+ azimuth: null,
4599
+ elevation: null,
4600
+ roll: null,
4601
+ distance: null
4602
+ };
4603
+ }
4604
+ function parseInlineViewCamera(tokens, line, current) {
4605
+ if (tokens.length === 0 || tokens.length % 2 !== 0) {
4606
+ throw new WorldOrbitError('Field "camera" expects "<field> <value>" pairs', line, tokens[0]?.column ?? 1);
4607
+ }
4608
+ const camera = current ? { ...current } : createEmptyViewCamera2();
4609
+ const seen = /* @__PURE__ */ new Set();
4610
+ for (let index = 0; index < tokens.length; index += 2) {
4611
+ const fieldToken = tokens[index];
4612
+ const valueToken = tokens[index + 1];
4613
+ const key = fieldToken.value.toLowerCase();
4614
+ if (seen.has(key)) {
4615
+ throw new WorldOrbitError(`Duplicate viewpoint camera field "${fieldToken.value}"`, line, fieldToken.column);
4616
+ }
4617
+ seen.add(key);
4618
+ const value = valueToken.value;
4619
+ switch (key) {
4620
+ case "azimuth":
4621
+ camera.azimuth = parseFiniteNumber2(value, line, fieldToken.column, "camera.azimuth");
4622
+ break;
4623
+ case "elevation":
4624
+ camera.elevation = parseFiniteNumber2(value, line, fieldToken.column, "camera.elevation");
4625
+ break;
4626
+ case "roll":
4627
+ camera.roll = parseFiniteNumber2(value, line, fieldToken.column, "camera.roll");
4628
+ break;
4629
+ case "distance":
4630
+ camera.distance = parsePositiveNumber2(value, line, fieldToken.column, "camera.distance");
4631
+ break;
4632
+ default:
4633
+ throw new WorldOrbitError(`Unknown viewpoint camera field "${fieldToken.value}"`, line, fieldToken.column);
4634
+ }
4635
+ }
4636
+ return camera;
4637
+ }
3738
4638
  function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
3739
4639
  const fields = [];
3740
4640
  let index = 0;
@@ -3822,7 +4722,7 @@
3822
4722
  }
3823
4723
  function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
3824
4724
  const fieldMap = collectDraftFields(node.fields);
3825
- const placement = extractDraftPlacement(node.objectType, fieldMap);
4725
+ const placement = extractPlacementFromFieldMap(fieldMap);
3826
4726
  const properties = normalizeDraftProperties(node.objectType, fieldMap);
3827
4727
  const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
3828
4728
  const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
@@ -3867,21 +4767,41 @@
3867
4767
  object.tolerances = tolerances;
3868
4768
  if (typedBlocks && Object.keys(typedBlocks).length > 0)
3869
4769
  object.typedBlocks = typedBlocks;
3870
- if (sourceSchemaVersion !== "2.1") {
4770
+ if (isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
3871
4771
  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) {
3872
4772
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
3873
4773
  }
3874
4774
  }
3875
4775
  return object;
3876
4776
  }
3877
- function collectDraftFields(fields) {
4777
+ function normalizeDraftEvent(event, rawPoses) {
4778
+ return {
4779
+ ...event,
4780
+ participantObjectIds: [...new Set(event.participantObjectIds)],
4781
+ tags: [...new Set(event.tags)],
4782
+ positions: rawPoses.map((pose) => normalizeDraftEventPose(pose))
4783
+ };
4784
+ }
4785
+ function normalizeDraftEventPose(rawPose) {
4786
+ const fieldMap = collectDraftFields(rawPose.fields, "event-pose");
4787
+ const placement = extractPlacementFromFieldMap(fieldMap);
4788
+ return {
4789
+ objectId: rawPose.objectId,
4790
+ placement,
4791
+ inner: parseOptionalUnitField(fieldMap.get("inner")?.[0], "inner"),
4792
+ outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer"),
4793
+ epoch: parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]),
4794
+ referencePlane: parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0])
4795
+ };
4796
+ }
4797
+ function collectDraftFields(fields, _mode = "object") {
3878
4798
  const grouped = /* @__PURE__ */ new Map();
3879
4799
  for (const field of fields) {
3880
4800
  const spec = getDraftObjectFieldSpec(field.key);
3881
- if (!spec) {
4801
+ if (!spec && !EVENT_POSE_FIELD_KEYS.has(field.key)) {
3882
4802
  throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
3883
4803
  }
3884
- if (!spec.allowRepeat && grouped.has(field.key)) {
4804
+ if (!spec?.allowRepeat && grouped.has(field.key)) {
3885
4805
  throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
3886
4806
  }
3887
4807
  const existing = grouped.get(field.key) ?? [];
@@ -3890,7 +4810,7 @@
3890
4810
  }
3891
4811
  return grouped;
3892
4812
  }
3893
- function extractDraftPlacement(objectType, fieldMap) {
4813
+ function extractPlacementFromFieldMap(fieldMap) {
3894
4814
  const orbitField = fieldMap.get("orbit")?.[0];
3895
4815
  const atField = fieldMap.get("at")?.[0];
3896
4816
  const surfaceField = fieldMap.get("surface")?.[0];
@@ -4058,7 +4978,7 @@
4058
4978
  }
4059
4979
  }
4060
4980
  function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
4061
- if (sourceSchemaVersion === "2.1") {
4981
+ if (!isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
4062
4982
  return;
4063
4983
  }
4064
4984
  diagnostics.push({
@@ -4070,6 +4990,34 @@
4070
4990
  column: location.column
4071
4991
  });
4072
4992
  }
4993
+ function warnIfSchema25Feature(sourceSchemaVersion, diagnostics, featureName, location) {
4994
+ if (!isSchemaOlderThan(sourceSchemaVersion, "2.5")) {
4995
+ return;
4996
+ }
4997
+ diagnostics.push({
4998
+ code: "parse.schema25.featureCompatibility",
4999
+ severity: "warning",
5000
+ source: "parse",
5001
+ message: `Feature "${featureName}" requires schema 2.5; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
5002
+ line: location.line,
5003
+ column: location.column
5004
+ });
5005
+ }
5006
+ function isSchemaOlderThan(sourceSchemaVersion, requiredVersion) {
5007
+ return schemaVersionRank(sourceSchemaVersion) < schemaVersionRank(requiredVersion);
5008
+ }
5009
+ function schemaVersionRank(version) {
5010
+ switch (version) {
5011
+ case "2.0-draft":
5012
+ return 0;
5013
+ case "2.0":
5014
+ return 1;
5015
+ case "2.1":
5016
+ return 2;
5017
+ case "2.5":
5018
+ return 3;
5019
+ }
5020
+ }
4073
5021
  function preprocessAtlasSource(source) {
4074
5022
  const chars = [...source];
4075
5023
  const comments = [];
@@ -4157,8 +5105,9 @@
4157
5105
  }
4158
5106
 
4159
5107
  // packages/core/dist/load.js
4160
- var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1)?$/i;
5108
+ var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1|\.5)?$/i;
4161
5109
  var ATLAS_SCHEMA_21_PATTERN = /^schema\s+2\.1$/i;
5110
+ var ATLAS_SCHEMA_25_PATTERN = /^schema\s+2\.5$/i;
4162
5111
  var LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
4163
5112
  function detectWorldOrbitSchemaVersion(source) {
4164
5113
  for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
@@ -4172,6 +5121,9 @@
4172
5121
  if (ATLAS_SCHEMA_21_PATTERN.test(trimmed)) {
4173
5122
  return "2.1";
4174
5123
  }
5124
+ if (ATLAS_SCHEMA_25_PATTERN.test(trimmed)) {
5125
+ return "2.5";
5126
+ }
4175
5127
  if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
4176
5128
  return "2.0";
4177
5129
  }
@@ -4232,7 +5184,7 @@
4232
5184
  }
4233
5185
  function loadWorldOrbitSourceWithDiagnostics(source) {
4234
5186
  const schemaVersion = detectWorldOrbitSchemaVersion(source);
4235
- if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1") {
5187
+ if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1" || schemaVersion === "2.5") {
4236
5188
  return loadAtlasSourceWithDiagnostics(source, schemaVersion);
4237
5189
  }
4238
5190
  let ast;
@@ -4326,6 +5278,7 @@
4326
5278
  background: true,
4327
5279
  guides: true,
4328
5280
  relations: true,
5281
+ events: true,
4329
5282
  orbits: true,
4330
5283
  objects: true,
4331
5284
  labels: true,
@@ -4529,6 +5482,7 @@
4529
5482
  const orbitMarkup = layers.orbits ? renderOrbitLayer(scene, visibleObjectIds, layers.structures) : { back: "", front: "" };
4530
5483
  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("") : "";
4531
5484
  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("") : "";
5485
+ const eventMarkup = layers.events ? scene.events.filter((event) => !event.hidden).map((event) => renderSceneEventOverlay(scene, event, visibleObjectIds, theme)).join("") : "";
4532
5486
  const objectMarkup = layers.objects ? visibleObjects.map((object) => renderSceneObject(object, options.selectedObjectId ?? null, theme)).join("") : "";
4533
5487
  const labelMarkup = layers.labels ? visibleLabels.map((label) => renderSceneLabel(scene, label, options.selectedObjectId ?? null)).join("") : "";
4534
5488
  const metadataMarkup = layers.metadata ? `<text class="wo-title" x="56" y="64">${escapeXml(scene.title)}</text>
@@ -4564,6 +5518,9 @@
4564
5518
  .wo-orbit-front { opacity: 0.9; }
4565
5519
  .wo-orbit-band { stroke: ${theme.orbitBand}; stroke-linecap: round; }
4566
5520
  .wo-relation { stroke: ${theme.relation}; stroke-width: 2; stroke-dasharray: 10 6; }
5521
+ .wo-event-line { stroke: ${theme.accent}; stroke-width: 1.6; stroke-dasharray: 5 5; opacity: 0.72; }
5522
+ .wo-event-node { fill: ${theme.accent}; stroke: ${theme.selected}; stroke-width: 1.4; opacity: 0.92; }
5523
+ .wo-event-label { fill: ${theme.accent}; font-family: ${theme.fontFamily}; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
4567
5524
  .wo-leader { stroke: ${theme.leader}; stroke-width: 1.5; stroke-dasharray: 6 5; }
4568
5525
  .wo-label { fill: ${theme.ink}; font-family: ${theme.fontFamily}; font-weight: 600; letter-spacing: 0.02em; }
4569
5526
  .wo-label-secondary { fill: ${theme.muted}; font-family: ${theme.fontFamily}; font-weight: 500; }
@@ -4598,6 +5555,7 @@
4598
5555
  ${layers.orbits ? `<g data-layer-id="orbits-back">${orbitMarkup.back}</g>` : ""}
4599
5556
  ${layers.guides ? `<g data-layer-id="guides">${leaderMarkup}</g>` : ""}
4600
5557
  ${layers.relations ? `<g data-layer-id="relations">${relationMarkup}</g>` : ""}
5558
+ ${layers.events ? `<g data-layer-id="events">${eventMarkup}</g>` : ""}
4601
5559
  ${layers.objects ? `<g data-layer-id="objects">${objectMarkup}</g>` : ""}
4602
5560
  ${layers.orbits ? `<g data-layer-id="orbits-front">${orbitMarkup.front}</g>` : ""}
4603
5561
  ${layers.labels ? `<g data-layer-id="labels">${labelMarkup}</g>` : ""}
@@ -4605,6 +5563,20 @@
4605
5563
  </g>
4606
5564
  </g>
4607
5565
  </svg>`;
5566
+ }
5567
+ function renderSceneEventOverlay(scene, event, visibleObjectIds, theme) {
5568
+ const participants = event.objectIds.filter((objectId) => visibleObjectIds.has(objectId)).map((objectId) => scene.objects.find((object) => object.objectId === objectId && !object.hidden)).filter(Boolean);
5569
+ if (participants.length === 0) {
5570
+ return "";
5571
+ }
5572
+ const stroke = event.event.color || theme.accent;
5573
+ const label = event.event.label || event.event.id;
5574
+ 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("");
5575
+ return `<g class="wo-event" data-render-id="${escapeXml(event.renderId)}" data-event-id="${escapeAttribute(event.eventId)}">
5576
+ ${lineMarkup}
5577
+ <circle class="wo-event-node" cx="${event.x}" cy="${event.y}" r="5" fill="${escapeAttribute(stroke)}" />
5578
+ <text class="wo-event-label" x="${event.x}" y="${event.y - 10}" text-anchor="middle" font-size="10">${escapeXml(label)}</text>
5579
+ </g>`;
4608
5580
  }
4609
5581
  function renderOrbitLayer(scene, visibleObjectIds, includeStructures) {
4610
5582
  const backParts = [];