worldorbit 2.5.15 → 2.5.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +1 -1
  2. package/dist/browser/core/dist/index.js +2444 -342
  3. package/dist/browser/editor/dist/index.js +11702 -0
  4. package/dist/browser/markdown/dist/index.js +2207 -392
  5. package/dist/browser/viewer/dist/index.js +2302 -382
  6. package/dist/unpkg/core/dist/index.js +2447 -345
  7. package/dist/unpkg/editor/dist/index.js +11727 -0
  8. package/dist/unpkg/markdown/dist/index.js +2210 -395
  9. package/dist/unpkg/viewer/dist/index.js +2305 -385
  10. package/dist/unpkg/worldorbit-core.min.js +12 -12
  11. package/dist/unpkg/worldorbit-editor.min.js +894 -0
  12. package/dist/unpkg/worldorbit-markdown.min.js +66 -58
  13. package/dist/unpkg/worldorbit-viewer.min.js +76 -68
  14. package/dist/unpkg/worldorbit.js +797 -78
  15. package/dist/unpkg/worldorbit.min.js +80 -72
  16. package/package.json +3 -2
  17. package/packages/core/dist/atlas-edit.js +74 -0
  18. package/packages/core/dist/atlas-validate.js +122 -8
  19. package/packages/core/dist/draft-parse.js +212 -8
  20. package/packages/core/dist/draft.d.ts +5 -2
  21. package/packages/core/dist/draft.js +59 -3
  22. package/packages/core/dist/format.js +63 -1
  23. package/packages/core/dist/normalize.js +1 -0
  24. package/packages/core/dist/scene.js +248 -46
  25. package/packages/core/dist/types.d.ts +41 -2
  26. package/packages/editor/dist/editor.d.ts +2 -0
  27. package/packages/editor/dist/editor.js +3578 -0
  28. package/packages/editor/dist/index.d.ts +2 -0
  29. package/packages/editor/dist/index.js +1 -0
  30. package/packages/editor/dist/types.d.ts +55 -0
  31. package/packages/editor/dist/types.js +1 -0
  32. package/packages/markdown/dist/html.d.ts +3 -0
  33. package/packages/markdown/dist/html.js +57 -0
  34. package/packages/markdown/dist/index.d.ts +4 -0
  35. package/packages/markdown/dist/index.js +3 -0
  36. package/packages/markdown/dist/rehype.d.ts +10 -0
  37. package/packages/markdown/dist/rehype.js +49 -0
  38. package/packages/markdown/dist/remark.d.ts +9 -0
  39. package/packages/markdown/dist/remark.js +28 -0
  40. package/packages/markdown/dist/types.d.ts +11 -0
  41. package/packages/markdown/dist/types.js +1 -0
  42. package/packages/viewer/dist/atlas-state.js +6 -0
  43. package/packages/viewer/dist/atlas-viewer.js +1 -0
  44. package/packages/viewer/dist/render.js +31 -2
  45. package/packages/viewer/dist/theme.js +1 -0
  46. package/packages/viewer/dist/tooltip.js +9 -0
  47. package/packages/viewer/dist/types.d.ts +8 -1
  48. package/packages/viewer/dist/viewer.js +12 -1
@@ -302,13 +302,13 @@
302
302
  function unitFamilyAllowsUnit(family, unit) {
303
303
  switch (family) {
304
304
  case "distance":
305
- return unit === null || ["au", "km", "re", "sol"].includes(unit);
305
+ return unit === null || ["au", "km", "m", "ly", "pc", "kpc", "re", "sol"].includes(unit);
306
306
  case "radius":
307
- return unit === null || ["km", "re", "sol"].includes(unit);
307
+ return unit === null || ["km", "m", "re", "rj", "sol"].includes(unit);
308
308
  case "mass":
309
- return unit === null || ["me", "sol"].includes(unit);
309
+ return unit === null || ["me", "mj", "sol"].includes(unit);
310
310
  case "duration":
311
- return unit === null || ["h", "d", "y"].includes(unit);
311
+ return unit === null || ["s", "min", "h", "d", "y", "ky", "my", "gy"].includes(unit);
312
312
  case "angle":
313
313
  return unit === null || unit === "deg";
314
314
  case "generic":
@@ -515,7 +515,7 @@
515
515
  }
516
516
 
517
517
  // packages/core/dist/normalize.js
518
- var UNIT_PATTERN = /^(-?\d+(?:\.\d+)?)(au|km|re|sol|me|d|y|h|deg)?$/;
518
+ var UNIT_PATTERN = /^(-?\d+(?:\.\d+)?)(kpc|min|mj|rj|ky|my|gy|au|km|me|re|pc|ly|deg|sol|K|m|s|h|d|y)?$/;
519
519
  var BOOLEAN_VALUES = /* @__PURE__ */ new Map([
520
520
  ["true", true],
521
521
  ["false", false],
@@ -540,7 +540,11 @@
540
540
  return {
541
541
  format: "worldorbit",
542
542
  version: "1.0",
543
+ schemaVersion: "1.0",
543
544
  system,
545
+ groups: [],
546
+ relations: [],
547
+ events: [],
544
548
  objects
545
549
  };
546
550
  }
@@ -550,13 +554,17 @@
550
554
  const fieldMap = collectFields(mergedFields);
551
555
  const placement = extractPlacement(node.objectType, fieldMap);
552
556
  const properties = normalizeProperties(fieldMap);
553
- const info = normalizeInfo(node.infoEntries);
557
+ const info2 = normalizeInfo(node.infoEntries);
554
558
  if (node.objectType === "system") {
555
559
  return {
556
560
  type: "system",
557
561
  id: node.name,
562
+ title: typeof properties.title === "string" ? properties.title : null,
563
+ description: null,
564
+ epoch: null,
565
+ referencePlane: null,
558
566
  properties,
559
- info
567
+ info: info2
560
568
  };
561
569
  }
562
570
  return {
@@ -564,7 +572,7 @@
564
572
  id: node.name,
565
573
  properties,
566
574
  placement,
567
- info
575
+ info: info2
568
576
  };
569
577
  }
570
578
  function validateFieldCompatibility(objectType, fields) {
@@ -694,14 +702,14 @@
694
702
  }
695
703
  }
696
704
  function normalizeInfo(entries) {
697
- const info = {};
705
+ const info2 = {};
698
706
  for (const entry of entries) {
699
- if (entry.key in info) {
707
+ if (entry.key in info2) {
700
708
  throw WorldOrbitError.fromLocation(`Duplicate info key "${entry.key}"`, entry.location);
701
709
  }
702
- info[entry.key] = entry.value;
710
+ info2[entry.key] = entry.value;
703
711
  }
704
- return info;
712
+ return info2;
705
713
  }
706
714
  function parseAtReference(target, location) {
707
715
  if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
@@ -875,38 +883,38 @@
875
883
  function createDiagnostic(diagnostic) {
876
884
  return { ...diagnostic };
877
885
  }
878
- function diagnosticFromError(error, source, code = `${source}.failed`) {
879
- if (error instanceof WorldOrbitError) {
886
+ function diagnosticFromError(error2, source, code = `${source}.failed`) {
887
+ if (error2 instanceof WorldOrbitError) {
880
888
  return {
881
889
  code,
882
890
  severity: "error",
883
891
  source,
884
- message: error.message,
885
- line: error.line,
886
- column: error.column
892
+ message: error2.message,
893
+ line: error2.line,
894
+ column: error2.column
887
895
  };
888
896
  }
889
- if (error instanceof Error) {
897
+ if (error2 instanceof Error) {
890
898
  return {
891
899
  code,
892
900
  severity: "error",
893
901
  source,
894
- message: error.message
902
+ message: error2.message
895
903
  };
896
904
  }
897
905
  return {
898
906
  code,
899
907
  severity: "error",
900
908
  source,
901
- message: String(error)
909
+ message: String(error2)
902
910
  };
903
911
  }
904
912
  function parseWithDiagnostics(source) {
905
913
  let ast;
906
914
  try {
907
915
  ast = parseWorldOrbit(source);
908
- } catch (error) {
909
- const diagnostic = diagnosticFromError(error, "parse");
916
+ } catch (error2) {
917
+ const diagnostic = diagnosticFromError(error2, "parse");
910
918
  return {
911
919
  ok: false,
912
920
  value: null,
@@ -916,20 +924,20 @@
916
924
  let document;
917
925
  try {
918
926
  document = normalizeDocument(ast);
919
- } catch (error) {
927
+ } catch (error2) {
920
928
  return {
921
929
  ok: false,
922
930
  value: null,
923
- diagnostics: [diagnosticFromError(error, "normalize")]
931
+ diagnostics: [diagnosticFromError(error2, "normalize")]
924
932
  };
925
933
  }
926
934
  try {
927
935
  validateDocument(document);
928
- } catch (error) {
936
+ } catch (error2) {
929
937
  return {
930
938
  ok: false,
931
939
  value: null,
932
- diagnostics: [diagnosticFromError(error, "validate")]
940
+ diagnostics: [diagnosticFromError(error2, "validate")]
933
941
  };
934
942
  }
935
943
  return {
@@ -948,11 +956,11 @@
948
956
  value: normalizeDocument(ast),
949
957
  diagnostics: []
950
958
  };
951
- } catch (error) {
959
+ } catch (error2) {
952
960
  return {
953
961
  ok: false,
954
962
  value: null,
955
- diagnostics: [diagnosticFromError(error, "normalize")]
963
+ diagnostics: [diagnosticFromError(error2, "normalize")]
956
964
  };
957
965
  }
958
966
  }
@@ -964,11 +972,11 @@
964
972
  value: document,
965
973
  diagnostics: []
966
974
  };
967
- } catch (error) {
975
+ } catch (error2) {
968
976
  return {
969
977
  ok: false,
970
978
  value: null,
971
- diagnostics: [diagnosticFromError(error, "validate")]
979
+ diagnostics: [diagnosticFromError(error2, "validate")]
972
980
  };
973
981
  }
974
982
  }
@@ -976,7 +984,11 @@
976
984
  // packages/core/dist/scene.js
977
985
  var AU_IN_KM = 1495978707e-1;
978
986
  var EARTH_RADIUS_IN_KM = 6371;
987
+ var JUPITER_RADIUS_IN_KM = 71492;
979
988
  var SOLAR_RADIUS_IN_KM = 695700;
989
+ var LY_IN_AU = 63241.077;
990
+ var PC_IN_AU = 206264.806;
991
+ var KPC_IN_AU = 206264806;
980
992
  var ISO_FLATTENING = 0.68;
981
993
  var MIN_ISO_MINOR_SCALE = 0.2;
982
994
  var ARC_SAMPLE_COUNT = 28;
@@ -990,8 +1002,10 @@
990
1002
  const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
991
1003
  const spacingFactor = layoutPresetSpacing(layoutPreset);
992
1004
  const systemId = document.system?.id ?? null;
993
- const objectMap = new Map(document.objects.map((object) => [object.id, object]));
994
- const relationships = buildSceneRelationships(document.objects, objectMap);
1005
+ const activeEventId = options.activeEventId ?? null;
1006
+ const effectiveObjects = createEffectiveObjects(document.objects, document.events ?? [], activeEventId);
1007
+ const objectMap = new Map(effectiveObjects.map((object) => [object.id, object]));
1008
+ const relationships = buildSceneRelationships(effectiveObjects, objectMap);
995
1009
  const positions = /* @__PURE__ */ new Map();
996
1010
  const orbitDrafts = [];
997
1011
  const leaderDrafts = [];
@@ -1000,7 +1014,7 @@
1000
1014
  const atObjects = [];
1001
1015
  const surfaceChildren = /* @__PURE__ */ new Map();
1002
1016
  const orbitChildren = /* @__PURE__ */ new Map();
1003
- for (const object of document.objects) {
1017
+ for (const object of effectiveObjects) {
1004
1018
  const placement = object.placement;
1005
1019
  if (!placement) {
1006
1020
  rootObjects.push(object);
@@ -1095,11 +1109,14 @@
1095
1109
  const objects = [...positions.values()].map((position) => createSceneObject(position, scaleModel, relationships));
1096
1110
  const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
1097
1111
  const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
1098
- const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
1099
- const layers = createSceneLayers(orbitVisuals, leaders, objects, labels);
1100
- const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
1112
+ const labels = createSceneLabels(objects, width, height, scaleModel.labelMultiplier);
1113
+ const relations = createSceneRelations(document, objects);
1114
+ const events = createSceneEvents(document.events ?? [], objects, activeEventId);
1115
+ const layers = createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels);
1116
+ const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, scaleModel.labelMultiplier);
1117
+ const semanticGroups = createSceneSemanticGroups(document, objects);
1101
1118
  const viewpoints = createSceneViewpoints(document, projection, frame.preset, relationships, objectMap);
1102
- const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
1119
+ const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, scaleModel.labelMultiplier);
1103
1120
  return {
1104
1121
  width,
1105
1122
  height,
@@ -1107,7 +1124,7 @@
1107
1124
  renderPreset: frame.preset,
1108
1125
  projection,
1109
1126
  scaleModel,
1110
- title: String(document.system?.properties.title ?? document.system?.id ?? "WorldOrbit") || "WorldOrbit",
1127
+ title: String(document.system?.title ?? document.system?.properties.title ?? document.system?.id ?? "WorldOrbit") || "WorldOrbit",
1111
1128
  subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
1112
1129
  systemId,
1113
1130
  viewMode: projection,
@@ -1123,9 +1140,13 @@
1123
1140
  contentBounds,
1124
1141
  layers,
1125
1142
  groups,
1143
+ semanticGroups,
1126
1144
  viewpoints,
1145
+ events,
1146
+ activeEventId,
1127
1147
  objects,
1128
1148
  orbitVisuals,
1149
+ relations,
1129
1150
  leaders,
1130
1151
  labels
1131
1152
  };
@@ -1141,6 +1162,35 @@
1141
1162
  y: center.y + dx * sin + dy * cos
1142
1163
  };
1143
1164
  }
1165
+ function createEffectiveObjects(objects, events, activeEventId) {
1166
+ const cloned = objects.map((object) => structuredClone(object));
1167
+ if (!activeEventId) {
1168
+ return cloned;
1169
+ }
1170
+ const activeEvent = events.find((event) => event.id === activeEventId);
1171
+ if (!activeEvent) {
1172
+ return cloned;
1173
+ }
1174
+ const objectMap = new Map(cloned.map((object) => [object.id, object]));
1175
+ for (const pose of activeEvent.positions) {
1176
+ const object = objectMap.get(pose.objectId);
1177
+ if (!object) {
1178
+ continue;
1179
+ }
1180
+ object.placement = pose.placement ? structuredClone(pose.placement) : null;
1181
+ if (pose.inner) {
1182
+ object.properties.inner = { ...pose.inner };
1183
+ } else {
1184
+ delete object.properties.inner;
1185
+ }
1186
+ if (pose.outer) {
1187
+ object.properties.outer = { ...pose.outer };
1188
+ } else {
1189
+ delete object.properties.outer;
1190
+ }
1191
+ }
1192
+ return cloned;
1193
+ }
1144
1194
  function resolveLayoutPreset(document) {
1145
1195
  const rawScale = String(document.system?.properties.scale ?? "balanced").toLowerCase();
1146
1196
  switch (rawScale) {
@@ -1235,6 +1285,7 @@
1235
1285
  }
1236
1286
  function createSceneObject(position, scaleModel, relationships) {
1237
1287
  const { object, x, y, radius, sortKey, anchorX, anchorY } = position;
1288
+ const renderPriority = object.renderHints?.renderPriority ?? 0;
1238
1289
  return {
1239
1290
  renderId: createRenderId(object.id),
1240
1291
  objectId: object.id,
@@ -1243,11 +1294,12 @@
1243
1294
  ancestorIds: relationships.ancestorIds.get(object.id) ?? [],
1244
1295
  childIds: relationships.childIds.get(object.id) ?? [],
1245
1296
  groupId: relationships.groupIds.get(object.id) ?? null,
1297
+ semanticGroupIds: [...object.groups ?? []],
1246
1298
  x,
1247
1299
  y,
1248
1300
  radius,
1249
1301
  visualRadius: visualExtentForObject(object, radius, scaleModel),
1250
- sortKey,
1302
+ sortKey: sortKey + renderPriority * 1e-3,
1251
1303
  anchorX,
1252
1304
  anchorY,
1253
1305
  label: object.id,
@@ -1264,6 +1316,7 @@
1264
1316
  object: draft.object,
1265
1317
  parentId: draft.parentId,
1266
1318
  groupId,
1319
+ semanticGroupIds: [...draft.object.groups ?? []],
1267
1320
  kind: draft.kind,
1268
1321
  cx: draft.cx,
1269
1322
  cy: draft.cy,
@@ -1275,7 +1328,7 @@
1275
1328
  bandThickness: draft.bandThickness,
1276
1329
  frontArcPath: draft.frontArcPath,
1277
1330
  backArcPath: draft.backArcPath,
1278
- hidden: draft.object.properties.hidden === true
1331
+ hidden: draft.object.properties.hidden === true || draft.object.renderHints?.renderOrbit === false
1279
1332
  };
1280
1333
  }
1281
1334
  function createLeaderLine(draft) {
@@ -1284,6 +1337,7 @@
1284
1337
  objectId: draft.object.id,
1285
1338
  object: draft.object,
1286
1339
  groupId: draft.groupId,
1340
+ semanticGroupIds: [...draft.object.groups ?? []],
1287
1341
  x1: draft.x1,
1288
1342
  y1: draft.y1,
1289
1343
  x2: draft.x2,
@@ -1292,42 +1346,144 @@
1292
1346
  hidden: draft.object.properties.hidden === true
1293
1347
  };
1294
1348
  }
1295
- function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1349
+ function createSceneLabels(objects, sceneWidth, sceneHeight, labelMultiplier) {
1296
1350
  const labels = [];
1297
1351
  const occupied = [];
1298
- const visibleObjects = [...objects].filter((object) => !object.hidden).sort((left, right) => left.sortKey - right.sortKey);
1352
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1353
+ const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort(compareLabelPlacementOrder);
1299
1354
  for (const object of visibleObjects) {
1300
- const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1301
- const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
1302
- let labelY = object.y + direction * (object.radius + 18 * labelMultiplier);
1303
- let secondaryY = labelY + direction * (16 * labelMultiplier);
1304
- let bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1305
- let attempts = 0;
1306
- while (occupied.some((entry) => rectsOverlap(entry, bounds)) && attempts < 10) {
1307
- labelY += direction * 14 * labelMultiplier;
1308
- secondaryY += direction * 14 * labelMultiplier;
1309
- bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1310
- attempts += 1;
1311
- }
1312
- occupied.push(bounds);
1355
+ const placement = selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) ?? createLabelPlacement(object, defaultVerticalDirection(object, objectMap.get(object.parentId ?? "") ?? null, sceneHeight), 0, labelMultiplier);
1356
+ occupied.push(createLabelRect(object, placement, labelMultiplier));
1313
1357
  labels.push({
1314
1358
  renderId: `${object.renderId}-label`,
1315
1359
  objectId: object.objectId,
1316
1360
  object: object.object,
1317
1361
  groupId: object.groupId,
1362
+ semanticGroupIds: [...object.semanticGroupIds],
1318
1363
  label: object.label,
1319
1364
  secondaryLabel: object.secondaryLabel,
1320
- x: object.x,
1321
- y: labelY,
1322
- secondaryY,
1323
- textAnchor: "middle",
1324
- direction: direction < 0 ? "above" : "below",
1365
+ x: placement.x,
1366
+ y: placement.labelY,
1367
+ secondaryY: placement.secondaryY,
1368
+ textAnchor: placement.textAnchor,
1369
+ direction: placement.direction,
1325
1370
  hidden: object.hidden
1326
1371
  });
1327
1372
  }
1328
1373
  return labels;
1329
1374
  }
1330
- function createSceneLayers(orbitVisuals, leaders, objects, labels) {
1375
+ function compareLabelPlacementOrder(left, right) {
1376
+ const priorityDiff = labelPlacementPriority(left) - labelPlacementPriority(right);
1377
+ if (priorityDiff !== 0) {
1378
+ return priorityDiff;
1379
+ }
1380
+ const renderPriorityDiff = (right.object.renderHints?.renderPriority ?? 0) - (left.object.renderHints?.renderPriority ?? 0);
1381
+ if (renderPriorityDiff !== 0) {
1382
+ return renderPriorityDiff;
1383
+ }
1384
+ return left.sortKey - right.sortKey;
1385
+ }
1386
+ function labelPlacementPriority(object) {
1387
+ switch (object.object.type) {
1388
+ case "star":
1389
+ return 0;
1390
+ case "planet":
1391
+ return 1;
1392
+ case "moon":
1393
+ return 2;
1394
+ case "belt":
1395
+ case "ring":
1396
+ return 3;
1397
+ case "asteroid":
1398
+ case "comet":
1399
+ return 4;
1400
+ case "structure":
1401
+ case "phenomenon":
1402
+ return 5;
1403
+ }
1404
+ }
1405
+ function selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) {
1406
+ for (const direction of preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight)) {
1407
+ const maxAttempts = direction === "left" || direction === "right" ? 4 : 6;
1408
+ for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
1409
+ const placement = createLabelPlacement(object, direction, attempt, labelMultiplier);
1410
+ const rect = createLabelRect(object, placement, labelMultiplier);
1411
+ if (!occupied.some((entry) => rectsOverlap(entry, rect))) {
1412
+ return placement;
1413
+ }
1414
+ }
1415
+ }
1416
+ return null;
1417
+ }
1418
+ function preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight) {
1419
+ const parent = object.parentId ? objectMap.get(object.parentId) ?? null : null;
1420
+ const vertical = defaultVerticalDirection(object, parent, sceneHeight);
1421
+ const oppositeVertical = vertical === "below" ? "above" : "below";
1422
+ const horizontal = defaultHorizontalDirection(object, parent, sceneWidth);
1423
+ const oppositeHorizontal = horizontal === "right" ? "left" : "right";
1424
+ 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";
1425
+ return preferHorizontal ? [horizontal, vertical, oppositeHorizontal, oppositeVertical] : [vertical, horizontal, oppositeVertical, oppositeHorizontal];
1426
+ }
1427
+ function defaultVerticalDirection(object, parent, sceneHeight) {
1428
+ if (parent && Math.abs(object.y - parent.y) > 6) {
1429
+ return object.y >= parent.y ? "below" : "above";
1430
+ }
1431
+ return object.y > sceneHeight * 0.62 ? "above" : "below";
1432
+ }
1433
+ function defaultHorizontalDirection(object, parent, sceneWidth) {
1434
+ if (parent && Math.abs(object.x - parent.x) > 6) {
1435
+ return object.x >= parent.x ? "right" : "left";
1436
+ }
1437
+ return object.x >= sceneWidth / 2 ? "right" : "left";
1438
+ }
1439
+ function createLabelPlacement(object, direction, attempt, labelMultiplier) {
1440
+ const step = 14 * labelMultiplier;
1441
+ switch (direction) {
1442
+ case "above": {
1443
+ const labelY = object.y - (object.radius + 18 * labelMultiplier + attempt * step);
1444
+ return {
1445
+ x: object.x,
1446
+ labelY,
1447
+ secondaryY: labelY - 16 * labelMultiplier,
1448
+ textAnchor: "middle",
1449
+ direction
1450
+ };
1451
+ }
1452
+ case "below": {
1453
+ const labelY = object.y + object.radius + 18 * labelMultiplier + attempt * step;
1454
+ return {
1455
+ x: object.x,
1456
+ labelY,
1457
+ secondaryY: labelY + 16 * labelMultiplier,
1458
+ textAnchor: "middle",
1459
+ direction
1460
+ };
1461
+ }
1462
+ case "left": {
1463
+ const x = object.x - (object.visualRadius + 16 * labelMultiplier + attempt * step);
1464
+ const labelY = object.y - 4 * labelMultiplier;
1465
+ return {
1466
+ x,
1467
+ labelY,
1468
+ secondaryY: labelY + 16 * labelMultiplier,
1469
+ textAnchor: "end",
1470
+ direction
1471
+ };
1472
+ }
1473
+ case "right": {
1474
+ const x = object.x + object.visualRadius + 16 * labelMultiplier + attempt * step;
1475
+ const labelY = object.y - 4 * labelMultiplier;
1476
+ return {
1477
+ x,
1478
+ labelY,
1479
+ secondaryY: labelY + 16 * labelMultiplier,
1480
+ textAnchor: "start",
1481
+ direction
1482
+ };
1483
+ }
1484
+ }
1485
+ }
1486
+ function createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels) {
1331
1487
  const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1332
1488
  const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1333
1489
  return [
@@ -1338,6 +1494,14 @@
1338
1494
  },
1339
1495
  { id: "orbits-back", renderIds: backOrbitIds },
1340
1496
  { id: "orbits-front", renderIds: frontOrbitIds },
1497
+ {
1498
+ id: "relations",
1499
+ renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId)
1500
+ },
1501
+ {
1502
+ id: "events",
1503
+ renderIds: events.filter((event) => !event.hidden).map((event) => event.renderId)
1504
+ },
1341
1505
  {
1342
1506
  id: "objects",
1343
1507
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
@@ -1349,7 +1513,7 @@
1349
1513
  { id: "metadata", renderIds: ["wo-title", "wo-subtitle", "wo-meta"] }
1350
1514
  ];
1351
1515
  }
1352
- function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships) {
1516
+ function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, labelMultiplier) {
1353
1517
  const groups = /* @__PURE__ */ new Map();
1354
1518
  const ensureGroup = (groupId) => {
1355
1519
  if (!groupId) {
@@ -1398,10 +1562,63 @@
1398
1562
  }
1399
1563
  }
1400
1564
  for (const group of groups.values()) {
1401
- group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels);
1565
+ group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier);
1402
1566
  }
1403
1567
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1404
1568
  }
1569
+ function createSceneSemanticGroups(document, objects) {
1570
+ return [...document.groups].map((group) => ({
1571
+ id: group.id,
1572
+ label: group.label,
1573
+ summary: group.summary,
1574
+ color: group.color,
1575
+ tags: [...group.tags],
1576
+ hidden: group.hidden,
1577
+ objectIds: objects.filter((object) => !object.hidden && object.semanticGroupIds.includes(group.id)).map((object) => object.objectId)
1578
+ })).sort((left, right) => left.label.localeCompare(right.label));
1579
+ }
1580
+ function createSceneRelations(document, objects) {
1581
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1582
+ return document.relations.map((relation) => {
1583
+ const from = objectMap.get(relation.from);
1584
+ const to = objectMap.get(relation.to);
1585
+ return {
1586
+ renderId: `${createRenderId(relation.id)}-relation`,
1587
+ relationId: relation.id,
1588
+ relation,
1589
+ fromObjectId: relation.from,
1590
+ toObjectId: relation.to,
1591
+ x1: from?.x ?? 0,
1592
+ y1: from?.y ?? 0,
1593
+ x2: to?.x ?? 0,
1594
+ y2: to?.y ?? 0,
1595
+ hidden: relation.hidden || !from || !to || from.hidden || to.hidden
1596
+ };
1597
+ }).sort((left, right) => left.relation.id.localeCompare(right.relation.id));
1598
+ }
1599
+ function createSceneEvents(events, objects, activeEventId) {
1600
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1601
+ return events.map((event) => {
1602
+ const objectIds = [.../* @__PURE__ */ new Set([
1603
+ ...event.targetObjectId ? [event.targetObjectId] : [],
1604
+ ...event.participantObjectIds
1605
+ ])];
1606
+ const positions = objectIds.map((objectId) => objectMap.get(objectId)).filter(Boolean);
1607
+ const centroidX = positions.length > 0 ? positions.reduce((sum, object) => sum + object.x, 0) / positions.length : 0;
1608
+ const centroidY = positions.length > 0 ? positions.reduce((sum, object) => sum + object.y, 0) / positions.length : 0;
1609
+ return {
1610
+ renderId: `${createRenderId(event.id)}-event`,
1611
+ eventId: event.id,
1612
+ event,
1613
+ objectIds,
1614
+ participantIds: [...event.participantObjectIds],
1615
+ targetObjectId: event.targetObjectId,
1616
+ x: centroidX,
1617
+ y: centroidY,
1618
+ hidden: event.hidden || positions.length === 0 || positions.every((object) => object.hidden) || activeEventId !== null && event.id !== activeEventId
1619
+ };
1620
+ }).sort((left, right) => left.event.id.localeCompare(right.event.id));
1621
+ }
1405
1622
  function createSceneViewpoints(document, projection, preset, relationships, objectMap) {
1406
1623
  const generatedOverview = createGeneratedOverviewViewpoint(document, projection, preset);
1407
1624
  const drafts = /* @__PURE__ */ new Map();
@@ -1419,7 +1636,7 @@
1419
1636
  }
1420
1637
  const field = fieldParts.join(".").toLowerCase();
1421
1638
  const draft = drafts.get(id) ?? { id };
1422
- applyViewpointField(draft, field, value, projection, preset, relationships, objectMap);
1639
+ applyViewpointField(draft, field, value, document, projection, preset, relationships, objectMap);
1423
1640
  drafts.set(id, draft);
1424
1641
  }
1425
1642
  const viewpoints = [...drafts.values()].map((draft) => finalizeViewpointDraft(draft, projection, preset, objectMap)).filter(Boolean);
@@ -1447,13 +1664,15 @@
1447
1664
  });
1448
1665
  }
1449
1666
  function createGeneratedOverviewViewpoint(document, projection, preset) {
1450
- const label = document.system?.properties.title ? `${String(document.system.properties.title)} Overview` : "Overview";
1667
+ const title = document.system?.title ?? document.system?.properties.title;
1668
+ const label = title ? `${String(title)} Overview` : "Overview";
1451
1669
  return {
1452
1670
  id: "overview",
1453
1671
  label,
1454
1672
  summary: "Fit the whole system with the current atlas defaults.",
1455
1673
  objectId: null,
1456
1674
  selectedObjectId: null,
1675
+ eventIds: [],
1457
1676
  projection,
1458
1677
  preset,
1459
1678
  rotationDeg: 0,
@@ -1463,7 +1682,7 @@
1463
1682
  generated: true
1464
1683
  };
1465
1684
  }
1466
- function applyViewpointField(draft, field, value, projection, preset, relationships, objectMap) {
1685
+ function applyViewpointField(draft, field, value, document, projection, preset, relationships, objectMap) {
1467
1686
  const normalizedValue = value.trim();
1468
1687
  switch (field) {
1469
1688
  case "label":
@@ -1490,6 +1709,9 @@
1490
1709
  draft.select = normalizedValue;
1491
1710
  }
1492
1711
  return;
1712
+ case "events":
1713
+ draft.eventIds = splitListValue(normalizedValue);
1714
+ return;
1493
1715
  case "projection":
1494
1716
  case "view":
1495
1717
  draft.projection = parseViewProjection(normalizedValue) ?? projection;
@@ -1530,7 +1752,7 @@
1530
1752
  case "groups":
1531
1753
  draft.filter = {
1532
1754
  ...draft.filter ?? createEmptyViewpointFilter(),
1533
- groupIds: parseViewpointGroups(normalizedValue, relationships, objectMap)
1755
+ groupIds: parseViewpointGroups(normalizedValue, document, relationships, objectMap)
1534
1756
  };
1535
1757
  return;
1536
1758
  }
@@ -1546,6 +1768,7 @@
1546
1768
  summary: draft.summary?.trim() || createViewpointSummary(label, objectId, filter),
1547
1769
  objectId,
1548
1770
  selectedObjectId,
1771
+ eventIds: [...new Set(draft.eventIds ?? [])],
1549
1772
  projection: draft.projection ?? projection,
1550
1773
  preset: draft.preset ?? preset,
1551
1774
  rotationDeg: draft.rotationDeg ?? 0,
@@ -1603,7 +1826,7 @@
1603
1826
  next["orbits-front"] = enabled;
1604
1827
  continue;
1605
1828
  }
1606
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1829
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "events" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1607
1830
  next[rawLayer] = enabled;
1608
1831
  }
1609
1832
  }
@@ -1612,8 +1835,11 @@
1612
1835
  function parseViewpointObjectTypes(value) {
1613
1836
  return splitListValue(value).filter((entry) => entry === "star" || entry === "planet" || entry === "moon" || entry === "belt" || entry === "asteroid" || entry === "comet" || entry === "ring" || entry === "structure" || entry === "phenomenon");
1614
1837
  }
1615
- function parseViewpointGroups(value, relationships, objectMap) {
1838
+ function parseViewpointGroups(value, document, relationships, objectMap) {
1616
1839
  return splitListValue(value).map((entry) => {
1840
+ if (document.schemaVersion === "2.1" || document.groups.some((group) => group.id === entry)) {
1841
+ return entry;
1842
+ }
1617
1843
  if (entry.startsWith("wo-") && entry.endsWith("-group")) {
1618
1844
  return entry;
1619
1845
  }
@@ -1648,7 +1874,7 @@
1648
1874
  }
1649
1875
  return parts.join(" - ");
1650
1876
  }
1651
- function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels) {
1877
+ function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, labelMultiplier) {
1652
1878
  let minX = Number.POSITIVE_INFINITY;
1653
1879
  let minY = Number.POSITIVE_INFINITY;
1654
1880
  let maxX = Number.NEGATIVE_INFINITY;
@@ -1678,7 +1904,7 @@
1678
1904
  for (const label of labels) {
1679
1905
  if (label.hidden)
1680
1906
  continue;
1681
- includeLabelBounds(label, include);
1907
+ includeLabelBounds(label, include, labelMultiplier);
1682
1908
  }
1683
1909
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
1684
1910
  return createBounds(0, 0, width, height);
@@ -1716,13 +1942,10 @@
1716
1942
  include(object.x - object.visualRadius - 24, object.y - object.visualRadius - 16);
1717
1943
  include(object.x + object.visualRadius + 24, object.y + object.visualRadius + 36);
1718
1944
  }
1719
- function includeLabelBounds(label, include) {
1720
- const labelScale = 1;
1721
- const labelHalfWidth = estimateLabelHalfWidthFromText(label.label, label.secondaryLabel, labelScale);
1722
- include(label.x - labelHalfWidth, label.y - 18);
1723
- include(label.x + labelHalfWidth, label.y + 8);
1724
- include(label.x - labelHalfWidth, label.secondaryY - 14);
1725
- include(label.x + labelHalfWidth, label.secondaryY + 8);
1945
+ function includeLabelBounds(label, include, labelMultiplier) {
1946
+ const bounds = createLabelRectFromText(label.x, label.y, label.secondaryY, label.textAnchor, label.direction, label.label, label.secondaryLabel, labelMultiplier);
1947
+ include(bounds.left, bounds.top);
1948
+ include(bounds.right, bounds.bottom);
1726
1949
  }
1727
1950
  function placeObject(object, x, y, depth, positions, orbitDrafts, leaderDrafts, context) {
1728
1951
  if (positions.has(object.id)) {
@@ -1744,8 +1967,9 @@
1744
1967
  }
1745
1968
  const orbiting = [...context.orbitChildren.get(object.id) ?? []].sort(compareOrbiting);
1746
1969
  const orbitMetricContext = computeOrbitMetricContext(orbiting, parent.radius, context.spacingFactor, context.scaleModel);
1970
+ const orbitRadiiPx = resolveOrbitRadiiPx(orbiting, orbitMetricContext);
1747
1971
  orbiting.forEach((child, index) => {
1748
- const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, context);
1972
+ const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, orbitRadiiPx[index] ?? orbitMetricContext.innerPx, context);
1749
1973
  orbitDrafts.push({
1750
1974
  object: child,
1751
1975
  parentId: object.id,
@@ -1819,7 +2043,8 @@
1819
2043
  metricSpread: 0,
1820
2044
  innerPx,
1821
2045
  stepPx,
1822
- pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx)
2046
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
2047
+ minimumGapPx: stepPx * 0.42
1823
2048
  };
1824
2049
  }
1825
2050
  const minMetric = Math.min(...presentMetrics);
@@ -1832,10 +2057,11 @@
1832
2057
  metricSpread,
1833
2058
  innerPx,
1834
2059
  stepPx,
1835
- pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx)
2060
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
2061
+ minimumGapPx: stepPx * 0.42
1836
2062
  };
1837
2063
  }
1838
- function resolveOrbitGeometry(object, index, count, parent, metricContext, context) {
2064
+ function resolveOrbitGeometry(object, index, count, parent, metricContext, orbitRadiusPx, context) {
1839
2065
  const placement = object.placement;
1840
2066
  const band = object.type === "belt" || object.type === "ring";
1841
2067
  if (!placement || placement.mode !== "orbit") {
@@ -1853,7 +2079,7 @@
1853
2079
  };
1854
2080
  }
1855
2081
  const eccentricity = clampNumber(typeof placement.eccentricity === "number" ? placement.eccentricity : 0, 0, 0.92);
1856
- const semiMajor = resolveOrbitRadiusPx(object, index, metricContext);
2082
+ const semiMajor = orbitRadiusPx;
1857
2083
  const baseMinor = Math.max(semiMajor * Math.sqrt(1 - eccentricity * eccentricity), semiMajor * 0.18);
1858
2084
  const inclinationDeg = unitValueToDegrees(placement.inclination) ?? 0;
1859
2085
  const inclinationScale = context.projection === "isometric" ? Math.max(MIN_ISO_MINOR_SCALE, Math.cos(degreesToRadians(inclinationDeg))) * ISO_FLATTENING : 1;
@@ -1883,15 +2109,19 @@
1883
2109
  objectY: objectPoint.y
1884
2110
  };
1885
2111
  }
1886
- function resolveOrbitRadiusPx(object, index, metricContext) {
1887
- const metric = orbitMetric(object);
1888
- if (metric === null) {
1889
- return metricContext.innerPx + index * metricContext.stepPx;
1890
- }
1891
- if (metricContext.metricSpread > 0) {
1892
- return metricContext.innerPx + (metric - metricContext.minMetric) / metricContext.metricSpread * metricContext.pixelSpread;
1893
- }
1894
- return metricContext.innerPx + Math.log10(metric + 1) * metricContext.stepPx;
2112
+ function resolveOrbitRadiusPx(metric, metricContext) {
2113
+ return metricContext.innerPx + metricContext.stepPx * log2(Math.max(metric, 0) + 1);
2114
+ }
2115
+ function resolveOrbitRadiiPx(objects, metricContext) {
2116
+ const radii = [];
2117
+ objects.forEach((object, index) => {
2118
+ const metric = orbitMetric(object);
2119
+ const fallbackRadius = metricContext.innerPx + index * metricContext.stepPx;
2120
+ const baseRadius = metric === null ? fallbackRadius : resolveOrbitRadiusPx(metric, metricContext);
2121
+ const minimumRadius = index === 0 ? metricContext.innerPx : (radii[index - 1] ?? metricContext.innerPx) + metricContext.minimumGapPx;
2122
+ radii.push(Math.max(baseRadius, minimumRadius));
2123
+ });
2124
+ return radii;
1895
2125
  }
1896
2126
  function orbitMetric(object) {
1897
2127
  if (!object.placement || object.placement.mode !== "orbit") {
@@ -1899,6 +2129,9 @@
1899
2129
  }
1900
2130
  return toDistanceMetric(object.placement.semiMajor ?? object.placement.distance ?? null);
1901
2131
  }
2132
+ function log2(value) {
2133
+ return Math.log(value) / Math.log(2);
2134
+ }
1902
2135
  function resolveOrbitPhase(phase, index, count) {
1903
2136
  const degreeValue = phase ? unitValueToDegrees(phase) : null;
1904
2137
  if (degreeValue !== null) {
@@ -2102,7 +2335,7 @@
2102
2335
  return null;
2103
2336
  }
2104
2337
  }
2105
- function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels) {
2338
+ function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier) {
2106
2339
  let minX = Number.POSITIVE_INFINITY;
2107
2340
  let minY = Number.POSITIVE_INFINITY;
2108
2341
  let maxX = Number.NEGATIVE_INFINITY;
@@ -2131,7 +2364,7 @@
2131
2364
  }
2132
2365
  for (const label of labels) {
2133
2366
  if (!label.hidden && group.labelIds.includes(label.objectId)) {
2134
- includeLabelBounds(label, include);
2367
+ includeLabelBounds(label, include, labelMultiplier);
2135
2368
  }
2136
2369
  }
2137
2370
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
@@ -2156,12 +2389,28 @@
2156
2389
  }
2157
2390
  return current.id;
2158
2391
  }
2159
- function createLabelRect(x, labelY, secondaryY, labelHalfWidth, direction) {
2392
+ function createLabelRect(object, placement, labelMultiplier) {
2393
+ return createLabelRectFromText(placement.x, placement.labelY, placement.secondaryY, placement.textAnchor, placement.direction, object.label, object.secondaryLabel, labelMultiplier);
2394
+ }
2395
+ function createLabelRectFromText(x, labelY, secondaryY, textAnchor, direction, label, secondaryLabel, labelMultiplier) {
2396
+ const labelHalfWidth = estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier);
2397
+ const labelWidth = labelHalfWidth * 2;
2398
+ const topPadding = direction === "above" ? 18 : 12;
2399
+ const bottomPadding = direction === "above" ? 8 : 12;
2400
+ let left = x - labelHalfWidth;
2401
+ let right = x + labelHalfWidth;
2402
+ if (textAnchor === "start") {
2403
+ left = x;
2404
+ right = x + labelWidth;
2405
+ } else if (textAnchor === "end") {
2406
+ left = x - labelWidth;
2407
+ right = x;
2408
+ }
2160
2409
  return {
2161
- left: x - labelHalfWidth,
2162
- right: x + labelHalfWidth,
2163
- top: Math.min(labelY, secondaryY) - (direction < 0 ? 18 : 12),
2164
- bottom: Math.max(labelY, secondaryY) + (direction < 0 ? 8 : 12)
2410
+ left,
2411
+ right,
2412
+ top: Math.min(labelY, secondaryY) - topPadding,
2413
+ bottom: Math.max(labelY, secondaryY) + bottomPadding
2165
2414
  };
2166
2415
  }
2167
2416
  function rectsOverlap(left, right) {
@@ -2222,8 +2471,18 @@
2222
2471
  return value.value;
2223
2472
  case "km":
2224
2473
  return value.value / AU_IN_KM;
2474
+ case "m":
2475
+ return value.value / 1e3 / AU_IN_KM;
2476
+ case "ly":
2477
+ return value.value * LY_IN_AU;
2478
+ case "pc":
2479
+ return value.value * PC_IN_AU;
2480
+ case "kpc":
2481
+ return value.value * KPC_IN_AU;
2225
2482
  case "re":
2226
2483
  return value.value * EARTH_RADIUS_IN_KM / AU_IN_KM;
2484
+ case "rj":
2485
+ return value.value * JUPITER_RADIUS_IN_KM / AU_IN_KM;
2227
2486
  case "sol":
2228
2487
  return value.value * SOLAR_RADIUS_IN_KM / AU_IN_KM;
2229
2488
  default:
@@ -2338,11 +2597,6 @@
2338
2597
  function customColorFor(value) {
2339
2598
  return typeof value === "string" && value.trim() ? value : void 0;
2340
2599
  }
2341
- function estimateLabelHalfWidth(object, labelMultiplier) {
2342
- const primaryWidth = object.label.length * 4.6 * labelMultiplier + 18;
2343
- const secondaryWidth = object.secondaryLabel.length * 3.9 * labelMultiplier + 18;
2344
- return Math.max(primaryWidth, secondaryWidth, object.visualRadius + 18);
2345
- }
2346
2600
  function estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier) {
2347
2601
  const primaryWidth = label.length * 4.6 * labelMultiplier + 18;
2348
2602
  const secondaryWidth = secondaryLabel.length * 3.9 * labelMultiplier + 18;
@@ -2377,8 +2631,12 @@
2377
2631
  return {
2378
2632
  format: "worldorbit",
2379
2633
  version: "2.0",
2634
+ schemaVersion: "2.0",
2380
2635
  sourceVersion: document.version,
2381
2636
  system,
2637
+ groups: structuredClone(document.groups ?? []),
2638
+ relations: structuredClone(document.relations ?? []),
2639
+ events: structuredClone(document.events ?? []),
2382
2640
  objects: document.objects.map(cloneWorldOrbitObject),
2383
2641
  diagnostics
2384
2642
  };
@@ -2386,18 +2644,28 @@
2386
2644
  function upgradeDocumentToDraftV2(document, options = {}) {
2387
2645
  return convertAtlasDocumentToLegacyDraft(upgradeDocumentToV2(document, options));
2388
2646
  }
2389
- function materializeAtlasDocument(document) {
2647
+ function materializeAtlasDocument(document, options = {}) {
2390
2648
  const system = document.system ? {
2391
2649
  type: "system",
2392
2650
  id: document.system.id,
2651
+ title: document.system.title,
2652
+ description: document.system.description,
2653
+ epoch: document.system.epoch,
2654
+ referencePlane: document.system.referencePlane,
2393
2655
  properties: materializeDraftSystemProperties(document.system),
2394
2656
  info: materializeDraftSystemInfo(document.system)
2395
2657
  } : null;
2658
+ const objects = document.objects.map(cloneWorldOrbitObject);
2659
+ applyEventPoseOverrides(objects, document.events ?? [], options.activeEventId ?? null);
2396
2660
  return {
2397
2661
  format: "worldorbit",
2398
2662
  version: "1.0",
2663
+ schemaVersion: document.version,
2399
2664
  system,
2400
- objects: document.objects.map(cloneWorldOrbitObject)
2665
+ groups: structuredClone(document.groups ?? []),
2666
+ relations: structuredClone(document.relations ?? []),
2667
+ events: document.events.map(cloneWorldOrbitEvent),
2668
+ objects
2401
2669
  };
2402
2670
  }
2403
2671
  function materializeDraftDocument(document) {
@@ -2411,7 +2679,10 @@
2411
2679
  return {
2412
2680
  type: "system",
2413
2681
  id: document.system?.id ?? "WorldOrbit",
2414
- title: typeof document.system?.properties.title === "string" ? document.system.properties.title : null,
2682
+ title: document.system?.title ?? (typeof document.system?.properties.title === "string" ? document.system.properties.title : null),
2683
+ description: document.system?.description ?? null,
2684
+ epoch: document.system?.epoch ?? null,
2685
+ referencePlane: document.system?.referencePlane ?? null,
2415
2686
  defaults,
2416
2687
  atlasMetadata,
2417
2688
  viewpoints: scene.viewpoints.map(mapSceneViewpointToDraftViewpoint),
@@ -2522,6 +2793,7 @@
2522
2793
  summary: viewpoint.summary,
2523
2794
  focusObjectId: viewpoint.objectId,
2524
2795
  selectedObjectId: viewpoint.selectedObjectId,
2796
+ events: [...viewpoint.eventIds],
2525
2797
  projection: viewpoint.projection,
2526
2798
  preset: viewpoint.preset,
2527
2799
  zoom: viewpoint.scale,
@@ -2538,11 +2810,68 @@
2538
2810
  function cloneWorldOrbitObject(object) {
2539
2811
  return {
2540
2812
  ...object,
2813
+ groups: object.groups ? [...object.groups] : void 0,
2814
+ resonance: object.resonance ? { ...object.resonance } : object.resonance,
2815
+ renderHints: object.renderHints ? { ...object.renderHints } : object.renderHints,
2816
+ deriveRules: object.deriveRules ? object.deriveRules.map((rule) => ({ ...rule })) : void 0,
2817
+ validationRules: object.validationRules ? object.validationRules.map((rule) => ({ ...rule })) : void 0,
2818
+ lockedFields: object.lockedFields ? [...object.lockedFields] : void 0,
2819
+ tolerances: object.tolerances ? object.tolerances.map((entry) => ({
2820
+ field: entry.field,
2821
+ value: entry.value && typeof entry.value === "object" && "value" in entry.value ? { value: entry.value.value, unit: entry.value.unit } : Array.isArray(entry.value) ? [...entry.value] : entry.value
2822
+ })) : void 0,
2823
+ typedBlocks: object.typedBlocks ? Object.fromEntries(Object.entries(object.typedBlocks).map(([key, block]) => [key, { ...block ?? {} }])) : void 0,
2541
2824
  properties: cloneProperties(object.properties),
2542
2825
  placement: object.placement ? structuredClone(object.placement) : null,
2543
2826
  info: { ...object.info }
2544
2827
  };
2545
2828
  }
2829
+ function cloneWorldOrbitEvent(event) {
2830
+ return {
2831
+ ...event,
2832
+ participantObjectIds: [...event.participantObjectIds],
2833
+ tags: [...event.tags],
2834
+ positions: event.positions.map(cloneWorldOrbitEventPose)
2835
+ };
2836
+ }
2837
+ function cloneWorldOrbitEventPose(pose) {
2838
+ return {
2839
+ objectId: pose.objectId,
2840
+ placement: clonePlacement(pose.placement),
2841
+ inner: pose.inner ? { ...pose.inner } : void 0,
2842
+ outer: pose.outer ? { ...pose.outer } : void 0
2843
+ };
2844
+ }
2845
+ function clonePlacement(placement) {
2846
+ return placement ? structuredClone(placement) : null;
2847
+ }
2848
+ function applyEventPoseOverrides(objects, events, activeEventId) {
2849
+ if (!activeEventId) {
2850
+ return;
2851
+ }
2852
+ const event = events.find((entry) => entry.id === activeEventId);
2853
+ if (!event) {
2854
+ return;
2855
+ }
2856
+ const objectMap = new Map(objects.map((object) => [object.id, object]));
2857
+ for (const pose of event.positions) {
2858
+ const object = objectMap.get(pose.objectId);
2859
+ if (!object) {
2860
+ continue;
2861
+ }
2862
+ object.placement = clonePlacement(pose.placement);
2863
+ if (pose.inner) {
2864
+ object.properties.inner = { ...pose.inner };
2865
+ } else {
2866
+ delete object.properties.inner;
2867
+ }
2868
+ if (pose.outer) {
2869
+ object.properties.outer = { ...pose.outer };
2870
+ } else {
2871
+ delete object.properties.outer;
2872
+ }
2873
+ }
2874
+ }
2546
2875
  function cloneProperties(properties) {
2547
2876
  const next = {};
2548
2877
  for (const [key, value] of Object.entries(properties)) {
@@ -2582,71 +2911,83 @@
2582
2911
  if (system.defaults.units) {
2583
2912
  properties.units = system.defaults.units;
2584
2913
  }
2914
+ if (system.description) {
2915
+ properties.description = system.description;
2916
+ }
2917
+ if (system.epoch) {
2918
+ properties.epoch = system.epoch;
2919
+ }
2920
+ if (system.referencePlane) {
2921
+ properties.referencePlane = system.referencePlane;
2922
+ }
2585
2923
  return properties;
2586
2924
  }
2587
2925
  function materializeDraftSystemInfo(system) {
2588
- const info = {
2926
+ const info2 = {
2589
2927
  ...system.atlasMetadata
2590
2928
  };
2591
2929
  if (system.defaults.theme) {
2592
- info["atlas.theme"] = system.defaults.theme;
2930
+ info2["atlas.theme"] = system.defaults.theme;
2593
2931
  }
2594
2932
  for (const viewpoint of system.viewpoints) {
2595
2933
  const prefix = `viewpoint.${viewpoint.id}`;
2596
- info[`${prefix}.label`] = viewpoint.label;
2934
+ info2[`${prefix}.label`] = viewpoint.label;
2597
2935
  if (viewpoint.summary) {
2598
- info[`${prefix}.summary`] = viewpoint.summary;
2936
+ info2[`${prefix}.summary`] = viewpoint.summary;
2599
2937
  }
2600
2938
  if (viewpoint.focusObjectId) {
2601
- info[`${prefix}.focus`] = viewpoint.focusObjectId;
2939
+ info2[`${prefix}.focus`] = viewpoint.focusObjectId;
2602
2940
  }
2603
2941
  if (viewpoint.selectedObjectId) {
2604
- info[`${prefix}.select`] = viewpoint.selectedObjectId;
2942
+ info2[`${prefix}.select`] = viewpoint.selectedObjectId;
2605
2943
  }
2606
2944
  if (viewpoint.projection) {
2607
- info[`${prefix}.projection`] = viewpoint.projection;
2945
+ info2[`${prefix}.projection`] = viewpoint.projection;
2608
2946
  }
2609
2947
  if (viewpoint.preset) {
2610
- info[`${prefix}.preset`] = viewpoint.preset;
2948
+ info2[`${prefix}.preset`] = viewpoint.preset;
2611
2949
  }
2612
2950
  if (viewpoint.zoom !== null) {
2613
- info[`${prefix}.zoom`] = String(viewpoint.zoom);
2951
+ info2[`${prefix}.zoom`] = String(viewpoint.zoom);
2614
2952
  }
2615
2953
  if (viewpoint.rotationDeg !== 0) {
2616
- info[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2954
+ info2[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2617
2955
  }
2618
2956
  const serializedLayers = serializeViewpointLayers(viewpoint.layers);
2619
2957
  if (serializedLayers) {
2620
- info[`${prefix}.layers`] = serializedLayers;
2958
+ info2[`${prefix}.layers`] = serializedLayers;
2621
2959
  }
2622
2960
  if (viewpoint.filter?.query) {
2623
- info[`${prefix}.query`] = viewpoint.filter.query;
2961
+ info2[`${prefix}.query`] = viewpoint.filter.query;
2624
2962
  }
2625
2963
  if ((viewpoint.filter?.objectTypes.length ?? 0) > 0) {
2626
- info[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
2964
+ info2[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
2627
2965
  }
2628
2966
  if ((viewpoint.filter?.tags.length ?? 0) > 0) {
2629
- info[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
2967
+ info2[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
2630
2968
  }
2631
2969
  if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2632
- info[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2970
+ info2[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2971
+ }
2972
+ if (viewpoint.events.length > 0) {
2973
+ info2[`${prefix}.events`] = viewpoint.events.join(" ");
2633
2974
  }
2634
2975
  }
2635
2976
  for (const annotation of system.annotations) {
2636
2977
  const prefix = `annotation.${annotation.id}`;
2637
- info[`${prefix}.label`] = annotation.label;
2978
+ info2[`${prefix}.label`] = annotation.label;
2638
2979
  if (annotation.targetObjectId) {
2639
- info[`${prefix}.target`] = annotation.targetObjectId;
2980
+ info2[`${prefix}.target`] = annotation.targetObjectId;
2640
2981
  }
2641
- info[`${prefix}.body`] = annotation.body;
2982
+ info2[`${prefix}.body`] = annotation.body;
2642
2983
  if (annotation.tags.length > 0) {
2643
- info[`${prefix}.tags`] = annotation.tags.join(" ");
2984
+ info2[`${prefix}.tags`] = annotation.tags.join(" ");
2644
2985
  }
2645
2986
  if (annotation.sourceObjectId) {
2646
- info[`${prefix}.source`] = annotation.sourceObjectId;
2987
+ info2[`${prefix}.source`] = annotation.sourceObjectId;
2647
2988
  }
2648
2989
  }
2649
- return info;
2990
+ return info2;
2650
2991
  }
2651
2992
  function serializeViewpointLayers(layers) {
2652
2993
  const tokens = [];
@@ -2655,7 +2996,7 @@
2655
2996
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2656
2997
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2657
2998
  }
2658
- for (const key of ["background", "guides", "objects", "labels", "metadata"]) {
2999
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
2659
3000
  if (layers[key] !== void 0) {
2660
3001
  tokens.push(layers[key] ? key : `-${key}`);
2661
3002
  }
@@ -2665,7 +3006,8 @@
2665
3006
  function convertAtlasDocumentToLegacyDraft(document) {
2666
3007
  return {
2667
3008
  ...document,
2668
- version: "2.0-draft"
3009
+ version: "2.0-draft",
3010
+ schemaVersion: "2.0-draft"
2669
3011
  };
2670
3012
  }
2671
3013
 
@@ -2707,19 +3049,28 @@
2707
3049
  ];
2708
3050
  function formatDocument(document, options = {}) {
2709
3051
  const schema = options.schema ?? "auto";
2710
- const useDraft = schema === "2.0" || schema === "2.0-draft" || document.version === "2.0" || document.version === "2.0-draft";
3052
+ const useDraft = schema === "2.0" || schema === "2.1" || schema === "2.0-draft" || document.version === "2.0" || document.version === "2.1" || document.version === "2.0-draft";
2711
3053
  if (useDraft) {
2712
3054
  if (schema === "2.0-draft") {
2713
- const legacyDraftDocument = document.version === "2.0-draft" ? document : document.version === "2.0" ? {
3055
+ const legacyDraftDocument = document.version === "2.0-draft" ? document : document.version === "2.0" || document.version === "2.1" ? {
2714
3056
  ...document,
2715
- version: "2.0-draft"
3057
+ version: "2.0-draft",
3058
+ schemaVersion: "2.0-draft"
2716
3059
  } : upgradeDocumentToDraftV2(document);
2717
3060
  return formatDraftDocument(legacyDraftDocument);
2718
3061
  }
2719
- const atlasDocument = document.version === "2.0" ? document : document.version === "2.0-draft" ? {
3062
+ const atlasDocument = document.version === "2.0" || document.version === "2.1" ? document : document.version === "2.0-draft" ? {
2720
3063
  ...document,
2721
- version: "2.0"
3064
+ version: "2.0",
3065
+ schemaVersion: "2.0"
2722
3066
  } : upgradeDocumentToV2(document);
3067
+ if (schema === "2.1" && atlasDocument.version !== "2.1") {
3068
+ return formatAtlasDocument({
3069
+ ...atlasDocument,
3070
+ version: "2.1",
3071
+ schemaVersion: "2.1"
3072
+ });
3073
+ }
2723
3074
  return formatAtlasDocument(atlasDocument);
2724
3075
  }
2725
3076
  const lines = [];
@@ -2737,10 +3088,22 @@
2737
3088
  return lines.join("\n");
2738
3089
  }
2739
3090
  function formatAtlasDocument(document) {
2740
- const lines = ["schema 2.0", ""];
3091
+ const lines = [`schema ${document.version}`, ""];
2741
3092
  if (document.system) {
2742
3093
  lines.push(...formatAtlasSystem(document.system));
2743
3094
  }
3095
+ for (const group of [...document.groups].sort(compareIdLike)) {
3096
+ lines.push("");
3097
+ lines.push(...formatAtlasGroup(group));
3098
+ }
3099
+ for (const relation of [...document.relations].sort(compareIdLike)) {
3100
+ lines.push("");
3101
+ lines.push(...formatAtlasRelation(relation));
3102
+ }
3103
+ for (const event of [...document.events].sort(compareIdLike)) {
3104
+ lines.push("");
3105
+ lines.push(...formatAtlasEvent(event));
3106
+ }
2744
3107
  const sortedObjects = [...document.objects].sort(compareObjects);
2745
3108
  if (sortedObjects.length > 0 && lines.at(-1) !== "") {
2746
3109
  lines.push("");
@@ -2756,12 +3119,25 @@
2756
3119
  function formatDraftDocument(document) {
2757
3120
  const legacy = document.version === "2.0-draft" ? document : {
2758
3121
  ...document,
2759
- version: "2.0-draft"
3122
+ version: "2.0-draft",
3123
+ schemaVersion: "2.0-draft"
2760
3124
  };
2761
3125
  const lines = ["schema 2.0-draft", ""];
2762
3126
  if (legacy.system) {
2763
3127
  lines.push(...formatAtlasSystem(legacy.system));
2764
3128
  }
3129
+ for (const group of [...legacy.groups].sort(compareIdLike)) {
3130
+ lines.push("");
3131
+ lines.push(...formatAtlasGroup(group));
3132
+ }
3133
+ for (const relation of [...legacy.relations].sort(compareIdLike)) {
3134
+ lines.push("");
3135
+ lines.push(...formatAtlasRelation(relation));
3136
+ }
3137
+ for (const event of [...legacy.events].sort(compareIdLike)) {
3138
+ lines.push("");
3139
+ lines.push(...formatAtlasEvent(event));
3140
+ }
2765
3141
  const sortedObjects = [...legacy.objects].sort(compareObjects);
2766
3142
  if (sortedObjects.length > 0 && lines.at(-1) !== "") {
2767
3143
  lines.push("");
@@ -2777,11 +3153,38 @@
2777
3153
  function formatSystem(system) {
2778
3154
  return formatLines("system", system.id, system.properties, null, system.info);
2779
3155
  }
3156
+ function formatLines(objectType, id, properties, placement, info2) {
3157
+ const lines = [`${objectType} ${id}`];
3158
+ const fieldLines = [...formatPlacement(placement), ...formatProperties(properties)];
3159
+ for (const fieldLine of fieldLines) {
3160
+ lines.push(` ${fieldLine}`);
3161
+ }
3162
+ const infoEntries = Object.entries(info2).sort(([left], [right]) => left.localeCompare(right));
3163
+ if (infoEntries.length > 0) {
3164
+ if (fieldLines.length > 0) {
3165
+ lines.push("");
3166
+ }
3167
+ lines.push(" info");
3168
+ for (const [key, value] of infoEntries) {
3169
+ lines.push(` ${key} ${quoteIfNeeded(value)}`);
3170
+ }
3171
+ }
3172
+ return lines;
3173
+ }
2780
3174
  function formatAtlasSystem(system) {
2781
3175
  const lines = [`system ${system.id}`];
2782
3176
  if (system.title) {
2783
3177
  lines.push(` title ${quoteIfNeeded(system.title)}`);
2784
3178
  }
3179
+ if (system.description) {
3180
+ lines.push(` description ${quoteIfNeeded(system.description)}`);
3181
+ }
3182
+ if (system.epoch) {
3183
+ lines.push(` epoch ${quoteIfNeeded(system.epoch)}`);
3184
+ }
3185
+ if (system.referencePlane) {
3186
+ lines.push(` referencePlane ${quoteIfNeeded(system.referencePlane)}`);
3187
+ }
2785
3188
  lines.push("");
2786
3189
  lines.push("defaults");
2787
3190
  lines.push(` view ${system.defaults.view}`);
@@ -2816,18 +3219,22 @@
2816
3219
  return lines;
2817
3220
  }
2818
3221
  function formatObject(object) {
2819
- return formatLines(object.type, object.id, object.properties, object.placement, object.info);
3222
+ return formatWorldOrbitObject(object.type, object.id, object);
2820
3223
  }
2821
3224
  function formatAtlasObject(object) {
2822
- return formatLines(`object ${object.type}`, object.id, object.properties, object.placement, object.info);
3225
+ return formatWorldOrbitObject(`object ${object.type}`, object.id, object);
2823
3226
  }
2824
- function formatLines(objectType, id, properties, placement, info) {
3227
+ function formatWorldOrbitObject(objectType, id, object) {
2825
3228
  const lines = [`${objectType} ${id}`];
2826
- const fieldLines = [...formatPlacement(placement), ...formatProperties(properties)];
3229
+ const fieldLines = [
3230
+ ...formatPlacement(object.placement),
3231
+ ...formatProperties(object.properties),
3232
+ ...formatObjectMetadata(object)
3233
+ ];
2827
3234
  for (const fieldLine of fieldLines) {
2828
3235
  lines.push(` ${fieldLine}`);
2829
3236
  }
2830
- const infoEntries = Object.entries(info).sort(([left], [right]) => left.localeCompare(right));
3237
+ const infoEntries = Object.entries(object.info).sort(([left], [right]) => left.localeCompare(right));
2831
3238
  if (infoEntries.length > 0) {
2832
3239
  if (fieldLines.length > 0) {
2833
3240
  lines.push("");
@@ -2837,6 +3244,16 @@
2837
3244
  lines.push(` ${key} ${quoteIfNeeded(value)}`);
2838
3245
  }
2839
3246
  }
3247
+ for (const blockName of ["climate", "habitability", "settlement"]) {
3248
+ const blockEntries = Object.entries(object.typedBlocks?.[blockName] ?? {}).sort(([left], [right]) => left.localeCompare(right));
3249
+ if (blockEntries.length > 0) {
3250
+ lines.push("");
3251
+ lines.push(` ${blockName}`);
3252
+ for (const [key, value] of blockEntries) {
3253
+ lines.push(` ${key} ${quoteIfNeeded(value)}`);
3254
+ }
3255
+ }
3256
+ }
2840
3257
  return lines;
2841
3258
  }
2842
3259
  function formatPlacement(placement) {
@@ -2865,6 +3282,46 @@
2865
3282
  function formatProperties(properties) {
2866
3283
  return Object.keys(properties).sort(compareFieldKeys).map((key) => `${key} ${formatValue(properties[key])}`);
2867
3284
  }
3285
+ function formatObjectMetadata(object) {
3286
+ const lines = [];
3287
+ if (object.groups?.length) {
3288
+ lines.push(`groups ${object.groups.join(" ")}`);
3289
+ }
3290
+ if (object.epoch) {
3291
+ lines.push(`epoch ${quoteIfNeeded(object.epoch)}`);
3292
+ }
3293
+ if (object.referencePlane) {
3294
+ lines.push(`referencePlane ${quoteIfNeeded(object.referencePlane)}`);
3295
+ }
3296
+ if (object.tidalLock !== void 0) {
3297
+ lines.push(`tidalLock ${object.tidalLock ? "true" : "false"}`);
3298
+ }
3299
+ if (object.renderHints?.renderLabel !== void 0) {
3300
+ lines.push(`renderLabel ${object.renderHints.renderLabel ? "true" : "false"}`);
3301
+ }
3302
+ if (object.renderHints?.renderOrbit !== void 0) {
3303
+ lines.push(`renderOrbit ${object.renderHints.renderOrbit ? "true" : "false"}`);
3304
+ }
3305
+ if (object.renderHints?.renderPriority !== void 0) {
3306
+ lines.push(`renderPriority ${object.renderHints.renderPriority}`);
3307
+ }
3308
+ if (object.resonance) {
3309
+ lines.push(`resonance ${object.resonance.targetObjectId} ${object.resonance.ratio}`);
3310
+ }
3311
+ for (const rule of object.deriveRules ?? []) {
3312
+ lines.push(`derive ${rule.field} ${rule.strategy}`);
3313
+ }
3314
+ for (const rule of object.validationRules ?? []) {
3315
+ lines.push(`validate ${rule.rule}`);
3316
+ }
3317
+ if (object.lockedFields?.length) {
3318
+ lines.push(`locked ${object.lockedFields.join(" ")}`);
3319
+ }
3320
+ for (const tolerance of object.tolerances ?? []) {
3321
+ lines.push(`tolerance ${tolerance.field} ${formatValue(tolerance.value)}`);
3322
+ }
3323
+ return lines;
3324
+ }
2868
3325
  function formatAtlasViewpoint(viewpoint) {
2869
3326
  const lines = [`viewpoint ${viewpoint.id}`, ` label ${quoteIfNeeded(viewpoint.label)}`];
2870
3327
  if (viewpoint.focusObjectId) {
@@ -2892,6 +3349,9 @@
2892
3349
  if (layerTokens.length > 0) {
2893
3350
  lines.push(` layers ${layerTokens.join(" ")}`);
2894
3351
  }
3352
+ if (viewpoint.events.length > 0) {
3353
+ lines.push(` events ${viewpoint.events.join(" ")}`);
3354
+ }
2895
3355
  if (viewpoint.filter) {
2896
3356
  lines.push(" filter");
2897
3357
  if (viewpoint.filter.query) {
@@ -2920,6 +3380,98 @@
2920
3380
  }
2921
3381
  return lines;
2922
3382
  }
3383
+ function formatAtlasGroup(group) {
3384
+ const lines = [`group ${group.id}`, ` label ${quoteIfNeeded(group.label)}`];
3385
+ if (group.summary) {
3386
+ lines.push(` summary ${quoteIfNeeded(group.summary)}`);
3387
+ }
3388
+ if (group.color) {
3389
+ lines.push(` color ${quoteIfNeeded(group.color)}`);
3390
+ }
3391
+ if (group.tags.length > 0) {
3392
+ lines.push(` tags ${group.tags.map(quoteIfNeeded).join(" ")}`);
3393
+ }
3394
+ if (group.hidden) {
3395
+ lines.push(" hidden true");
3396
+ }
3397
+ return lines;
3398
+ }
3399
+ function formatAtlasRelation(relation) {
3400
+ const lines = [`relation ${relation.id}`];
3401
+ if (relation.from) {
3402
+ lines.push(` from ${quoteIfNeeded(relation.from)}`);
3403
+ }
3404
+ if (relation.to) {
3405
+ lines.push(` to ${quoteIfNeeded(relation.to)}`);
3406
+ }
3407
+ if (relation.kind) {
3408
+ lines.push(` kind ${quoteIfNeeded(relation.kind)}`);
3409
+ }
3410
+ if (relation.label) {
3411
+ lines.push(` label ${quoteIfNeeded(relation.label)}`);
3412
+ }
3413
+ if (relation.summary) {
3414
+ lines.push(` summary ${quoteIfNeeded(relation.summary)}`);
3415
+ }
3416
+ if (relation.tags.length > 0) {
3417
+ lines.push(` tags ${relation.tags.map(quoteIfNeeded).join(" ")}`);
3418
+ }
3419
+ if (relation.color) {
3420
+ lines.push(` color ${quoteIfNeeded(relation.color)}`);
3421
+ }
3422
+ if (relation.hidden) {
3423
+ lines.push(" hidden true");
3424
+ }
3425
+ return lines;
3426
+ }
3427
+ function formatAtlasEvent(event) {
3428
+ const lines = [`event ${event.id}`, ` kind ${quoteIfNeeded(event.kind)}`];
3429
+ if (event.label) {
3430
+ lines.push(` label ${quoteIfNeeded(event.label)}`);
3431
+ }
3432
+ if (event.summary) {
3433
+ lines.push(` summary ${quoteIfNeeded(event.summary)}`);
3434
+ }
3435
+ if (event.targetObjectId) {
3436
+ lines.push(` target ${event.targetObjectId}`);
3437
+ }
3438
+ if (event.participantObjectIds.length > 0) {
3439
+ lines.push(` participants ${event.participantObjectIds.join(" ")}`);
3440
+ }
3441
+ if (event.timing) {
3442
+ lines.push(` timing ${quoteIfNeeded(event.timing)}`);
3443
+ }
3444
+ if (event.visibility) {
3445
+ lines.push(` visibility ${quoteIfNeeded(event.visibility)}`);
3446
+ }
3447
+ if (event.tags.length > 0) {
3448
+ lines.push(` tags ${event.tags.map(quoteIfNeeded).join(" ")}`);
3449
+ }
3450
+ if (event.color) {
3451
+ lines.push(` color ${quoteIfNeeded(event.color)}`);
3452
+ }
3453
+ if (event.hidden) {
3454
+ lines.push(" hidden true");
3455
+ }
3456
+ if (event.positions.length > 0) {
3457
+ lines.push("");
3458
+ lines.push(" positions");
3459
+ for (const pose of [...event.positions].sort(comparePoseObjectId)) {
3460
+ lines.push(` pose ${pose.objectId}`);
3461
+ for (const fieldLine of formatEventPoseFields(pose)) {
3462
+ lines.push(` ${fieldLine}`);
3463
+ }
3464
+ }
3465
+ }
3466
+ return lines;
3467
+ }
3468
+ function formatEventPoseFields(pose) {
3469
+ return [
3470
+ ...formatPlacement(pose.placement),
3471
+ ...formatOptionalUnit("inner", pose.inner),
3472
+ ...formatOptionalUnit("outer", pose.outer)
3473
+ ];
3474
+ }
2923
3475
  function formatValue(value) {
2924
3476
  if (Array.isArray(value)) {
2925
3477
  return value.map((item) => quoteIfNeeded(item)).join(" ");
@@ -2961,7 +3513,7 @@
2961
3513
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2962
3514
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2963
3515
  }
2964
- for (const key of ["background", "guides", "objects", "labels", "metadata"]) {
3516
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
2965
3517
  if (layers[key] !== void 0) {
2966
3518
  tokens.push(layers[key] ? key : `-${key}`);
2967
3519
  }
@@ -2986,6 +3538,12 @@
2986
3538
  return leftIndex - rightIndex;
2987
3539
  return left.id.localeCompare(right.id);
2988
3540
  }
3541
+ function compareIdLike(left, right) {
3542
+ return left.id.localeCompare(right.id);
3543
+ }
3544
+ function comparePoseObjectId(left, right) {
3545
+ return left.objectId.localeCompare(right.objectId);
3546
+ }
2989
3547
  function objectTypeIndex(objectType) {
2990
3548
  switch (objectType) {
2991
3549
  case "star":
@@ -3015,107 +3573,760 @@
3015
3573
  return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
3016
3574
  }
3017
3575
 
3018
- // packages/core/dist/draft-parse.js
3019
- function parseWorldOrbitAtlas(source) {
3020
- return parseAtlasSource(source, "2.0");
3576
+ // packages/core/dist/atlas-utils.js
3577
+ var UNIT_PATTERN2 = /^(-?\d+(?:\.\d+)?)(kpc|min|mj|rj|ky|my|gy|au|km|me|re|pc|ly|deg|sol|K|m|s|h|d|y)?$/;
3578
+ var BOOLEAN_VALUES2 = /* @__PURE__ */ new Map([
3579
+ ["true", true],
3580
+ ["false", false],
3581
+ ["yes", true],
3582
+ ["no", false]
3583
+ ]);
3584
+ var URL_SCHEME_PATTERN2 = /^[A-Za-z][A-Za-z0-9+.-]*:/;
3585
+ function normalizeIdentifier2(value) {
3586
+ return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
3021
3587
  }
3022
- function parseWorldOrbitDraft(source) {
3023
- return parseAtlasSource(source, "2.0-draft");
3588
+ function humanizeIdentifier3(value) {
3589
+ return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
3024
3590
  }
3025
- function parseAtlasSource(source, outputVersion) {
3026
- const lines = source.split(/\r?\n/);
3027
- let sawSchemaHeader = false;
3028
- let schemaVersion = "2.0";
3029
- let system = null;
3030
- let section = null;
3031
- const objectNodes = [];
3032
- let sawDefaults = false;
3033
- let sawAtlas = false;
3034
- const viewpointIds = /* @__PURE__ */ new Set();
3035
- const annotationIds = /* @__PURE__ */ new Set();
3036
- for (let index = 0; index < lines.length; index++) {
3037
- const rawLine = lines[index];
3038
- const lineNumber = index + 1;
3039
- if (!rawLine.trim()) {
3040
- continue;
3041
- }
3042
- const indent = getIndent(rawLine);
3043
- const tokens = tokenizeLineDetailed(rawLine.slice(indent), {
3044
- line: lineNumber,
3045
- columnOffset: indent
3046
- });
3047
- if (tokens.length === 0) {
3048
- continue;
3049
- }
3050
- if (!sawSchemaHeader) {
3051
- schemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
3052
- sawSchemaHeader = true;
3053
- continue;
3054
- }
3055
- if (indent === 0) {
3056
- section = startTopLevelSection(tokens, lineNumber, system, objectNodes, viewpointIds, annotationIds, {
3057
- sawDefaults,
3058
- sawAtlas
3059
- });
3060
- if (section.kind === "system") {
3061
- system = section.system;
3062
- } else if (section.kind === "defaults") {
3063
- sawDefaults = true;
3064
- } else if (section.kind === "atlas") {
3065
- sawAtlas = true;
3066
- }
3067
- continue;
3068
- }
3069
- if (!section) {
3070
- throw new WorldOrbitError("Indented line without parent atlas section", lineNumber, indent + 1);
3071
- }
3072
- handleSectionLine(section, indent, tokens, lineNumber);
3073
- }
3074
- if (!sawSchemaHeader) {
3075
- throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
3591
+ function parseAtlasUnitValue(input, location, fieldKey) {
3592
+ const match = input.match(UNIT_PATTERN2);
3593
+ if (!match) {
3594
+ throw WorldOrbitError.fromLocation(`Invalid unit value "${input}"`, location);
3076
3595
  }
3077
- const ast = {
3078
- type: "document",
3079
- objects: objectNodes
3596
+ const unitValue = {
3597
+ value: Number(match[1]),
3598
+ unit: match[2] ?? null
3080
3599
  };
3081
- const normalizedObjects = normalizeDocument(ast).objects;
3082
- validateDocument({
3083
- format: "worldorbit",
3084
- version: "1.0",
3085
- system: null,
3086
- objects: normalizedObjects
3087
- });
3088
- const diagnostics = schemaVersion === "2.0-draft" && outputVersion === "2.0" ? [
3089
- {
3090
- code: "load.schema.deprecatedDraft",
3091
- severity: "warning",
3092
- source: "upgrade",
3093
- message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".'
3600
+ if (fieldKey) {
3601
+ const schema = getFieldSchema(fieldKey);
3602
+ if (schema?.unitFamily && !unitFamilyAllowsUnit(schema.unitFamily, unitValue.unit)) {
3603
+ throw WorldOrbitError.fromLocation(`Unit "${unitValue.unit ?? "none"}" is not valid for "${fieldKey}"`, location);
3094
3604
  }
3095
- ] : [];
3605
+ }
3606
+ return unitValue;
3607
+ }
3608
+ function tryParseAtlasUnitValue(input) {
3609
+ const match = input.match(UNIT_PATTERN2);
3610
+ if (!match) {
3611
+ return null;
3612
+ }
3096
3613
  return {
3614
+ value: Number(match[1]),
3615
+ unit: match[2] ?? null
3616
+ };
3617
+ }
3618
+ function parseAtlasNumber(input, key, location) {
3619
+ const value = Number(input);
3620
+ if (!Number.isFinite(value)) {
3621
+ throw WorldOrbitError.fromLocation(`Invalid numeric value "${input}" for "${key}"`, location);
3622
+ }
3623
+ return value;
3624
+ }
3625
+ function parseAtlasBoolean(input, key, location) {
3626
+ const parsed = BOOLEAN_VALUES2.get(input.toLowerCase());
3627
+ if (parsed === void 0) {
3628
+ throw WorldOrbitError.fromLocation(`Invalid boolean value "${input}" for "${key}"`, location);
3629
+ }
3630
+ return parsed;
3631
+ }
3632
+ function parseAtlasAtReference(target, location) {
3633
+ if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
3634
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
3635
+ }
3636
+ const pairedMatch = target.match(/^([A-Za-z0-9._-]+)-([A-Za-z0-9._-]+):(L[1-5])$/);
3637
+ if (pairedMatch) {
3638
+ return {
3639
+ kind: "lagrange",
3640
+ primary: pairedMatch[1],
3641
+ secondary: pairedMatch[2],
3642
+ point: pairedMatch[3]
3643
+ };
3644
+ }
3645
+ const simpleMatch = target.match(/^([A-Za-z0-9._-]+):(L[1-5])$/);
3646
+ if (simpleMatch) {
3647
+ return {
3648
+ kind: "lagrange",
3649
+ primary: simpleMatch[1],
3650
+ secondary: null,
3651
+ point: simpleMatch[2]
3652
+ };
3653
+ }
3654
+ if (/^[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
3655
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
3656
+ }
3657
+ const anchorMatch = target.match(/^([A-Za-z0-9._-]+):([A-Za-z0-9._-]+)$/);
3658
+ if (anchorMatch) {
3659
+ return {
3660
+ kind: "anchor",
3661
+ objectId: anchorMatch[1],
3662
+ anchor: anchorMatch[2]
3663
+ };
3664
+ }
3665
+ return {
3666
+ kind: "named",
3667
+ name: target
3668
+ };
3669
+ }
3670
+ function validateAtlasImageSource(value, location) {
3671
+ if (!value) {
3672
+ throw WorldOrbitError.fromLocation('Field "image" must not be empty', location);
3673
+ }
3674
+ if (value.startsWith("//")) {
3675
+ throw WorldOrbitError.fromLocation('Field "image" must use a relative path, root-relative path, or an http/https URL', location);
3676
+ }
3677
+ const schemeMatch = value.match(URL_SCHEME_PATTERN2);
3678
+ if (!schemeMatch) {
3679
+ return;
3680
+ }
3681
+ const scheme = schemeMatch[0].slice(0, -1).toLowerCase();
3682
+ if (scheme !== "http" && scheme !== "https") {
3683
+ throw WorldOrbitError.fromLocation(`Field "image" does not support the "${scheme}" scheme`, location);
3684
+ }
3685
+ }
3686
+ function normalizeLegacyScalarValue(key, values, location) {
3687
+ const schema = getFieldSchema(key);
3688
+ if (!schema) {
3689
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
3690
+ }
3691
+ if (schema.arity === "single" && values.length !== 1) {
3692
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
3693
+ }
3694
+ switch (schema.kind) {
3695
+ case "list":
3696
+ return values;
3697
+ case "boolean":
3698
+ return parseAtlasBoolean(singleAtlasValue(values, key, location), key, location);
3699
+ case "number":
3700
+ return parseAtlasNumber(singleAtlasValue(values, key, location), key, location);
3701
+ case "unit":
3702
+ return parseAtlasUnitValue(singleAtlasValue(values, key, location), location, key);
3703
+ case "string": {
3704
+ const value = values.join(" ").trim();
3705
+ if (key === "image") {
3706
+ validateAtlasImageSource(value, location);
3707
+ }
3708
+ return value;
3709
+ }
3710
+ }
3711
+ }
3712
+ function ensureAtlasFieldSupported(key, objectType, location) {
3713
+ const schema = getFieldSchema(key);
3714
+ if (!schema) {
3715
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
3716
+ }
3717
+ if (!schema.objectTypes.includes(objectType)) {
3718
+ throw WorldOrbitError.fromLocation(`Field "${key}" is not valid on "${objectType}"`, location);
3719
+ }
3720
+ }
3721
+ function singleAtlasValue(values, key, location) {
3722
+ if (values.length !== 1) {
3723
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
3724
+ }
3725
+ return values[0];
3726
+ }
3727
+
3728
+ // packages/core/dist/atlas-validate.js
3729
+ var SURFACE_TARGET_TYPES2 = /* @__PURE__ */ new Set(["star", "planet", "moon", "asteroid", "comet"]);
3730
+ var EARTH_MASSES_PER_SOLAR = 332946.0487;
3731
+ var JUPITER_MASSES_PER_SOLAR = 1047.3486;
3732
+ var AU_IN_KM2 = 1495978707e-1;
3733
+ var EARTH_RADIUS_IN_KM2 = 6371;
3734
+ var SOLAR_RADIUS_IN_KM2 = 695700;
3735
+ var LY_IN_AU2 = 63241.077;
3736
+ var PC_IN_AU2 = 206264.806;
3737
+ var KPC_IN_AU2 = 206264806;
3738
+ function collectAtlasDiagnostics(document, sourceSchemaVersion) {
3739
+ const diagnostics = [];
3740
+ const objectMap = new Map(document.objects.map((object) => [object.id, object]));
3741
+ const groupIds = new Set(document.groups.map((group) => group.id));
3742
+ const eventIds = new Set(document.events.map((event) => event.id));
3743
+ if (!document.system) {
3744
+ diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
3745
+ }
3746
+ const knownIds = /* @__PURE__ */ new Map();
3747
+ for (const [kind, ids] of [
3748
+ ["group", document.groups.map((group) => group.id)],
3749
+ ["viewpoint", document.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
3750
+ ["annotation", document.system?.annotations.map((annotation) => annotation.id) ?? []],
3751
+ ["relation", document.relations.map((relation) => relation.id)],
3752
+ ["event", document.events.map((event) => event.id)],
3753
+ ["object", document.objects.map((object) => object.id)]
3754
+ ]) {
3755
+ for (const id of ids) {
3756
+ const previous = knownIds.get(id);
3757
+ if (previous) {
3758
+ diagnostics.push(error("validate.id.duplicate", `Duplicate ${kind} id "${id}" already used by ${previous}.`));
3759
+ } else {
3760
+ knownIds.set(id, kind);
3761
+ }
3762
+ }
3763
+ }
3764
+ for (const relation of document.relations) {
3765
+ validateRelation(relation, objectMap, diagnostics);
3766
+ }
3767
+ for (const viewpoint of document.system?.viewpoints ?? []) {
3768
+ validateViewpoint(viewpoint.filter, viewpoint.events ?? [], groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpoint.id);
3769
+ }
3770
+ for (const object of document.objects) {
3771
+ validateObject(object, document.system, objectMap, groupIds, diagnostics);
3772
+ }
3773
+ for (const event of document.events) {
3774
+ validateEvent(event, objectMap, diagnostics);
3775
+ }
3776
+ return diagnostics;
3777
+ }
3778
+ function validateRelation(relation, objectMap, diagnostics) {
3779
+ if (!relation.from) {
3780
+ diagnostics.push(error("validate.relation.from.required", `Relation "${relation.id}" is missing a "from" target.`));
3781
+ } else if (!objectMap.has(relation.from)) {
3782
+ diagnostics.push(error("validate.relation.from.unknown", `Unknown relation source "${relation.from}" on "${relation.id}".`));
3783
+ }
3784
+ if (!relation.to) {
3785
+ diagnostics.push(error("validate.relation.to.required", `Relation "${relation.id}" is missing a "to" target.`));
3786
+ } else if (!objectMap.has(relation.to)) {
3787
+ diagnostics.push(error("validate.relation.to.unknown", `Unknown relation target "${relation.to}" on "${relation.id}".`));
3788
+ }
3789
+ if (!relation.kind) {
3790
+ diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
3791
+ }
3792
+ }
3793
+ function validateViewpoint(filter, eventRefs, groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpointId) {
3794
+ if (sourceSchemaVersion === "2.1") {
3795
+ if (filter) {
3796
+ for (const groupId of filter.groupIds) {
3797
+ if (!groupIds.has(groupId)) {
3798
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.groups`));
3799
+ }
3800
+ }
3801
+ }
3802
+ for (const eventId of eventRefs) {
3803
+ if (!eventIds.has(eventId)) {
3804
+ diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.events`));
3805
+ }
3806
+ }
3807
+ }
3808
+ }
3809
+ function validateObject(object, system, objectMap, groupIds, diagnostics) {
3810
+ const placement = object.placement;
3811
+ const orbitPlacement = placement?.mode === "orbit" ? placement : null;
3812
+ const parentObject = placement?.mode === "orbit" ? objectMap.get(placement.target) ?? null : null;
3813
+ if (object.groups) {
3814
+ for (const groupId of object.groups) {
3815
+ if (!groupIds.has(groupId)) {
3816
+ diagnostics.push(warn("validate.group.unknown", `Unknown group "${groupId}" on "${object.id}".`, object.id, "groups"));
3817
+ }
3818
+ }
3819
+ }
3820
+ if (orbitPlacement) {
3821
+ if (!objectMap.has(orbitPlacement.target)) {
3822
+ diagnostics.push(error("validate.orbit.target.unknown", `Unknown placement target "${orbitPlacement.target}" on "${object.id}".`, object.id, "orbit"));
3823
+ }
3824
+ if (orbitPlacement.distance && orbitPlacement.semiMajor) {
3825
+ diagnostics.push(error("validate.orbit.distanceConflict", `Object "${object.id}" cannot declare both "distance" and "semiMajor".`, object.id, "distance"));
3826
+ }
3827
+ if (orbitPlacement.phase && !object.epoch && !system?.epoch) {
3828
+ diagnostics.push(warn("validate.phase.epochMissing", `Object "${object.id}" sets "phase" without an object or system epoch.`, object.id, "phase"));
3829
+ }
3830
+ if (orbitPlacement.inclination && !object.referencePlane && !system?.referencePlane) {
3831
+ diagnostics.push(warn("validate.inclination.referencePlaneMissing", `Object "${object.id}" sets "inclination" without an object or system reference plane.`, object.id, "inclination"));
3832
+ }
3833
+ if (orbitPlacement.period && !massInSolar(parentObject?.properties.mass)) {
3834
+ diagnostics.push(warn("validate.period.massMissing", `Object "${object.id}" sets "period" but its central mass cannot be derived.`, object.id, "period"));
3835
+ }
3836
+ }
3837
+ if (placement?.mode === "surface") {
3838
+ const target = objectMap.get(placement.target);
3839
+ if (!target) {
3840
+ diagnostics.push(error("validate.surface.target.unknown", `Unknown placement target "${placement.target}" on "${object.id}".`, object.id, "surface"));
3841
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
3842
+ diagnostics.push(error("validate.surface.target.invalid", `Surface target "${placement.target}" on "${object.id}" is not surface-capable.`, object.id, "surface"));
3843
+ }
3844
+ }
3845
+ if (placement?.mode === "at") {
3846
+ if (object.type !== "structure" && object.type !== "phenomenon") {
3847
+ diagnostics.push(error("validate.at.objectType", `Only structures and phenomena may use "at" placement; found "${object.type}" on "${object.id}".`, object.id, "at"));
3848
+ }
3849
+ if (!validateAtTarget(object, objectMap, diagnostics)) {
3850
+ diagnostics.push(error("validate.at.target.unknown", `Unknown at-reference target "${placement.target}" on "${object.id}".`, object.id, "at"));
3851
+ }
3852
+ }
3853
+ if (object.resonance) {
3854
+ const target = objectMap.get(object.resonance.targetObjectId);
3855
+ if (!target) {
3856
+ diagnostics.push(error("validate.resonance.target.unknown", `Unknown resonance target "${object.resonance.targetObjectId}" on "${object.id}".`, object.id, "resonance"));
3857
+ } else if (object.placement?.mode !== "orbit" || target.placement?.mode !== "orbit" || object.placement.target !== target.placement.target) {
3858
+ diagnostics.push(warn("validate.resonance.orbitMismatch", `Resonance target "${object.resonance.targetObjectId}" on "${object.id}" does not share a compatible orbital parent.`, object.id, "resonance"));
3859
+ }
3860
+ }
3861
+ for (const rule of object.deriveRules ?? []) {
3862
+ if (rule.field !== "period" || rule.strategy !== "kepler") {
3863
+ diagnostics.push(warn("validate.derive.unsupported", `Unsupported derive rule "${rule.field} ${rule.strategy}" on "${object.id}".`, object.id, "derive"));
3864
+ continue;
3865
+ }
3866
+ const derivedPeriodDays = keplerPeriodDays(object, parentObject);
3867
+ if (derivedPeriodDays === null) {
3868
+ diagnostics.push(warn("validate.derive.inputsMissing", `Object "${object.id}" requests "derive period kepler" but lacks enough input data.`, object.id, "derive"));
3869
+ continue;
3870
+ }
3871
+ if (!orbitPlacement?.period) {
3872
+ diagnostics.push(info("validate.derive.period.available", `Object "${object.id}" can derive a Kepler period of ${formatDays(derivedPeriodDays)}.`, object.id, "derive"));
3873
+ }
3874
+ }
3875
+ for (const rule of object.validationRules ?? []) {
3876
+ if (rule.rule !== "kepler") {
3877
+ diagnostics.push(warn("validate.rule.unsupported", `Unsupported validation rule "${rule.rule}" on "${object.id}".`, object.id, "validate"));
3878
+ continue;
3879
+ }
3880
+ const actualPeriodDays = durationInDays(orbitPlacement?.period);
3881
+ const derivedPeriodDays = keplerPeriodDays(object, parentObject);
3882
+ if (actualPeriodDays === null || derivedPeriodDays === null) {
3883
+ continue;
3884
+ }
3885
+ const toleranceDays = toleranceForField(object, "period");
3886
+ if (Math.abs(actualPeriodDays - derivedPeriodDays) > toleranceDays) {
3887
+ diagnostics.push(error("validate.kepler.mismatch", `Object "${object.id}" fails Kepler validation for "period".`, object.id, "validate"));
3888
+ }
3889
+ }
3890
+ }
3891
+ function validateEvent(event, objectMap, diagnostics) {
3892
+ const fieldPrefix = `event.${event.id}`;
3893
+ const referencedIds = /* @__PURE__ */ new Set();
3894
+ if (!event.kind.trim()) {
3895
+ diagnostics.push(error("validate.event.kind.required", `Event "${event.id}" is missing a "kind" value.`, void 0, `${fieldPrefix}.kind`));
3896
+ }
3897
+ if (!event.targetObjectId && event.participantObjectIds.length === 0) {
3898
+ diagnostics.push(error("validate.event.references.required", `Event "${event.id}" must define a "target" or at least one participant.`, void 0, `${fieldPrefix}.participants`));
3899
+ }
3900
+ if (event.targetObjectId) {
3901
+ referencedIds.add(event.targetObjectId);
3902
+ if (!objectMap.has(event.targetObjectId)) {
3903
+ diagnostics.push(error("validate.event.target.unknown", `Unknown event target "${event.targetObjectId}" on "${event.id}".`, void 0, `${fieldPrefix}.target`));
3904
+ }
3905
+ }
3906
+ const seenParticipants = /* @__PURE__ */ new Set();
3907
+ for (const participantId of event.participantObjectIds) {
3908
+ referencedIds.add(participantId);
3909
+ if (seenParticipants.has(participantId)) {
3910
+ diagnostics.push(warn("validate.event.participants.duplicate", `Event "${event.id}" repeats participant "${participantId}".`, void 0, `${fieldPrefix}.participants`));
3911
+ continue;
3912
+ }
3913
+ seenParticipants.add(participantId);
3914
+ if (!objectMap.has(participantId)) {
3915
+ diagnostics.push(error("validate.event.participants.unknown", `Unknown event participant "${participantId}" on "${event.id}".`, void 0, `${fieldPrefix}.participants`));
3916
+ }
3917
+ }
3918
+ if (event.targetObjectId && event.participantObjectIds.length > 0 && !event.participantObjectIds.includes(event.targetObjectId)) {
3919
+ diagnostics.push(warn("validate.event.target.notParticipant", `Event "${event.id}" defines a target outside its participants list.`, void 0, `${fieldPrefix}.target`));
3920
+ }
3921
+ if (event.positions.length === 0) {
3922
+ diagnostics.push(warn("validate.event.positions.missing", `Event "${event.id}" has no positions block and cannot drive a scene snapshot.`, void 0, `${fieldPrefix}.positions`));
3923
+ }
3924
+ if (/(?:^|[-_])(solar-eclipse|lunar-eclipse|transit|occultation)(?:$|[-_])/.test(event.kind) && referencedIds.size < 3) {
3925
+ 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`));
3926
+ }
3927
+ const poseIds = /* @__PURE__ */ new Set();
3928
+ for (const pose of event.positions) {
3929
+ const poseFieldPrefix = `${fieldPrefix}.pose.${pose.objectId}`;
3930
+ if (poseIds.has(pose.objectId)) {
3931
+ diagnostics.push(error("validate.event.pose.duplicate", `Event "${event.id}" defines "${pose.objectId}" more than once in positions.`, void 0, poseFieldPrefix));
3932
+ continue;
3933
+ }
3934
+ poseIds.add(pose.objectId);
3935
+ const object = objectMap.get(pose.objectId);
3936
+ if (!object) {
3937
+ diagnostics.push(error("validate.event.pose.object.unknown", `Unknown event pose object "${pose.objectId}" on "${event.id}".`, void 0, poseFieldPrefix));
3938
+ continue;
3939
+ }
3940
+ if (!referencedIds.has(pose.objectId)) {
3941
+ diagnostics.push(warn("validate.event.pose.unreferenced", `Event pose "${pose.objectId}" on "${event.id}" is not listed in target/participants.`, void 0, poseFieldPrefix));
3942
+ }
3943
+ validateEventPose(pose, object, objectMap, diagnostics, poseFieldPrefix, event.id);
3944
+ }
3945
+ }
3946
+ function validateEventPose(pose, object, objectMap, diagnostics, fieldPrefix, eventId) {
3947
+ const placement = pose.placement;
3948
+ if (!placement) {
3949
+ diagnostics.push(error("validate.event.pose.placement.required", `Event "${eventId}" pose "${pose.objectId}" is missing a placement mode.`, void 0, fieldPrefix));
3950
+ return;
3951
+ }
3952
+ if (placement.mode === "orbit") {
3953
+ if (!objectMap.has(placement.target)) {
3954
+ diagnostics.push(error("validate.event.pose.orbit.target.unknown", `Unknown event orbit target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.orbit`));
3955
+ }
3956
+ if (placement.distance && placement.semiMajor) {
3957
+ diagnostics.push(error("validate.event.pose.orbit.distanceConflict", `Event "${eventId}" pose "${pose.objectId}" cannot declare both "distance" and "semiMajor".`, void 0, `${fieldPrefix}.distance`));
3958
+ }
3959
+ return;
3960
+ }
3961
+ if (placement.mode === "surface") {
3962
+ const target = objectMap.get(placement.target);
3963
+ if (!target) {
3964
+ diagnostics.push(error("validate.event.pose.surface.target.unknown", `Unknown event surface target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.surface`));
3965
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
3966
+ 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`));
3967
+ }
3968
+ return;
3969
+ }
3970
+ if (placement.mode === "at") {
3971
+ if (object.type !== "structure" && object.type !== "phenomenon") {
3972
+ 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`));
3973
+ }
3974
+ const reference = placement.reference;
3975
+ if (reference.kind === "named" && !objectMap.has(reference.name)) {
3976
+ diagnostics.push(error("validate.event.pose.at.target.unknown", `Unknown event at-reference target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3977
+ } else if (reference.kind === "anchor" && !objectMap.has(reference.objectId)) {
3978
+ diagnostics.push(error("validate.event.pose.anchor.target.unknown", `Unknown event anchor target "${reference.objectId}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3979
+ } else if (reference.kind === "lagrange") {
3980
+ if (!objectMap.has(reference.primary)) {
3981
+ diagnostics.push(error("validate.event.pose.lagrange.primary.unknown", `Unknown event Lagrange target "${reference.primary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3982
+ } else if (reference.secondary && !objectMap.has(reference.secondary)) {
3983
+ diagnostics.push(error("validate.event.pose.lagrange.secondary.unknown", `Unknown event Lagrange target "${reference.secondary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3984
+ }
3985
+ }
3986
+ }
3987
+ }
3988
+ function validateAtTarget(object, objectMap, diagnostics) {
3989
+ const reference = object.placement?.mode === "at" ? object.placement.reference : null;
3990
+ if (!reference) {
3991
+ return true;
3992
+ }
3993
+ if (reference.kind === "named") {
3994
+ return objectMap.has(reference.name);
3995
+ }
3996
+ if (reference.kind === "anchor") {
3997
+ if (!objectMap.has(reference.objectId)) {
3998
+ diagnostics.push(error("validate.anchor.target.unknown", `Unknown anchor target "${reference.objectId}" on "${object.id}".`, object.id, "at"));
3999
+ return false;
4000
+ }
4001
+ return true;
4002
+ }
4003
+ if (!objectMap.has(reference.primary)) {
4004
+ diagnostics.push(error("validate.lagrange.primary.unknown", `Unknown Lagrange reference "${reference.primary}" on "${object.id}".`, object.id, "at"));
4005
+ return false;
4006
+ }
4007
+ if (reference.secondary && !objectMap.has(reference.secondary)) {
4008
+ diagnostics.push(error("validate.lagrange.secondary.unknown", `Unknown Lagrange reference "${reference.secondary}" on "${object.id}".`, object.id, "at"));
4009
+ return false;
4010
+ }
4011
+ return true;
4012
+ }
4013
+ function keplerPeriodDays(object, parentObject) {
4014
+ const placement = object.placement;
4015
+ if (!placement || placement.mode !== "orbit") {
4016
+ return null;
4017
+ }
4018
+ const semiMajorAu = distanceInAu(placement.semiMajor ?? placement.distance);
4019
+ const centralMassSolar = massInSolar(parentObject?.properties.mass);
4020
+ if (semiMajorAu === null || centralMassSolar === null || centralMassSolar <= 0) {
4021
+ return null;
4022
+ }
4023
+ const periodYears = Math.sqrt(semiMajorAu ** 3 / centralMassSolar);
4024
+ return periodYears * 365.25;
4025
+ }
4026
+ function distanceInAu(value) {
4027
+ if (!value)
4028
+ return null;
4029
+ switch (value.unit) {
4030
+ case null:
4031
+ case "au":
4032
+ return value.value;
4033
+ case "km":
4034
+ return value.value / AU_IN_KM2;
4035
+ case "m":
4036
+ return value.value / (AU_IN_KM2 * 1e3);
4037
+ case "ly":
4038
+ return value.value * LY_IN_AU2;
4039
+ case "pc":
4040
+ return value.value * PC_IN_AU2;
4041
+ case "kpc":
4042
+ return value.value * KPC_IN_AU2;
4043
+ case "re":
4044
+ return value.value * EARTH_RADIUS_IN_KM2 / AU_IN_KM2;
4045
+ case "sol":
4046
+ return value.value * SOLAR_RADIUS_IN_KM2 / AU_IN_KM2;
4047
+ default:
4048
+ return null;
4049
+ }
4050
+ }
4051
+ function massInSolar(value) {
4052
+ if (!value || typeof value !== "object" || !("value" in value)) {
4053
+ return null;
4054
+ }
4055
+ const unitValue = value;
4056
+ switch (unitValue.unit) {
4057
+ case null:
4058
+ case "sol":
4059
+ return unitValue.value;
4060
+ case "me":
4061
+ return unitValue.value / EARTH_MASSES_PER_SOLAR;
4062
+ case "mj":
4063
+ return unitValue.value / JUPITER_MASSES_PER_SOLAR;
4064
+ default:
4065
+ return null;
4066
+ }
4067
+ }
4068
+ function durationInDays(value) {
4069
+ if (!value)
4070
+ return null;
4071
+ switch (value.unit) {
4072
+ case null:
4073
+ case "d":
4074
+ return value.value;
4075
+ case "s":
4076
+ return value.value / 86400;
4077
+ case "min":
4078
+ return value.value / 1440;
4079
+ case "h":
4080
+ return value.value / 24;
4081
+ case "y":
4082
+ return value.value * 365.25;
4083
+ case "ky":
4084
+ return value.value * 365250;
4085
+ case "my":
4086
+ return value.value * 36525e4;
4087
+ case "gy":
4088
+ return value.value * 36525e7;
4089
+ default:
4090
+ return null;
4091
+ }
4092
+ }
4093
+ function toleranceForField(object, field) {
4094
+ const tolerance = object.tolerances?.find((entry) => entry.field === field)?.value;
4095
+ if (typeof tolerance === "number") {
4096
+ return tolerance;
4097
+ }
4098
+ if (tolerance && typeof tolerance === "object" && "value" in tolerance) {
4099
+ return durationInDays(tolerance) ?? 0;
4100
+ }
4101
+ return 0;
4102
+ }
4103
+ function formatDays(days) {
4104
+ return `${Math.round(days * 100) / 100}d`;
4105
+ }
4106
+ function error(code, message, objectId, field) {
4107
+ return { code, severity: "error", source: "validate", message, objectId, field };
4108
+ }
4109
+ function warn(code, message, objectId, field) {
4110
+ return { code, severity: "warning", source: "validate", message, objectId, field };
4111
+ }
4112
+ function info(code, message, objectId, field) {
4113
+ return { code, severity: "info", source: "validate", message, objectId, field };
4114
+ }
4115
+
4116
+ // packages/core/dist/draft-parse.js
4117
+ var STRUCTURED_TYPED_BLOCKS = /* @__PURE__ */ new Set([
4118
+ "climate",
4119
+ "habitability",
4120
+ "settlement"
4121
+ ]);
4122
+ var DRAFT_OBJECT_FIELD_SPECS = /* @__PURE__ */ new Map();
4123
+ for (const key of [
4124
+ "orbit",
4125
+ "distance",
4126
+ "semiMajor",
4127
+ "eccentricity",
4128
+ "period",
4129
+ "angle",
4130
+ "inclination",
4131
+ "phase",
4132
+ "at",
4133
+ "surface",
4134
+ "free",
4135
+ "kind",
4136
+ "class",
4137
+ "culture",
4138
+ "tags",
4139
+ "color",
4140
+ "image",
4141
+ "hidden",
4142
+ "radius",
4143
+ "mass",
4144
+ "density",
4145
+ "gravity",
4146
+ "temperature",
4147
+ "albedo",
4148
+ "atmosphere",
4149
+ "inner",
4150
+ "outer",
4151
+ "on",
4152
+ "source",
4153
+ "cycle"
4154
+ ]) {
4155
+ const schema = getFieldSchema(key);
4156
+ if (schema) {
4157
+ DRAFT_OBJECT_FIELD_SPECS.set(key, {
4158
+ key,
4159
+ version: "2.0",
4160
+ inlineMode: schema.arity === "multiple" ? "multiple" : "single",
4161
+ allowRepeat: false,
4162
+ legacySchema: schema
4163
+ });
4164
+ }
4165
+ }
4166
+ for (const spec of [
4167
+ { key: "groups", inlineMode: "multiple", allowRepeat: false },
4168
+ { key: "epoch", inlineMode: "single", allowRepeat: false },
4169
+ { key: "referencePlane", inlineMode: "single", allowRepeat: false },
4170
+ { key: "tidalLock", inlineMode: "single", allowRepeat: false },
4171
+ { key: "renderLabel", inlineMode: "single", allowRepeat: false },
4172
+ { key: "renderOrbit", inlineMode: "single", allowRepeat: false },
4173
+ { key: "renderPriority", inlineMode: "single", allowRepeat: false },
4174
+ { key: "resonance", inlineMode: "pair", allowRepeat: false },
4175
+ { key: "derive", inlineMode: "pair", allowRepeat: true },
4176
+ { key: "validate", inlineMode: "single", allowRepeat: true },
4177
+ { key: "locked", inlineMode: "multiple", allowRepeat: false },
4178
+ { key: "tolerance", inlineMode: "pair", allowRepeat: true }
4179
+ ]) {
4180
+ DRAFT_OBJECT_FIELD_SPECS.set(spec.key, {
4181
+ key: spec.key,
4182
+ version: "2.1",
4183
+ inlineMode: spec.inlineMode,
4184
+ allowRepeat: spec.allowRepeat
4185
+ });
4186
+ }
4187
+ var DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
4188
+ var EVENT_POSE_FIELD_KEYS = /* @__PURE__ */ new Set([
4189
+ "orbit",
4190
+ "distance",
4191
+ "semiMajor",
4192
+ "eccentricity",
4193
+ "period",
4194
+ "angle",
4195
+ "inclination",
4196
+ "phase",
4197
+ "at",
4198
+ "surface",
4199
+ "free",
4200
+ "inner",
4201
+ "outer"
4202
+ ]);
4203
+ function parseWorldOrbitAtlas(source) {
4204
+ return parseAtlasSource(source);
4205
+ }
4206
+ function parseWorldOrbitDraft(source) {
4207
+ return parseAtlasSource(source, "2.0-draft");
4208
+ }
4209
+ function parseAtlasSource(source, forcedOutputVersion) {
4210
+ const prepared = preprocessAtlasSource(source);
4211
+ const lines = prepared.source.split(/\r?\n/);
4212
+ const diagnostics = [];
4213
+ let sawSchemaHeader = false;
4214
+ let sourceSchemaVersion = "2.0";
4215
+ let system = null;
4216
+ let section = null;
4217
+ const objectNodes = [];
4218
+ const groups = [];
4219
+ const relations = [];
4220
+ const events = [];
4221
+ const eventPoseNodes = /* @__PURE__ */ new Map();
4222
+ let sawDefaults = false;
4223
+ let sawAtlas = false;
4224
+ const viewpointIds = /* @__PURE__ */ new Set();
4225
+ const annotationIds = /* @__PURE__ */ new Set();
4226
+ const groupIds = /* @__PURE__ */ new Set();
4227
+ const relationIds = /* @__PURE__ */ new Set();
4228
+ const eventIds = /* @__PURE__ */ new Set();
4229
+ for (let index = 0; index < lines.length; index++) {
4230
+ const rawLine = lines[index];
4231
+ const lineNumber = index + 1;
4232
+ if (!rawLine.trim()) {
4233
+ continue;
4234
+ }
4235
+ const indent = getIndent(rawLine);
4236
+ const tokens = tokenizeLineDetailed(rawLine.slice(indent), {
4237
+ line: lineNumber,
4238
+ columnOffset: indent
4239
+ });
4240
+ if (tokens.length === 0) {
4241
+ continue;
4242
+ }
4243
+ if (!sawSchemaHeader) {
4244
+ sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
4245
+ sawSchemaHeader = true;
4246
+ if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
4247
+ diagnostics.push({
4248
+ code: "parse.schema21.commentCompatibility",
4249
+ severity: "warning",
4250
+ source: "parse",
4251
+ message: `Comments require schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
4252
+ line: prepared.comments[0].line,
4253
+ column: prepared.comments[0].column
4254
+ });
4255
+ }
4256
+ continue;
4257
+ }
4258
+ if (indent === 0) {
4259
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, { sawDefaults, sawAtlas });
4260
+ if (section.kind === "system") {
4261
+ system = section.system;
4262
+ } else if (section.kind === "defaults") {
4263
+ sawDefaults = true;
4264
+ } else if (section.kind === "atlas") {
4265
+ sawAtlas = true;
4266
+ }
4267
+ continue;
4268
+ }
4269
+ if (!section) {
4270
+ throw new WorldOrbitError("Indented line without parent atlas section", lineNumber, indent + 1);
4271
+ }
4272
+ handleSectionLine(section, indent, tokens, lineNumber);
4273
+ }
4274
+ if (!sawSchemaHeader) {
4275
+ throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
4276
+ }
4277
+ const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
4278
+ const normalizedEvents = events.map((event) => normalizeDraftEvent(event, eventPoseNodes.get(event.id) ?? []));
4279
+ const outputVersion = forcedOutputVersion ?? (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
4280
+ const baseDocument = {
3097
4281
  format: "worldorbit",
3098
- version: outputVersion,
3099
4282
  sourceVersion: "1.0",
3100
4283
  system,
3101
- objects: normalizedObjects,
4284
+ groups,
4285
+ relations,
4286
+ events: normalizedEvents,
4287
+ objects,
3102
4288
  diagnostics
3103
4289
  };
4290
+ if (outputVersion === "2.0-draft") {
4291
+ const document2 = {
4292
+ ...baseDocument,
4293
+ version: "2.0-draft",
4294
+ schemaVersion: "2.0-draft"
4295
+ };
4296
+ document2.diagnostics.push(...collectAtlasDiagnostics(document2, sourceSchemaVersion));
4297
+ return document2;
4298
+ }
4299
+ const document = {
4300
+ ...baseDocument,
4301
+ version: outputVersion,
4302
+ schemaVersion: outputVersion
4303
+ };
4304
+ if (sourceSchemaVersion === "2.0-draft") {
4305
+ document.diagnostics.push({
4306
+ code: "load.schema.deprecatedDraft",
4307
+ severity: "warning",
4308
+ source: "upgrade",
4309
+ message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".'
4310
+ });
4311
+ }
4312
+ document.diagnostics.push(...collectAtlasDiagnostics(document, sourceSchemaVersion));
4313
+ return document;
3104
4314
  }
3105
4315
  function assertDraftSchemaHeader(tokens, line) {
3106
- if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || tokens[1].value.toLowerCase() !== "2.0-draft" && tokens[1].value.toLowerCase() !== "2.0") {
3107
- throw new WorldOrbitError('Expected atlas header "schema 2.0" or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
4316
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
4317
+ throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
3108
4318
  }
3109
- return tokens[1].value.toLowerCase() === "2.0-draft" ? "2.0-draft" : "2.0";
4319
+ const version = tokens[1].value.toLowerCase();
4320
+ return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
3110
4321
  }
3111
- function startTopLevelSection(tokens, line, system, objectNodes, viewpointIds, annotationIds, flags) {
4322
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, flags) {
3112
4323
  const keyword = tokens[0]?.value.toLowerCase();
3113
4324
  switch (keyword) {
3114
4325
  case "system":
3115
4326
  if (system) {
3116
4327
  throw new WorldOrbitError('Atlas section "system" may only appear once', line, tokens[0].column);
3117
4328
  }
3118
- return startSystemSection(tokens, line);
4329
+ return startSystemSection(tokens, line, sourceSchemaVersion, diagnostics);
3119
4330
  case "defaults":
3120
4331
  if (!system) {
3121
4332
  throw new WorldOrbitError('Atlas section "defaults" requires a preceding system declaration', line, tokens[0].column);
@@ -3145,19 +4356,28 @@
3145
4356
  if (!system) {
3146
4357
  throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
3147
4358
  }
3148
- return startViewpointSection(tokens, line, system, viewpointIds);
4359
+ return startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics);
3149
4360
  case "annotation":
3150
4361
  if (!system) {
3151
4362
  throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
3152
4363
  }
3153
4364
  return startAnnotationSection(tokens, line, system, annotationIds);
4365
+ case "group":
4366
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "group", { line, column: tokens[0].column });
4367
+ return startGroupSection(tokens, line, groups, groupIds);
4368
+ case "relation":
4369
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
4370
+ return startRelationSection(tokens, line, relations, relationIds);
4371
+ case "event":
4372
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "event", { line, column: tokens[0].column });
4373
+ return startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics);
3154
4374
  case "object":
3155
- return startObjectSection(tokens, line, objectNodes);
4375
+ return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
3156
4376
  default:
3157
4377
  throw new WorldOrbitError(`Unknown atlas section "${tokens[0]?.value ?? ""}"`, line, tokens[0]?.column ?? 1);
3158
4378
  }
3159
4379
  }
3160
- function startSystemSection(tokens, line) {
4380
+ function startSystemSection(tokens, line, sourceSchemaVersion, diagnostics) {
3161
4381
  if (tokens.length !== 2) {
3162
4382
  throw new WorldOrbitError("Invalid atlas system declaration", line, tokens[0]?.column ?? 1);
3163
4383
  }
@@ -3165,6 +4385,9 @@
3165
4385
  type: "system",
3166
4386
  id: tokens[1].value,
3167
4387
  title: null,
4388
+ description: null,
4389
+ epoch: null,
4390
+ referencePlane: null,
3168
4391
  defaults: {
3169
4392
  view: "topdown",
3170
4393
  scale: null,
@@ -3179,10 +4402,12 @@
3179
4402
  return {
3180
4403
  kind: "system",
3181
4404
  system,
4405
+ sourceSchemaVersion,
4406
+ diagnostics,
3182
4407
  seenFields: /* @__PURE__ */ new Set()
3183
4408
  };
3184
4409
  }
3185
- function startViewpointSection(tokens, line, system, viewpointIds) {
4410
+ function startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics) {
3186
4411
  if (tokens.length !== 2) {
3187
4412
  throw new WorldOrbitError("Invalid viewpoint declaration", line, tokens[0]?.column ?? 1);
3188
4413
  }
@@ -3199,6 +4424,7 @@
3199
4424
  summary: "",
3200
4425
  focusObjectId: null,
3201
4426
  selectedObjectId: null,
4427
+ events: [],
3202
4428
  projection: system.defaults.view,
3203
4429
  preset: system.defaults.preset,
3204
4430
  zoom: null,
@@ -3211,6 +4437,8 @@
3211
4437
  return {
3212
4438
  kind: "viewpoint",
3213
4439
  viewpoint,
4440
+ sourceSchemaVersion,
4441
+ diagnostics,
3214
4442
  seenFields: /* @__PURE__ */ new Set(),
3215
4443
  inFilter: false,
3216
4444
  filterIndent: null,
@@ -3244,7 +4472,107 @@
3244
4472
  seenFields: /* @__PURE__ */ new Set()
3245
4473
  };
3246
4474
  }
3247
- function startObjectSection(tokens, line, objectNodes) {
4475
+ function startGroupSection(tokens, line, groups, groupIds) {
4476
+ if (tokens.length !== 2) {
4477
+ throw new WorldOrbitError("Invalid group declaration", line, tokens[0]?.column ?? 1);
4478
+ }
4479
+ const id = normalizeIdentifier2(tokens[1].value);
4480
+ if (!id) {
4481
+ throw new WorldOrbitError("Group id must not be empty", line, tokens[1].column);
4482
+ }
4483
+ if (groupIds.has(id)) {
4484
+ throw new WorldOrbitError(`Duplicate group id "${id}"`, line, tokens[1].column);
4485
+ }
4486
+ const group = {
4487
+ id,
4488
+ label: humanizeIdentifier3(id),
4489
+ summary: "",
4490
+ color: null,
4491
+ tags: [],
4492
+ hidden: false
4493
+ };
4494
+ groups.push(group);
4495
+ groupIds.add(id);
4496
+ return {
4497
+ kind: "group",
4498
+ group,
4499
+ seenFields: /* @__PURE__ */ new Set()
4500
+ };
4501
+ }
4502
+ function startRelationSection(tokens, line, relations, relationIds) {
4503
+ if (tokens.length !== 2) {
4504
+ throw new WorldOrbitError("Invalid relation declaration", line, tokens[0]?.column ?? 1);
4505
+ }
4506
+ const id = normalizeIdentifier2(tokens[1].value);
4507
+ if (!id) {
4508
+ throw new WorldOrbitError("Relation id must not be empty", line, tokens[1].column);
4509
+ }
4510
+ if (relationIds.has(id)) {
4511
+ throw new WorldOrbitError(`Duplicate relation id "${id}"`, line, tokens[1].column);
4512
+ }
4513
+ const relation = {
4514
+ id,
4515
+ from: "",
4516
+ to: "",
4517
+ kind: "",
4518
+ label: null,
4519
+ summary: null,
4520
+ tags: [],
4521
+ color: null,
4522
+ hidden: false
4523
+ };
4524
+ relations.push(relation);
4525
+ relationIds.add(id);
4526
+ return {
4527
+ kind: "relation",
4528
+ relation,
4529
+ seenFields: /* @__PURE__ */ new Set()
4530
+ };
4531
+ }
4532
+ function startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics) {
4533
+ if (tokens.length !== 2) {
4534
+ throw new WorldOrbitError("Invalid event declaration", line, tokens[0]?.column ?? 1);
4535
+ }
4536
+ const id = normalizeIdentifier2(tokens[1].value);
4537
+ if (!id) {
4538
+ throw new WorldOrbitError("Event id must not be empty", line, tokens[1].column);
4539
+ }
4540
+ if (eventIds.has(id)) {
4541
+ throw new WorldOrbitError(`Duplicate event id "${id}"`, line, tokens[1].column);
4542
+ }
4543
+ const event = {
4544
+ id,
4545
+ kind: "",
4546
+ label: humanizeIdentifier3(id),
4547
+ summary: null,
4548
+ targetObjectId: null,
4549
+ participantObjectIds: [],
4550
+ timing: null,
4551
+ visibility: null,
4552
+ tags: [],
4553
+ color: null,
4554
+ hidden: false,
4555
+ positions: []
4556
+ };
4557
+ const rawPoses = [];
4558
+ events.push(event);
4559
+ eventPoseNodes.set(id, rawPoses);
4560
+ eventIds.add(id);
4561
+ return {
4562
+ kind: "event",
4563
+ event,
4564
+ sourceSchemaVersion,
4565
+ diagnostics,
4566
+ seenFields: /* @__PURE__ */ new Set(),
4567
+ rawPoses,
4568
+ inPositions: false,
4569
+ positionsIndent: null,
4570
+ activePose: null,
4571
+ poseIndent: null,
4572
+ activePoseSeenFields: /* @__PURE__ */ new Set()
4573
+ };
4574
+ }
4575
+ function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
3248
4576
  if (tokens.length < 3) {
3249
4577
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
3250
4578
  }
@@ -3255,12 +4583,11 @@
3255
4583
  throw new WorldOrbitError(`Unknown object type "${objectTypeToken.value}"`, line, objectTypeToken.column);
3256
4584
  }
3257
4585
  const objectNode = {
3258
- type: "object",
3259
4586
  objectType,
3260
- name: idToken.value,
3261
- inlineFields: parseInlineFields2(tokens.slice(3), line),
3262
- blockFields: [],
4587
+ id: idToken.value,
4588
+ fields: parseInlineObjectFields(tokens.slice(3), line, objectType, sourceSchemaVersion, diagnostics),
3263
4589
  infoEntries: [],
4590
+ typedBlockEntries: {},
3264
4591
  location: {
3265
4592
  line,
3266
4593
  column: objectTypeToken.column
@@ -3270,8 +4597,12 @@
3270
4597
  return {
3271
4598
  kind: "object",
3272
4599
  objectNode,
3273
- inInfoBlock: false,
3274
- infoIndent: null
4600
+ sourceSchemaVersion,
4601
+ diagnostics,
4602
+ activeBlock: null,
4603
+ blockIndent: null,
4604
+ seenInfoKeys: /* @__PURE__ */ new Set(),
4605
+ seenTypedBlockKeys: {}
3275
4606
  };
3276
4607
  }
3277
4608
  function handleSectionLine(section, indent, tokens, line) {
@@ -3291,6 +4622,15 @@
3291
4622
  case "annotation":
3292
4623
  applyAnnotationField(section, tokens, line);
3293
4624
  return;
4625
+ case "group":
4626
+ applyGroupField(section, tokens, line);
4627
+ return;
4628
+ case "relation":
4629
+ applyRelationField(section, tokens, line);
4630
+ return;
4631
+ case "event":
4632
+ applyEventField(section, indent, tokens, line);
4633
+ return;
3294
4634
  case "object":
3295
4635
  applyObjectField(section, indent, tokens, line);
3296
4636
  return;
@@ -3298,10 +4638,35 @@
3298
4638
  }
3299
4639
  function applySystemField(section, tokens, line) {
3300
4640
  const key = requireUniqueField(tokens, section.seenFields, line);
3301
- if (key !== "title") {
3302
- throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
4641
+ const value = joinFieldValue(tokens, line);
4642
+ switch (key) {
4643
+ case "title":
4644
+ section.system.title = value;
4645
+ return;
4646
+ case "description":
4647
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
4648
+ line,
4649
+ column: tokens[0].column
4650
+ });
4651
+ section.system.description = value;
4652
+ return;
4653
+ case "epoch":
4654
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
4655
+ line,
4656
+ column: tokens[0].column
4657
+ });
4658
+ section.system.epoch = value;
4659
+ return;
4660
+ case "referenceplane":
4661
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "referencePlane", {
4662
+ line,
4663
+ column: tokens[0].column
4664
+ });
4665
+ section.system.referencePlane = value;
4666
+ return;
4667
+ default:
4668
+ throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
3303
4669
  }
3304
- section.system.title = joinFieldValue(tokens, line);
3305
4670
  }
3306
4671
  function applyDefaultsField(section, tokens, line) {
3307
4672
  const key = requireUniqueField(tokens, section.seenFields, line);
@@ -3332,14 +4697,11 @@
3332
4697
  section.metadataIndent = null;
3333
4698
  }
3334
4699
  if (section.inMetadata) {
3335
- if (tokens.length < 2) {
3336
- throw new WorldOrbitError("Invalid atlas metadata entry", line, tokens[0]?.column ?? 1);
4700
+ const entry = parseInfoLikeEntry(tokens, line, "Invalid atlas metadata entry");
4701
+ if (entry.key in section.system.atlasMetadata) {
4702
+ throw new WorldOrbitError(`Duplicate atlas metadata key "${entry.key}"`, line, tokens[0].column);
3337
4703
  }
3338
- const key = tokens[0].value;
3339
- if (key in section.system.atlasMetadata) {
3340
- throw new WorldOrbitError(`Duplicate atlas metadata key "${key}"`, line, tokens[0].column);
3341
- }
3342
- section.system.atlasMetadata[key] = joinFieldValue(tokens, line);
4704
+ section.system.atlasMetadata[entry.key] = entry.value;
3343
4705
  return;
3344
4706
  }
3345
4707
  if (tokens.length === 1 && tokens[0].value.toLowerCase() === "metadata") {
@@ -3395,7 +4757,14 @@
3395
4757
  section.viewpoint.rotationDeg = parseFiniteNumber2(value, line, tokens[0].column, "rotation");
3396
4758
  return;
3397
4759
  case "layers":
3398
- section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line);
4760
+ section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line, section.sourceSchemaVersion, section.diagnostics);
4761
+ return;
4762
+ case "events":
4763
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.events", {
4764
+ line,
4765
+ column: tokens[0].column
4766
+ });
4767
+ section.viewpoint.events = parseTokenList(tokens.slice(1), line, "events");
3399
4768
  return;
3400
4769
  default:
3401
4770
  throw new WorldOrbitError(`Unknown viewpoint field "${tokens[0].value}"`, line, tokens[0].column);
@@ -3441,21 +4810,202 @@
3441
4810
  throw new WorldOrbitError(`Unknown annotation field "${tokens[0].value}"`, line, tokens[0].column);
3442
4811
  }
3443
4812
  }
3444
- function applyObjectField(section, indent, tokens, line) {
3445
- if (tokens.length === 1 && tokens[0].value === "info") {
3446
- section.inInfoBlock = true;
3447
- section.infoIndent = indent;
4813
+ function applyGroupField(section, tokens, line) {
4814
+ const key = requireUniqueField(tokens, section.seenFields, line);
4815
+ switch (key) {
4816
+ case "label":
4817
+ section.group.label = joinFieldValue(tokens, line);
4818
+ return;
4819
+ case "summary":
4820
+ section.group.summary = joinFieldValue(tokens, line);
4821
+ return;
4822
+ case "color":
4823
+ section.group.color = joinFieldValue(tokens, line);
4824
+ return;
4825
+ case "tags":
4826
+ section.group.tags = parseTokenList(tokens.slice(1), line, "tags");
4827
+ return;
4828
+ case "hidden":
4829
+ section.group.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4830
+ line,
4831
+ column: tokens[0].column
4832
+ });
4833
+ return;
4834
+ default:
4835
+ throw new WorldOrbitError(`Unknown group field "${tokens[0].value}"`, line, tokens[0].column);
4836
+ }
4837
+ }
4838
+ function applyRelationField(section, tokens, line) {
4839
+ const key = requireUniqueField(tokens, section.seenFields, line);
4840
+ switch (key) {
4841
+ case "from":
4842
+ section.relation.from = joinFieldValue(tokens, line);
4843
+ return;
4844
+ case "to":
4845
+ section.relation.to = joinFieldValue(tokens, line);
4846
+ return;
4847
+ case "kind":
4848
+ section.relation.kind = joinFieldValue(tokens, line);
4849
+ return;
4850
+ case "label":
4851
+ section.relation.label = joinFieldValue(tokens, line);
4852
+ return;
4853
+ case "summary":
4854
+ section.relation.summary = joinFieldValue(tokens, line);
4855
+ return;
4856
+ case "tags":
4857
+ section.relation.tags = parseTokenList(tokens.slice(1), line, "tags");
4858
+ return;
4859
+ case "color":
4860
+ section.relation.color = joinFieldValue(tokens, line);
4861
+ return;
4862
+ case "hidden":
4863
+ section.relation.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4864
+ line,
4865
+ column: tokens[0].column
4866
+ });
4867
+ return;
4868
+ default:
4869
+ throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
4870
+ }
4871
+ }
4872
+ function applyEventField(section, indent, tokens, line) {
4873
+ if (section.activePose && indent <= (section.poseIndent ?? 0)) {
4874
+ section.activePose = null;
4875
+ section.poseIndent = null;
4876
+ section.activePoseSeenFields.clear();
4877
+ }
4878
+ if (!section.activePose && section.inPositions && indent <= (section.positionsIndent ?? 0)) {
4879
+ section.inPositions = false;
4880
+ section.positionsIndent = null;
4881
+ }
4882
+ if (section.activePose) {
4883
+ section.activePose.fields.push(parseEventPoseField(tokens, line, section.activePoseSeenFields));
4884
+ return;
4885
+ }
4886
+ if (section.inPositions) {
4887
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "pose") {
4888
+ throw new WorldOrbitError(`Unknown event positions field "${tokens[0].value}"`, line, tokens[0]?.column ?? 1);
4889
+ }
4890
+ const objectId = tokens[1].value;
4891
+ if (!objectId.trim()) {
4892
+ throw new WorldOrbitError("Event pose object id must not be empty", line, tokens[1].column);
4893
+ }
4894
+ const rawPose = {
4895
+ objectId,
4896
+ fields: [],
4897
+ location: { line, column: tokens[0].column }
4898
+ };
4899
+ section.rawPoses.push(rawPose);
4900
+ section.activePose = rawPose;
4901
+ section.poseIndent = indent;
4902
+ section.activePoseSeenFields = /* @__PURE__ */ new Set();
3448
4903
  return;
3449
4904
  }
3450
- if (section.inInfoBlock && indent <= (section.infoIndent ?? 0)) {
3451
- section.inInfoBlock = false;
3452
- section.infoIndent = null;
4905
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "positions") {
4906
+ if (section.seenFields.has("positions")) {
4907
+ throw new WorldOrbitError('Duplicate event field "positions"', line, tokens[0].column);
4908
+ }
4909
+ section.seenFields.add("positions");
4910
+ section.inPositions = true;
4911
+ section.positionsIndent = indent;
4912
+ return;
4913
+ }
4914
+ const key = requireUniqueField(tokens, section.seenFields, line);
4915
+ switch (key) {
4916
+ case "kind":
4917
+ section.event.kind = joinFieldValue(tokens, line);
4918
+ return;
4919
+ case "label":
4920
+ section.event.label = joinFieldValue(tokens, line);
4921
+ return;
4922
+ case "summary":
4923
+ section.event.summary = joinFieldValue(tokens, line);
4924
+ return;
4925
+ case "target":
4926
+ section.event.targetObjectId = joinFieldValue(tokens, line);
4927
+ return;
4928
+ case "participants":
4929
+ section.event.participantObjectIds = parseTokenList(tokens.slice(1), line, "participants");
4930
+ return;
4931
+ case "timing":
4932
+ section.event.timing = joinFieldValue(tokens, line);
4933
+ return;
4934
+ case "visibility":
4935
+ section.event.visibility = joinFieldValue(tokens, line);
4936
+ return;
4937
+ case "tags":
4938
+ section.event.tags = parseTokenList(tokens.slice(1), line, "tags");
4939
+ return;
4940
+ case "color":
4941
+ section.event.color = joinFieldValue(tokens, line);
4942
+ return;
4943
+ case "hidden":
4944
+ section.event.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4945
+ line,
4946
+ column: tokens[0].column
4947
+ });
4948
+ return;
4949
+ default:
4950
+ throw new WorldOrbitError(`Unknown event field "${tokens[0].value}"`, line, tokens[0].column);
4951
+ }
4952
+ }
4953
+ function parseEventPoseField(tokens, line, seenFields) {
4954
+ if (tokens.length < 2) {
4955
+ throw new WorldOrbitError("Invalid event pose field line", line, tokens[0]?.column ?? 1);
4956
+ }
4957
+ const key = tokens[0].value;
4958
+ if (!EVENT_POSE_FIELD_KEYS.has(key)) {
4959
+ throw new WorldOrbitError(`Unknown event pose field "${key}"`, line, tokens[0].column);
4960
+ }
4961
+ if (seenFields.has(key)) {
4962
+ throw new WorldOrbitError(`Duplicate event pose field "${key}"`, line, tokens[0].column);
4963
+ }
4964
+ seenFields.add(key);
4965
+ return {
4966
+ type: "field",
4967
+ key,
4968
+ values: tokens.slice(1).map((token) => token.value),
4969
+ location: { line, column: tokens[0].column }
4970
+ };
4971
+ }
4972
+ function applyObjectField(section, indent, tokens, line) {
4973
+ if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
4974
+ section.activeBlock = null;
4975
+ section.blockIndent = null;
4976
+ }
4977
+ if (tokens.length === 1) {
4978
+ const blockName = tokens[0].value.toLowerCase();
4979
+ if (blockName === "info" || STRUCTURED_TYPED_BLOCKS.has(blockName)) {
4980
+ if (blockName !== "info") {
4981
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, blockName, { line, column: tokens[0].column });
4982
+ }
4983
+ section.activeBlock = blockName;
4984
+ section.blockIndent = indent;
4985
+ return;
4986
+ }
3453
4987
  }
3454
- if (section.inInfoBlock) {
3455
- section.objectNode.infoEntries.push(parseInfoEntry2(tokens, line));
4988
+ if (section.activeBlock) {
4989
+ const entry = parseInfoLikeEntry(tokens, line, `Invalid ${section.activeBlock} entry`);
4990
+ if (section.activeBlock === "info") {
4991
+ if (section.seenInfoKeys.has(entry.key)) {
4992
+ throw new WorldOrbitError(`Duplicate info key "${entry.key}"`, line, tokens[0].column);
4993
+ }
4994
+ section.seenInfoKeys.add(entry.key);
4995
+ section.objectNode.infoEntries.push(entry);
4996
+ return;
4997
+ }
4998
+ const typedBlock = section.activeBlock;
4999
+ const seenKeys = section.seenTypedBlockKeys[typedBlock] ?? (section.seenTypedBlockKeys[typedBlock] = /* @__PURE__ */ new Set());
5000
+ if (seenKeys.has(entry.key)) {
5001
+ throw new WorldOrbitError(`Duplicate ${typedBlock} key "${entry.key}"`, line, tokens[0].column);
5002
+ }
5003
+ seenKeys.add(entry.key);
5004
+ const entries = section.objectNode.typedBlockEntries[typedBlock] ?? (section.objectNode.typedBlockEntries[typedBlock] = []);
5005
+ entries.push(entry);
3456
5006
  return;
3457
5007
  }
3458
- section.objectNode.blockFields.push(parseField2(tokens, line));
5008
+ section.objectNode.fields.push(parseObjectField(tokens, line, section.objectNode.objectType, section.sourceSchemaVersion, section.diagnostics));
3459
5009
  }
3460
5010
  function requireUniqueField(tokens, seenFields, line) {
3461
5011
  if (tokens.length < 2) {
@@ -3475,50 +5025,46 @@
3475
5025
  return tokens.slice(1).map((token) => token.value).join(" ").trim();
3476
5026
  }
3477
5027
  function parseObjectTypeTokens(tokens, line) {
3478
- if (tokens.length === 0) {
3479
- throw new WorldOrbitError("Missing value for atlas field", line);
3480
- }
3481
- return tokens.map((token) => {
3482
- const value = token.value;
3483
- if (value !== "star" && value !== "planet" && value !== "moon" && value !== "belt" && value !== "asteroid" && value !== "comet" && value !== "ring" && value !== "structure" && value !== "phenomenon") {
3484
- throw new WorldOrbitError(`Unknown viewpoint object type "${token.value}"`, line, token.column);
3485
- }
3486
- return value;
3487
- });
5028
+ 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");
3488
5029
  }
3489
- function parseTokenList(tokens, line, field) {
3490
- if (tokens.length === 0) {
3491
- throw new WorldOrbitError(`Missing value for field "${field}"`, line);
5030
+ function parseLayerTokens(tokens, line, sourceSchemaVersion, diagnostics) {
5031
+ const layers = {};
5032
+ for (const token of parseTokenList(tokens, line, "layers")) {
5033
+ const enabled = !token.startsWith("-") && !token.startsWith("!");
5034
+ const raw = token.replace(/^[-!]+/, "").toLowerCase();
5035
+ if (raw === "orbits") {
5036
+ layers["orbits-back"] = enabled;
5037
+ layers["orbits-front"] = enabled;
5038
+ continue;
5039
+ }
5040
+ if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "events" || raw === "objects" || raw === "labels" || raw === "metadata") {
5041
+ if (raw === "events" && sourceSchemaVersion && diagnostics) {
5042
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "layers.events", {
5043
+ line,
5044
+ column: tokens[0]?.column ?? 1
5045
+ });
5046
+ }
5047
+ layers[raw] = enabled;
5048
+ }
3492
5049
  }
3493
- return tokens.map((token) => token.value);
5050
+ return layers;
3494
5051
  }
3495
- function parseLayerTokens(tokens, line) {
5052
+ function parseTokenList(tokens, line, fieldName) {
3496
5053
  if (tokens.length === 0) {
3497
- throw new WorldOrbitError('Missing value for field "layers"', line);
5054
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, 1);
3498
5055
  }
3499
- const next = {};
3500
- for (const token of tokens) {
3501
- const enabled = !token.value.startsWith("-") && !token.value.startsWith("!");
3502
- const rawLayer = token.value.replace(/^[-!]+/, "").toLowerCase();
3503
- if (rawLayer === "orbits") {
3504
- next["orbits-back"] = enabled;
3505
- next["orbits-front"] = enabled;
3506
- continue;
3507
- }
3508
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
3509
- next[rawLayer] = enabled;
3510
- continue;
3511
- }
3512
- throw new WorldOrbitError(`Unknown layer token "${token.value}"`, line, token.column);
5056
+ const values = tokens.map((token) => token.value).filter(Boolean);
5057
+ if (values.length === 0) {
5058
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, tokens[0]?.column ?? 1);
3513
5059
  }
3514
- return next;
5060
+ return values;
3515
5061
  }
3516
5062
  function parseProjectionValue(value, line, column) {
3517
5063
  const normalized = value.toLowerCase();
3518
- if (normalized === "topdown" || normalized === "isometric") {
3519
- return normalized;
5064
+ if (normalized !== "topdown" && normalized !== "isometric") {
5065
+ throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
3520
5066
  }
3521
- throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
5067
+ return normalized;
3522
5068
  }
3523
5069
  function parsePresetValue(value, line, column) {
3524
5070
  const normalized = value.toLowerCase();
@@ -3528,16 +5074,16 @@
3528
5074
  throw new WorldOrbitError(`Unknown render preset "${value}"`, line, column);
3529
5075
  }
3530
5076
  function parsePositiveNumber2(value, line, column, field) {
3531
- const parsed = Number(value);
3532
- if (!Number.isFinite(parsed) || parsed <= 0) {
3533
- throw new WorldOrbitError(`Field "${field}" expects a positive number`, line, column);
5077
+ const parsed = parseFiniteNumber2(value, line, column, field);
5078
+ if (parsed <= 0) {
5079
+ throw new WorldOrbitError(`Field "${field}" must be greater than zero`, line, column);
3534
5080
  }
3535
5081
  return parsed;
3536
5082
  }
3537
5083
  function parseFiniteNumber2(value, line, column, field) {
3538
5084
  const parsed = Number(value);
3539
5085
  if (!Number.isFinite(parsed)) {
3540
- throw new WorldOrbitError(`Field "${field}" expects a finite number`, line, column);
5086
+ throw new WorldOrbitError(`Invalid numeric value "${value}" for "${field}"`, line, column);
3541
5087
  }
3542
5088
  return parsed;
3543
5089
  }
@@ -3549,28 +5095,43 @@
3549
5095
  groupIds: []
3550
5096
  };
3551
5097
  }
3552
- function parseInlineFields2(tokens, line) {
5098
+ function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
3553
5099
  const fields = [];
3554
5100
  let index = 0;
3555
5101
  while (index < tokens.length) {
3556
5102
  const keyToken = tokens[index];
3557
- const schema = getFieldSchema(keyToken.value);
3558
- if (!schema) {
5103
+ const spec = getDraftObjectFieldSpec(keyToken.value);
5104
+ if (!spec) {
3559
5105
  throw new WorldOrbitError(`Unknown field "${keyToken.value}"`, line, keyToken.column);
3560
5106
  }
5107
+ if (spec.version === "2.1") {
5108
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, keyToken.value, {
5109
+ line,
5110
+ column: keyToken.column
5111
+ });
5112
+ }
3561
5113
  index++;
3562
5114
  const valueTokens = [];
3563
- if (schema.arity === "multiple") {
3564
- while (index < tokens.length && !isKnownFieldKey(tokens[index].value)) {
3565
- valueTokens.push(tokens[index]);
3566
- index++;
3567
- }
3568
- } else {
5115
+ if (spec.inlineMode === "single") {
3569
5116
  const nextToken = tokens[index];
3570
5117
  if (nextToken) {
3571
5118
  valueTokens.push(nextToken);
3572
5119
  index++;
3573
5120
  }
5121
+ } else if (spec.inlineMode === "pair") {
5122
+ for (let count = 0; count < 2; count++) {
5123
+ const nextToken = tokens[index];
5124
+ if (!nextToken) {
5125
+ break;
5126
+ }
5127
+ valueTokens.push(nextToken);
5128
+ index++;
5129
+ }
5130
+ } else {
5131
+ while (index < tokens.length && !DRAFT_OBJECT_FIELD_KEYS.has(tokens[index].value)) {
5132
+ valueTokens.push(tokens[index]);
5133
+ index++;
5134
+ }
3574
5135
  }
3575
5136
  if (valueTokens.length === 0) {
3576
5137
  throw new WorldOrbitError(`Missing value for field "${keyToken.value}"`, line, keyToken.column);
@@ -3582,25 +5143,35 @@
3582
5143
  location: { line, column: keyToken.column }
3583
5144
  });
3584
5145
  }
5146
+ validateDraftObjectFieldCompatibility(fields, objectType);
3585
5147
  return fields;
3586
5148
  }
3587
- function parseField2(tokens, line) {
5149
+ function parseObjectField(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
3588
5150
  if (tokens.length < 2) {
3589
5151
  throw new WorldOrbitError("Invalid field line", line, tokens[0]?.column ?? 1);
3590
5152
  }
3591
- if (!getFieldSchema(tokens[0].value)) {
5153
+ const spec = getDraftObjectFieldSpec(tokens[0].value);
5154
+ if (!spec) {
3592
5155
  throw new WorldOrbitError(`Unknown field "${tokens[0].value}"`, line, tokens[0].column);
3593
5156
  }
3594
- return {
5157
+ if (spec.version === "2.1") {
5158
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, tokens[0].value, {
5159
+ line,
5160
+ column: tokens[0].column
5161
+ });
5162
+ }
5163
+ const field = {
3595
5164
  type: "field",
3596
5165
  key: tokens[0].value,
3597
5166
  values: tokens.slice(1).map((token) => token.value),
3598
5167
  location: { line, column: tokens[0].column }
3599
5168
  };
5169
+ validateDraftObjectFieldCompatibility([field], objectType);
5170
+ return field;
3600
5171
  }
3601
- function parseInfoEntry2(tokens, line) {
5172
+ function parseInfoLikeEntry(tokens, line, errorMessage) {
3602
5173
  if (tokens.length < 2) {
3603
- throw new WorldOrbitError("Invalid info entry", line, tokens[0]?.column ?? 1);
5174
+ throw new WorldOrbitError(errorMessage, line, tokens[0]?.column ?? 1);
3604
5175
  }
3605
5176
  return {
3606
5177
  type: "info-entry",
@@ -3609,23 +5180,374 @@
3609
5180
  location: { line, column: tokens[0].column }
3610
5181
  };
3611
5182
  }
3612
- function normalizeIdentifier2(value) {
3613
- return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
5183
+ function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
5184
+ const fieldMap = collectDraftFields(node.fields);
5185
+ const placement = extractPlacementFromFieldMap(fieldMap);
5186
+ const properties = normalizeDraftProperties(node.objectType, fieldMap);
5187
+ const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
5188
+ const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
5189
+ const referencePlane = parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0]);
5190
+ const tidalLock = fieldMap.has("tidalLock") ? parseAtlasBoolean(singleFieldValue2(fieldMap.get("tidalLock")[0]), "tidalLock", fieldMap.get("tidalLock")[0].location) : void 0;
5191
+ const resonance = fieldMap.has("resonance") ? parseResonanceField(fieldMap.get("resonance")[0]) : void 0;
5192
+ const renderHints = extractRenderHints(fieldMap);
5193
+ const deriveRules = fieldMap.get("derive")?.map((field) => parseDeriveField(field));
5194
+ const validationRules = fieldMap.get("validate")?.map((field) => ({
5195
+ rule: singleFieldValue2(field)
5196
+ }));
5197
+ const lockedFields = fieldMap.has("locked") ? [...new Set(fieldMap.get("locked").flatMap((field) => field.values))] : void 0;
5198
+ const tolerances = fieldMap.get("tolerance")?.map((field) => parseToleranceField(field));
5199
+ const typedBlocks = normalizeTypedBlocks(node.typedBlockEntries);
5200
+ const info2 = normalizeInfoEntries(node.infoEntries, "info");
5201
+ const object = {
5202
+ type: node.objectType,
5203
+ id: node.id,
5204
+ properties,
5205
+ placement,
5206
+ info: info2
5207
+ };
5208
+ if (groups.length > 0)
5209
+ object.groups = groups;
5210
+ if (epoch)
5211
+ object.epoch = epoch;
5212
+ if (referencePlane)
5213
+ object.referencePlane = referencePlane;
5214
+ if (tidalLock !== void 0)
5215
+ object.tidalLock = tidalLock;
5216
+ if (resonance)
5217
+ object.resonance = resonance;
5218
+ if (renderHints)
5219
+ object.renderHints = renderHints;
5220
+ if (deriveRules?.length)
5221
+ object.deriveRules = deriveRules;
5222
+ if (validationRules?.length)
5223
+ object.validationRules = validationRules;
5224
+ if (lockedFields?.length)
5225
+ object.lockedFields = lockedFields;
5226
+ if (tolerances?.length)
5227
+ object.tolerances = tolerances;
5228
+ if (typedBlocks && Object.keys(typedBlocks).length > 0)
5229
+ object.typedBlocks = typedBlocks;
5230
+ if (sourceSchemaVersion !== "2.1") {
5231
+ 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) {
5232
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
5233
+ }
5234
+ }
5235
+ return object;
3614
5236
  }
3615
- function humanizeIdentifier3(value) {
3616
- return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
5237
+ function normalizeDraftEvent(event, rawPoses) {
5238
+ return {
5239
+ ...event,
5240
+ participantObjectIds: [...new Set(event.participantObjectIds)],
5241
+ tags: [...new Set(event.tags)],
5242
+ positions: rawPoses.map((pose) => normalizeDraftEventPose(pose))
5243
+ };
5244
+ }
5245
+ function normalizeDraftEventPose(rawPose) {
5246
+ const fieldMap = collectDraftFields(rawPose.fields);
5247
+ const placement = extractPlacementFromFieldMap(fieldMap);
5248
+ return {
5249
+ objectId: rawPose.objectId,
5250
+ placement,
5251
+ inner: parseOptionalUnitField(fieldMap.get("inner")?.[0], "inner"),
5252
+ outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer")
5253
+ };
5254
+ }
5255
+ function collectDraftFields(fields) {
5256
+ const grouped = /* @__PURE__ */ new Map();
5257
+ for (const field of fields) {
5258
+ const spec = getDraftObjectFieldSpec(field.key);
5259
+ if (!spec) {
5260
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
5261
+ }
5262
+ if (!spec.allowRepeat && grouped.has(field.key)) {
5263
+ throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
5264
+ }
5265
+ const existing = grouped.get(field.key) ?? [];
5266
+ existing.push(field);
5267
+ grouped.set(field.key, existing);
5268
+ }
5269
+ return grouped;
5270
+ }
5271
+ function extractPlacementFromFieldMap(fieldMap) {
5272
+ const orbitField = fieldMap.get("orbit")?.[0];
5273
+ const atField = fieldMap.get("at")?.[0];
5274
+ const surfaceField = fieldMap.get("surface")?.[0];
5275
+ const freeField = fieldMap.get("free")?.[0];
5276
+ const count = [orbitField, atField, surfaceField, freeField].filter(Boolean).length;
5277
+ if (count > 1) {
5278
+ const conflictingField = orbitField ?? atField ?? surfaceField ?? freeField;
5279
+ throw WorldOrbitError.fromLocation("Object has multiple placement modes", conflictingField?.location);
5280
+ }
5281
+ if (orbitField) {
5282
+ return {
5283
+ mode: "orbit",
5284
+ target: singleFieldValue2(orbitField),
5285
+ distance: parseOptionalUnitField(fieldMap.get("distance")?.[0], "distance"),
5286
+ semiMajor: parseOptionalUnitField(fieldMap.get("semiMajor")?.[0], "semiMajor"),
5287
+ eccentricity: parseOptionalNumberField(fieldMap.get("eccentricity")?.[0], "eccentricity"),
5288
+ period: parseOptionalUnitField(fieldMap.get("period")?.[0], "period"),
5289
+ angle: parseOptionalUnitField(fieldMap.get("angle")?.[0], "angle"),
5290
+ inclination: parseOptionalUnitField(fieldMap.get("inclination")?.[0], "inclination"),
5291
+ phase: parseOptionalUnitField(fieldMap.get("phase")?.[0], "phase")
5292
+ };
5293
+ }
5294
+ if (atField) {
5295
+ const target = singleFieldValue2(atField);
5296
+ return {
5297
+ mode: "at",
5298
+ target,
5299
+ reference: parseAtlasAtReference(target, atField.location)
5300
+ };
5301
+ }
5302
+ if (surfaceField) {
5303
+ return {
5304
+ mode: "surface",
5305
+ target: singleFieldValue2(surfaceField)
5306
+ };
5307
+ }
5308
+ if (freeField) {
5309
+ const raw = singleFieldValue2(freeField);
5310
+ const distance = tryParseAtlasUnitValue(raw);
5311
+ return {
5312
+ mode: "free",
5313
+ distance: distance ?? void 0,
5314
+ descriptor: distance ? void 0 : raw
5315
+ };
5316
+ }
5317
+ return null;
5318
+ }
5319
+ function normalizeDraftProperties(objectType, fieldMap) {
5320
+ const properties = {};
5321
+ for (const [key, fields] of fieldMap.entries()) {
5322
+ const field = fields[0];
5323
+ const spec = getDraftObjectFieldSpec(key);
5324
+ if (!field || !spec?.legacySchema || spec.legacySchema.placement) {
5325
+ continue;
5326
+ }
5327
+ ensureAtlasFieldSupported(key, objectType, field.location);
5328
+ properties[key] = normalizeLegacyScalarValue(key, field.values, field.location);
5329
+ }
5330
+ return properties;
5331
+ }
5332
+ function normalizeInfoEntries(entries, label) {
5333
+ const normalized = {};
5334
+ for (const entry of entries) {
5335
+ if (entry.key in normalized) {
5336
+ throw WorldOrbitError.fromLocation(`Duplicate ${label} key "${entry.key}"`, entry.location);
5337
+ }
5338
+ normalized[entry.key] = entry.value;
5339
+ }
5340
+ return normalized;
5341
+ }
5342
+ function normalizeTypedBlocks(typedBlockEntries) {
5343
+ const typedBlocks = {};
5344
+ for (const blockName of Object.keys(typedBlockEntries)) {
5345
+ const entries = typedBlockEntries[blockName];
5346
+ if (entries?.length) {
5347
+ typedBlocks[blockName] = normalizeInfoEntries(entries, blockName);
5348
+ }
5349
+ }
5350
+ return typedBlocks;
5351
+ }
5352
+ function extractRenderHints(fieldMap) {
5353
+ const renderHints = {};
5354
+ const renderLabelField = fieldMap.get("renderLabel")?.[0];
5355
+ const renderOrbitField = fieldMap.get("renderOrbit")?.[0];
5356
+ const renderPriorityField = fieldMap.get("renderPriority")?.[0];
5357
+ if (renderLabelField) {
5358
+ renderHints.renderLabel = parseAtlasBoolean(singleFieldValue2(renderLabelField), "renderLabel", renderLabelField.location);
5359
+ }
5360
+ if (renderOrbitField) {
5361
+ renderHints.renderOrbit = parseAtlasBoolean(singleFieldValue2(renderOrbitField), "renderOrbit", renderOrbitField.location);
5362
+ }
5363
+ if (renderPriorityField) {
5364
+ renderHints.renderPriority = parseAtlasNumber(singleFieldValue2(renderPriorityField), "renderPriority", renderPriorityField.location);
5365
+ }
5366
+ return Object.keys(renderHints).length > 0 ? renderHints : void 0;
5367
+ }
5368
+ function parseResonanceField(field) {
5369
+ if (field.values.length !== 2) {
5370
+ throw WorldOrbitError.fromLocation('Field "resonance" expects "<targetObjectId> <ratio>"', field.location);
5371
+ }
5372
+ const ratio = field.values[1];
5373
+ if (!/^\d+:\d+$/.test(ratio)) {
5374
+ throw WorldOrbitError.fromLocation(`Invalid resonance ratio "${ratio}"`, field.location);
5375
+ }
5376
+ return {
5377
+ targetObjectId: field.values[0],
5378
+ ratio
5379
+ };
5380
+ }
5381
+ function parseDeriveField(field) {
5382
+ if (field.values.length !== 2) {
5383
+ throw WorldOrbitError.fromLocation('Field "derive" expects "<field> <strategy>"', field.location);
5384
+ }
5385
+ return {
5386
+ field: field.values[0],
5387
+ strategy: field.values[1]
5388
+ };
5389
+ }
5390
+ function parseToleranceField(field) {
5391
+ if (field.values.length !== 2) {
5392
+ throw WorldOrbitError.fromLocation('Field "tolerance" expects "<field> <value>"', field.location);
5393
+ }
5394
+ const rawValue = field.values[1];
5395
+ const unitValue = tryParseAtlasUnitValue(rawValue);
5396
+ const numericValue = Number(rawValue);
5397
+ return {
5398
+ field: field.values[0],
5399
+ value: unitValue ?? (Number.isFinite(numericValue) ? numericValue : rawValue)
5400
+ };
5401
+ }
5402
+ function parseOptionalTokenList(field) {
5403
+ return field ? [...new Set(field.values)] : [];
5404
+ }
5405
+ function parseOptionalJoinedValue(field) {
5406
+ if (!field) {
5407
+ return null;
5408
+ }
5409
+ return field.values.join(" ").trim() || null;
5410
+ }
5411
+ function parseOptionalUnitField(field, key) {
5412
+ return field ? parseAtlasUnitValue(singleFieldValue2(field), field.location, key) : void 0;
5413
+ }
5414
+ function parseOptionalNumberField(field, key) {
5415
+ return field ? parseAtlasNumber(singleFieldValue2(field), key, field.location) : void 0;
5416
+ }
5417
+ function singleFieldValue2(field) {
5418
+ return singleAtlasValue(field.values, field.key, field.location);
5419
+ }
5420
+ function getDraftObjectFieldSpec(key) {
5421
+ return DRAFT_OBJECT_FIELD_SPECS.get(key);
5422
+ }
5423
+ function validateDraftObjectFieldCompatibility(fields, objectType) {
5424
+ for (const field of fields) {
5425
+ const spec = getDraftObjectFieldSpec(field.key);
5426
+ if (!spec) {
5427
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
5428
+ }
5429
+ if (spec.legacySchema) {
5430
+ ensureAtlasFieldSupported(field.key, objectType, field.location);
5431
+ continue;
5432
+ }
5433
+ if ((field.key === "renderLabel" || field.key === "renderOrbit" || field.key === "tidalLock") && field.values.length !== 1) {
5434
+ throw WorldOrbitError.fromLocation(`Field "${field.key}" expects exactly one value`, field.location);
5435
+ }
5436
+ }
5437
+ }
5438
+ function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
5439
+ if (sourceSchemaVersion === "2.1") {
5440
+ return;
5441
+ }
5442
+ diagnostics.push({
5443
+ code: "parse.schema21.featureCompatibility",
5444
+ severity: "warning",
5445
+ source: "parse",
5446
+ message: `Feature "${featureName}" requires schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
5447
+ line: location.line,
5448
+ column: location.column
5449
+ });
5450
+ }
5451
+ function preprocessAtlasSource(source) {
5452
+ const chars = [...source];
5453
+ const comments = [];
5454
+ let inString = false;
5455
+ let inBlockComment = false;
5456
+ let blockCommentStart = null;
5457
+ let line = 1;
5458
+ let column = 1;
5459
+ for (let index = 0; index < chars.length; index++) {
5460
+ const ch = chars[index];
5461
+ const next = chars[index + 1];
5462
+ if (inBlockComment) {
5463
+ if (ch === "*" && next === "/") {
5464
+ chars[index] = " ";
5465
+ chars[index + 1] = " ";
5466
+ inBlockComment = false;
5467
+ blockCommentStart = null;
5468
+ index++;
5469
+ column += 2;
5470
+ continue;
5471
+ }
5472
+ if (ch !== "\n" && ch !== "\r") {
5473
+ chars[index] = " ";
5474
+ }
5475
+ if (ch === "\n") {
5476
+ line++;
5477
+ column = 1;
5478
+ } else {
5479
+ column++;
5480
+ }
5481
+ continue;
5482
+ }
5483
+ if (!inString && ch === "/" && next === "*") {
5484
+ comments.push({ kind: "block", line, column });
5485
+ chars[index] = " ";
5486
+ chars[index + 1] = " ";
5487
+ inBlockComment = true;
5488
+ blockCommentStart = { line, column };
5489
+ index++;
5490
+ column += 2;
5491
+ continue;
5492
+ }
5493
+ if (!inString && ch === "#" && !isHexColorLiteral(chars, index)) {
5494
+ comments.push({ kind: "line", line, column });
5495
+ chars[index] = " ";
5496
+ let inner = index + 1;
5497
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
5498
+ chars[inner] = " ";
5499
+ inner++;
5500
+ }
5501
+ column += inner - index;
5502
+ index = inner - 1;
5503
+ continue;
5504
+ }
5505
+ if (ch === '"' && chars[index - 1] !== "\\") {
5506
+ inString = !inString;
5507
+ }
5508
+ if (ch === "\n") {
5509
+ line++;
5510
+ column = 1;
5511
+ } else {
5512
+ column++;
5513
+ }
5514
+ }
5515
+ if (inBlockComment) {
5516
+ throw WorldOrbitError.fromLocation("Unclosed block comment", blockCommentStart ?? void 0);
5517
+ }
5518
+ return {
5519
+ source: chars.join(""),
5520
+ comments
5521
+ };
5522
+ }
5523
+ function isHexColorLiteral(chars, start) {
5524
+ let index = start + 1;
5525
+ let length = 0;
5526
+ while (index < chars.length && /[0-9a-f]/i.test(chars[index] ?? "")) {
5527
+ index++;
5528
+ length++;
5529
+ }
5530
+ if (![3, 4, 6, 8].includes(length)) {
5531
+ return false;
5532
+ }
5533
+ const next = chars[index];
5534
+ return next === void 0 || next === " " || next === " " || next === "\r" || next === "\n";
3617
5535
  }
3618
5536
 
3619
5537
  // packages/core/dist/atlas-edit.js
3620
- function createEmptyAtlasDocument(systemId = "WorldOrbit") {
5538
+ function createEmptyAtlasDocument(systemId = "WorldOrbit", version = "2.0") {
3621
5539
  return {
3622
5540
  format: "worldorbit",
3623
- version: "2.0",
5541
+ version,
5542
+ schemaVersion: version,
3624
5543
  sourceVersion: "1.0",
3625
5544
  system: {
3626
5545
  type: "system",
3627
5546
  id: systemId,
3628
5547
  title: systemId,
5548
+ description: null,
5549
+ epoch: null,
5550
+ referencePlane: null,
3629
5551
  defaults: {
3630
5552
  view: "topdown",
3631
5553
  scale: null,
@@ -3637,6 +5559,9 @@
3637
5559
  viewpoints: [],
3638
5560
  annotations: []
3639
5561
  },
5562
+ groups: [],
5563
+ relations: [],
5564
+ events: [],
3640
5565
  objects: [],
3641
5566
  diagnostics: []
3642
5567
  };
@@ -3650,14 +5575,26 @@
3650
5575
  for (const key of Object.keys(document.system.atlasMetadata).sort()) {
3651
5576
  paths.push({ kind: "metadata", key });
3652
5577
  }
3653
- for (const viewpoint of [...document.system.viewpoints].sort(compareIdLike)) {
5578
+ for (const viewpoint of [...document.system.viewpoints].sort(compareIdLike2)) {
3654
5579
  paths.push({ kind: "viewpoint", id: viewpoint.id });
3655
5580
  }
3656
- for (const annotation of [...document.system.annotations].sort(compareIdLike)) {
5581
+ for (const annotation of [...document.system.annotations].sort(compareIdLike2)) {
3657
5582
  paths.push({ kind: "annotation", id: annotation.id });
3658
5583
  }
3659
5584
  }
3660
- for (const object of [...document.objects].sort(compareIdLike)) {
5585
+ for (const group of [...document.groups].sort(compareIdLike2)) {
5586
+ paths.push({ kind: "group", id: group.id });
5587
+ }
5588
+ for (const relation of [...document.relations].sort(compareIdLike2)) {
5589
+ paths.push({ kind: "relation", id: relation.id });
5590
+ }
5591
+ for (const event of [...document.events].sort(compareIdLike2)) {
5592
+ paths.push({ kind: "event", id: event.id });
5593
+ for (const pose of [...event.positions].sort(comparePoseObjectId2)) {
5594
+ paths.push({ kind: "event-pose", id: event.id, key: pose.objectId });
5595
+ }
5596
+ }
5597
+ for (const object of [...document.objects].sort(compareIdLike2)) {
3661
5598
  paths.push({ kind: "object", id: object.id });
3662
5599
  }
3663
5600
  return paths;
@@ -3670,12 +5607,20 @@
3670
5607
  return document.system?.defaults ?? null;
3671
5608
  case "metadata":
3672
5609
  return path.key ? document.system?.atlasMetadata[path.key] ?? null : null;
5610
+ case "group":
5611
+ return path.id ? findGroup(document, path.id) : null;
5612
+ case "event":
5613
+ return path.id ? findEvent(document, path.id) : null;
5614
+ case "event-pose":
5615
+ return path.id && path.key ? findEventPose(document, path.id, path.key) : null;
3673
5616
  case "object":
3674
5617
  return path.id ? findObject(document, path.id) : null;
3675
5618
  case "viewpoint":
3676
5619
  return path.id ? findViewpoint(document.system, path.id) : null;
3677
5620
  case "annotation":
3678
5621
  return path.id ? findAnnotation(document.system, path.id) : null;
5622
+ case "relation":
5623
+ return path.id ? findRelation(document, path.id) : null;
3679
5624
  }
3680
5625
  }
3681
5626
  function upsertAtlasDocumentNode(document, path, value) {
@@ -3701,6 +5646,24 @@
3701
5646
  system.atlasMetadata[path.key] = String(value);
3702
5647
  }
3703
5648
  return next;
5649
+ case "group":
5650
+ if (!path.id) {
5651
+ throw new Error('Group updates require an "id" value.');
5652
+ }
5653
+ upsertById(next.groups, value);
5654
+ return next;
5655
+ case "event":
5656
+ if (!path.id) {
5657
+ throw new Error('Event updates require an "id" value.');
5658
+ }
5659
+ upsertById(next.events, value);
5660
+ return next;
5661
+ case "event-pose":
5662
+ if (!path.id || !path.key) {
5663
+ throw new Error('Event pose updates require an event "id" and pose "key" value.');
5664
+ }
5665
+ upsertEventPose(next.events, path.id, value);
5666
+ return next;
3704
5667
  case "object":
3705
5668
  if (!path.id) {
3706
5669
  throw new Error('Object updates require an "id" value.');
@@ -3719,6 +5682,12 @@
3719
5682
  }
3720
5683
  upsertById(system.annotations, value);
3721
5684
  return next;
5685
+ case "relation":
5686
+ if (!path.id) {
5687
+ throw new Error('Relation updates require an "id" value.');
5688
+ }
5689
+ upsertById(next.relations, value);
5690
+ return next;
3722
5691
  }
3723
5692
  }
3724
5693
  function updateAtlasDocumentNode(document, path, updater) {
@@ -3738,6 +5707,24 @@
3738
5707
  next.objects = next.objects.filter((object) => object.id !== path.id);
3739
5708
  }
3740
5709
  return next;
5710
+ case "group":
5711
+ if (path.id) {
5712
+ next.groups = next.groups.filter((group) => group.id !== path.id);
5713
+ }
5714
+ return next;
5715
+ case "event":
5716
+ if (path.id) {
5717
+ next.events = next.events.filter((event) => event.id !== path.id);
5718
+ }
5719
+ return next;
5720
+ case "event-pose":
5721
+ if (path.id && path.key) {
5722
+ const event = findEvent(next, path.id);
5723
+ if (event) {
5724
+ event.positions = event.positions.filter((pose) => pose.objectId !== path.key);
5725
+ }
5726
+ }
5727
+ return next;
3741
5728
  case "viewpoint":
3742
5729
  if (path.id) {
3743
5730
  system.viewpoints = system.viewpoints.filter((viewpoint) => viewpoint.id !== path.id);
@@ -3748,6 +5735,11 @@
3748
5735
  system.annotations = system.annotations.filter((annotation) => annotation.id !== path.id);
3749
5736
  }
3750
5737
  return next;
5738
+ case "relation":
5739
+ if (path.id) {
5740
+ next.relations = next.relations.filter((relation) => relation.id !== path.id);
5741
+ }
5742
+ return next;
3751
5743
  default:
3752
5744
  return next;
3753
5745
  }
@@ -3765,6 +5757,15 @@
3765
5757
  id: diagnostic.objectId
3766
5758
  };
3767
5759
  }
5760
+ if (diagnostic.field?.startsWith("group.")) {
5761
+ const parts = diagnostic.field.split(".");
5762
+ if (parts[1] && findGroup(document, parts[1])) {
5763
+ return {
5764
+ kind: "group",
5765
+ id: parts[1]
5766
+ };
5767
+ }
5768
+ }
3768
5769
  if (diagnostic.field?.startsWith("viewpoint.")) {
3769
5770
  const parts = diagnostic.field.split(".");
3770
5771
  if (parts[1] && findViewpoint(document.system, parts[1])) {
@@ -3783,6 +5784,31 @@
3783
5784
  };
3784
5785
  }
3785
5786
  }
5787
+ if (diagnostic.field?.startsWith("relation.")) {
5788
+ const parts = diagnostic.field.split(".");
5789
+ if (parts[1] && findRelation(document, parts[1])) {
5790
+ return {
5791
+ kind: "relation",
5792
+ id: parts[1]
5793
+ };
5794
+ }
5795
+ }
5796
+ if (diagnostic.field?.startsWith("event.")) {
5797
+ const parts = diagnostic.field.split(".");
5798
+ if (parts[1] && findEvent(document, parts[1])) {
5799
+ if (parts[2] === "pose" && parts[3] && findEventPose(document, parts[1], parts[3])) {
5800
+ return {
5801
+ kind: "event-pose",
5802
+ id: parts[1],
5803
+ key: parts[3]
5804
+ };
5805
+ }
5806
+ return {
5807
+ kind: "event",
5808
+ id: parts[1]
5809
+ };
5810
+ }
5811
+ }
3786
5812
  if (diagnostic.field && diagnostic.field in ensureSystem(document).atlasMetadata) {
3787
5813
  return {
3788
5814
  kind: "metadata",
@@ -3792,9 +5818,11 @@
3792
5818
  return null;
3793
5819
  }
3794
5820
  function validateAtlasDocumentWithDiagnostics(document) {
3795
- const materialized = materializeAtlasDocument(document);
3796
- const result = validateDocumentWithDiagnostics(materialized);
3797
- return resolveAtlasDiagnostics(document, result.diagnostics);
5821
+ const diagnostics = [
5822
+ ...document.diagnostics,
5823
+ ...collectAtlasDiagnostics(document, document.version)
5824
+ ];
5825
+ return resolveAtlasDiagnostics(document, diagnostics);
3798
5826
  }
3799
5827
  function ensureSystem(document) {
3800
5828
  if (document.system) {
@@ -3806,6 +5834,18 @@
3806
5834
  function findObject(document, objectId) {
3807
5835
  return document.objects.find((object) => object.id === objectId) ?? null;
3808
5836
  }
5837
+ function findGroup(document, groupId) {
5838
+ return document.groups.find((group) => group.id === groupId) ?? null;
5839
+ }
5840
+ function findRelation(document, relationId) {
5841
+ return document.relations.find((relation) => relation.id === relationId) ?? null;
5842
+ }
5843
+ function findEvent(document, eventId) {
5844
+ return document.events.find((event) => event.id === eventId) ?? null;
5845
+ }
5846
+ function findEventPose(document, eventId, objectId) {
5847
+ return findEvent(document, eventId)?.positions.find((pose) => pose.objectId === objectId) ?? null;
5848
+ }
3809
5849
  function findViewpoint(system, viewpointId) {
3810
5850
  return system?.viewpoints.find((viewpoint) => viewpoint.id === viewpointId) ?? null;
3811
5851
  }
@@ -3816,20 +5856,37 @@
3816
5856
  const index = items.findIndex((item) => item.id === value.id);
3817
5857
  if (index === -1) {
3818
5858
  items.push(value);
3819
- items.sort(compareIdLike);
5859
+ items.sort(compareIdLike2);
3820
5860
  return;
3821
5861
  }
3822
5862
  items[index] = value;
3823
5863
  }
3824
- function compareIdLike(left, right) {
5864
+ function upsertEventPose(events, eventId, value) {
5865
+ const event = events.find((entry) => entry.id === eventId);
5866
+ if (!event) {
5867
+ throw new Error(`Unknown event "${eventId}" for pose update.`);
5868
+ }
5869
+ const index = event.positions.findIndex((entry) => entry.objectId === value.objectId);
5870
+ if (index === -1) {
5871
+ event.positions.push(value);
5872
+ event.positions.sort(comparePoseObjectId2);
5873
+ return;
5874
+ }
5875
+ event.positions[index] = value;
5876
+ }
5877
+ function compareIdLike2(left, right) {
3825
5878
  return left.id.localeCompare(right.id);
3826
5879
  }
5880
+ function comparePoseObjectId2(left, right) {
5881
+ return left.objectId.localeCompare(right.objectId);
5882
+ }
3827
5883
 
3828
5884
  // packages/core/dist/load.js
3829
- var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0)?$/i;
5885
+ var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1)?$/i;
5886
+ var ATLAS_SCHEMA_21_PATTERN = /^schema\s+2\.1$/i;
3830
5887
  var LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
3831
5888
  function detectWorldOrbitSchemaVersion(source) {
3832
- for (const line of source.split(/\r?\n/)) {
5889
+ for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
3833
5890
  const trimmed = line.trim();
3834
5891
  if (!trimmed) {
3835
5892
  continue;
@@ -3837,6 +5894,9 @@
3837
5894
  if (LEGACY_DRAFT_SCHEMA_PATTERN.test(trimmed)) {
3838
5895
  return "2.0-draft";
3839
5896
  }
5897
+ if (ATLAS_SCHEMA_21_PATTERN.test(trimmed)) {
5898
+ return "2.1";
5899
+ }
3840
5900
  if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
3841
5901
  return "2.0";
3842
5902
  }
@@ -3844,6 +5904,49 @@
3844
5904
  }
3845
5905
  return "1.0";
3846
5906
  }
5907
+ function stripCommentsForSchemaDetection(source) {
5908
+ const chars = [...source];
5909
+ let inString = false;
5910
+ let inBlockComment = false;
5911
+ for (let index = 0; index < chars.length; index++) {
5912
+ const ch = chars[index];
5913
+ const next = chars[index + 1];
5914
+ if (inBlockComment) {
5915
+ if (ch === "*" && next === "/") {
5916
+ chars[index] = " ";
5917
+ chars[index + 1] = " ";
5918
+ inBlockComment = false;
5919
+ index++;
5920
+ continue;
5921
+ }
5922
+ if (ch !== "\n" && ch !== "\r") {
5923
+ chars[index] = " ";
5924
+ }
5925
+ continue;
5926
+ }
5927
+ if (!inString && ch === "/" && next === "*") {
5928
+ chars[index] = " ";
5929
+ chars[index + 1] = " ";
5930
+ inBlockComment = true;
5931
+ index++;
5932
+ continue;
5933
+ }
5934
+ if (!inString && ch === "#") {
5935
+ chars[index] = " ";
5936
+ let inner = index + 1;
5937
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
5938
+ chars[inner] = " ";
5939
+ inner++;
5940
+ }
5941
+ index = inner - 1;
5942
+ continue;
5943
+ }
5944
+ if (ch === '"' && chars[index - 1] !== "\\") {
5945
+ inString = !inString;
5946
+ }
5947
+ }
5948
+ return chars.join("");
5949
+ }
3847
5950
  function loadWorldOrbitSource(source) {
3848
5951
  const result = loadWorldOrbitSourceWithDiagnostics(source);
3849
5952
  if (!result.ok || !result.value) {
@@ -3854,36 +5957,36 @@
3854
5957
  }
3855
5958
  function loadWorldOrbitSourceWithDiagnostics(source) {
3856
5959
  const schemaVersion = detectWorldOrbitSchemaVersion(source);
3857
- if (schemaVersion === "2.0" || schemaVersion === "2.0-draft") {
5960
+ if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1") {
3858
5961
  return loadAtlasSourceWithDiagnostics(source, schemaVersion);
3859
5962
  }
3860
5963
  let ast;
3861
5964
  try {
3862
5965
  ast = parseWorldOrbit(source);
3863
- } catch (error) {
5966
+ } catch (error2) {
3864
5967
  return {
3865
5968
  ok: false,
3866
5969
  value: null,
3867
- diagnostics: [diagnosticFromError(error, "parse")]
5970
+ diagnostics: [diagnosticFromError(error2, "parse")]
3868
5971
  };
3869
5972
  }
3870
5973
  let document;
3871
5974
  try {
3872
5975
  document = normalizeDocument(ast);
3873
- } catch (error) {
5976
+ } catch (error2) {
3874
5977
  return {
3875
5978
  ok: false,
3876
5979
  value: null,
3877
- diagnostics: [diagnosticFromError(error, "normalize")]
5980
+ diagnostics: [diagnosticFromError(error2, "normalize")]
3878
5981
  };
3879
5982
  }
3880
5983
  try {
3881
5984
  validateDocument(document);
3882
- } catch (error) {
5985
+ } catch (error2) {
3883
5986
  return {
3884
5987
  ok: false,
3885
5988
  value: null,
3886
- diagnostics: [diagnosticFromError(error, "validate")]
5989
+ diagnostics: [diagnosticFromError(error2, "validate")]
3887
5990
  };
3888
5991
  }
3889
5992
  return {
@@ -3903,30 +6006,29 @@
3903
6006
  let atlasDocument;
3904
6007
  try {
3905
6008
  atlasDocument = parseWorldOrbitAtlas(source);
3906
- } catch (error) {
6009
+ } catch (error2) {
3907
6010
  return {
3908
6011
  ok: false,
3909
6012
  value: null,
3910
- diagnostics: [diagnosticFromError(error, "parse", "load.atlas.failed")]
6013
+ diagnostics: [diagnosticFromError(error2, "parse", "load.atlas.failed")]
3911
6014
  };
3912
6015
  }
3913
- let document;
3914
- try {
3915
- document = materializeAtlasDocument(atlasDocument);
3916
- } catch (error) {
6016
+ const atlasDiagnostics = [...atlasDocument.diagnostics];
6017
+ if (atlasDiagnostics.some((diagnostic) => diagnostic.severity === "error")) {
3917
6018
  return {
3918
6019
  ok: false,
3919
6020
  value: null,
3920
- diagnostics: [diagnosticFromError(error, "normalize", "load.atlas.materialize.failed")]
6021
+ diagnostics: atlasDiagnostics
3921
6022
  };
3922
6023
  }
6024
+ let document;
3923
6025
  try {
3924
- validateDocument(document);
3925
- } catch (error) {
6026
+ document = materializeAtlasDocument(atlasDocument);
6027
+ } catch (error2) {
3926
6028
  return {
3927
6029
  ok: false,
3928
6030
  value: null,
3929
- diagnostics: [diagnosticFromError(error, "validate", "load.atlas.validate.failed")]
6031
+ diagnostics: [diagnosticFromError(error2, "normalize", "load.atlas.materialize.failed")]
3930
6032
  };
3931
6033
  }
3932
6034
  const loaded = {
@@ -3935,12 +6037,12 @@
3935
6037
  document,
3936
6038
  atlasDocument,
3937
6039
  draftDocument: atlasDocument,
3938
- diagnostics: [...atlasDocument.diagnostics]
6040
+ diagnostics: atlasDiagnostics
3939
6041
  };
3940
6042
  return {
3941
6043
  ok: true,
3942
6044
  value: loaded,
3943
- diagnostics: [...atlasDocument.diagnostics]
6045
+ diagnostics: atlasDiagnostics
3944
6046
  };
3945
6047
  }
3946
6048