worldorbit 2.5.15 → 2.5.16

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.
@@ -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,10 @@
540
540
  return {
541
541
  format: "worldorbit",
542
542
  version: "1.0",
543
+ schemaVersion: "1.0",
543
544
  system,
545
+ groups: [],
546
+ relations: [],
544
547
  objects
545
548
  };
546
549
  }
@@ -550,13 +553,17 @@
550
553
  const fieldMap = collectFields(mergedFields);
551
554
  const placement = extractPlacement(node.objectType, fieldMap);
552
555
  const properties = normalizeProperties(fieldMap);
553
- const info = normalizeInfo(node.infoEntries);
556
+ const info2 = normalizeInfo(node.infoEntries);
554
557
  if (node.objectType === "system") {
555
558
  return {
556
559
  type: "system",
557
560
  id: node.name,
561
+ title: typeof properties.title === "string" ? properties.title : null,
562
+ description: null,
563
+ epoch: null,
564
+ referencePlane: null,
558
565
  properties,
559
- info
566
+ info: info2
560
567
  };
561
568
  }
562
569
  return {
@@ -564,7 +571,7 @@
564
571
  id: node.name,
565
572
  properties,
566
573
  placement,
567
- info
574
+ info: info2
568
575
  };
569
576
  }
570
577
  function validateFieldCompatibility(objectType, fields) {
@@ -694,14 +701,14 @@
694
701
  }
695
702
  }
696
703
  function normalizeInfo(entries) {
697
- const info = {};
704
+ const info2 = {};
698
705
  for (const entry of entries) {
699
- if (entry.key in info) {
706
+ if (entry.key in info2) {
700
707
  throw WorldOrbitError.fromLocation(`Duplicate info key "${entry.key}"`, entry.location);
701
708
  }
702
- info[entry.key] = entry.value;
709
+ info2[entry.key] = entry.value;
703
710
  }
704
- return info;
711
+ return info2;
705
712
  }
706
713
  function parseAtReference(target, location) {
707
714
  if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
@@ -875,38 +882,38 @@
875
882
  function createDiagnostic(diagnostic) {
876
883
  return { ...diagnostic };
877
884
  }
878
- function diagnosticFromError(error, source, code = `${source}.failed`) {
879
- if (error instanceof WorldOrbitError) {
885
+ function diagnosticFromError(error2, source, code = `${source}.failed`) {
886
+ if (error2 instanceof WorldOrbitError) {
880
887
  return {
881
888
  code,
882
889
  severity: "error",
883
890
  source,
884
- message: error.message,
885
- line: error.line,
886
- column: error.column
891
+ message: error2.message,
892
+ line: error2.line,
893
+ column: error2.column
887
894
  };
888
895
  }
889
- if (error instanceof Error) {
896
+ if (error2 instanceof Error) {
890
897
  return {
891
898
  code,
892
899
  severity: "error",
893
900
  source,
894
- message: error.message
901
+ message: error2.message
895
902
  };
896
903
  }
897
904
  return {
898
905
  code,
899
906
  severity: "error",
900
907
  source,
901
- message: String(error)
908
+ message: String(error2)
902
909
  };
903
910
  }
904
911
  function parseWithDiagnostics(source) {
905
912
  let ast;
906
913
  try {
907
914
  ast = parseWorldOrbit(source);
908
- } catch (error) {
909
- const diagnostic = diagnosticFromError(error, "parse");
915
+ } catch (error2) {
916
+ const diagnostic = diagnosticFromError(error2, "parse");
910
917
  return {
911
918
  ok: false,
912
919
  value: null,
@@ -916,20 +923,20 @@
916
923
  let document;
917
924
  try {
918
925
  document = normalizeDocument(ast);
919
- } catch (error) {
926
+ } catch (error2) {
920
927
  return {
921
928
  ok: false,
922
929
  value: null,
923
- diagnostics: [diagnosticFromError(error, "normalize")]
930
+ diagnostics: [diagnosticFromError(error2, "normalize")]
924
931
  };
925
932
  }
926
933
  try {
927
934
  validateDocument(document);
928
- } catch (error) {
935
+ } catch (error2) {
929
936
  return {
930
937
  ok: false,
931
938
  value: null,
932
- diagnostics: [diagnosticFromError(error, "validate")]
939
+ diagnostics: [diagnosticFromError(error2, "validate")]
933
940
  };
934
941
  }
935
942
  return {
@@ -948,11 +955,11 @@
948
955
  value: normalizeDocument(ast),
949
956
  diagnostics: []
950
957
  };
951
- } catch (error) {
958
+ } catch (error2) {
952
959
  return {
953
960
  ok: false,
954
961
  value: null,
955
- diagnostics: [diagnosticFromError(error, "normalize")]
962
+ diagnostics: [diagnosticFromError(error2, "normalize")]
956
963
  };
957
964
  }
958
965
  }
@@ -964,11 +971,11 @@
964
971
  value: document,
965
972
  diagnostics: []
966
973
  };
967
- } catch (error) {
974
+ } catch (error2) {
968
975
  return {
969
976
  ok: false,
970
977
  value: null,
971
- diagnostics: [diagnosticFromError(error, "validate")]
978
+ diagnostics: [diagnosticFromError(error2, "validate")]
972
979
  };
973
980
  }
974
981
  }
@@ -976,7 +983,11 @@
976
983
  // packages/core/dist/scene.js
977
984
  var AU_IN_KM = 1495978707e-1;
978
985
  var EARTH_RADIUS_IN_KM = 6371;
986
+ var JUPITER_RADIUS_IN_KM = 71492;
979
987
  var SOLAR_RADIUS_IN_KM = 695700;
988
+ var LY_IN_AU = 63241.077;
989
+ var PC_IN_AU = 206264.806;
990
+ var KPC_IN_AU = 206264806;
980
991
  var ISO_FLATTENING = 0.68;
981
992
  var MIN_ISO_MINOR_SCALE = 0.2;
982
993
  var ARC_SAMPLE_COUNT = 28;
@@ -1096,8 +1107,10 @@
1096
1107
  const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
1097
1108
  const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
1098
1109
  const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
1099
- const layers = createSceneLayers(orbitVisuals, leaders, objects, labels);
1110
+ const relations = createSceneRelations(document, objects);
1111
+ const layers = createSceneLayers(orbitVisuals, relations, leaders, objects, labels);
1100
1112
  const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
1113
+ const semanticGroups = createSceneSemanticGroups(document, objects);
1101
1114
  const viewpoints = createSceneViewpoints(document, projection, frame.preset, relationships, objectMap);
1102
1115
  const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
1103
1116
  return {
@@ -1107,7 +1120,7 @@
1107
1120
  renderPreset: frame.preset,
1108
1121
  projection,
1109
1122
  scaleModel,
1110
- title: String(document.system?.properties.title ?? document.system?.id ?? "WorldOrbit") || "WorldOrbit",
1123
+ title: String(document.system?.title ?? document.system?.properties.title ?? document.system?.id ?? "WorldOrbit") || "WorldOrbit",
1111
1124
  subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
1112
1125
  systemId,
1113
1126
  viewMode: projection,
@@ -1123,9 +1136,11 @@
1123
1136
  contentBounds,
1124
1137
  layers,
1125
1138
  groups,
1139
+ semanticGroups,
1126
1140
  viewpoints,
1127
1141
  objects,
1128
1142
  orbitVisuals,
1143
+ relations,
1129
1144
  leaders,
1130
1145
  labels
1131
1146
  };
@@ -1235,6 +1250,7 @@
1235
1250
  }
1236
1251
  function createSceneObject(position, scaleModel, relationships) {
1237
1252
  const { object, x, y, radius, sortKey, anchorX, anchorY } = position;
1253
+ const renderPriority = object.renderHints?.renderPriority ?? 0;
1238
1254
  return {
1239
1255
  renderId: createRenderId(object.id),
1240
1256
  objectId: object.id,
@@ -1243,11 +1259,12 @@
1243
1259
  ancestorIds: relationships.ancestorIds.get(object.id) ?? [],
1244
1260
  childIds: relationships.childIds.get(object.id) ?? [],
1245
1261
  groupId: relationships.groupIds.get(object.id) ?? null,
1262
+ semanticGroupIds: [...object.groups ?? []],
1246
1263
  x,
1247
1264
  y,
1248
1265
  radius,
1249
1266
  visualRadius: visualExtentForObject(object, radius, scaleModel),
1250
- sortKey,
1267
+ sortKey: sortKey + renderPriority * 1e-3,
1251
1268
  anchorX,
1252
1269
  anchorY,
1253
1270
  label: object.id,
@@ -1264,6 +1281,7 @@
1264
1281
  object: draft.object,
1265
1282
  parentId: draft.parentId,
1266
1283
  groupId,
1284
+ semanticGroupIds: [...draft.object.groups ?? []],
1267
1285
  kind: draft.kind,
1268
1286
  cx: draft.cx,
1269
1287
  cy: draft.cy,
@@ -1275,7 +1293,7 @@
1275
1293
  bandThickness: draft.bandThickness,
1276
1294
  frontArcPath: draft.frontArcPath,
1277
1295
  backArcPath: draft.backArcPath,
1278
- hidden: draft.object.properties.hidden === true
1296
+ hidden: draft.object.properties.hidden === true || draft.object.renderHints?.renderOrbit === false
1279
1297
  };
1280
1298
  }
1281
1299
  function createLeaderLine(draft) {
@@ -1284,6 +1302,7 @@
1284
1302
  objectId: draft.object.id,
1285
1303
  object: draft.object,
1286
1304
  groupId: draft.groupId,
1305
+ semanticGroupIds: [...draft.object.groups ?? []],
1287
1306
  x1: draft.x1,
1288
1307
  y1: draft.y1,
1289
1308
  x2: draft.x2,
@@ -1295,7 +1314,7 @@
1295
1314
  function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1296
1315
  const labels = [];
1297
1316
  const occupied = [];
1298
- const visibleObjects = [...objects].filter((object) => !object.hidden).sort((left, right) => left.sortKey - right.sortKey);
1317
+ const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort((left, right) => left.sortKey - right.sortKey);
1299
1318
  for (const object of visibleObjects) {
1300
1319
  const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1301
1320
  const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
@@ -1315,6 +1334,7 @@
1315
1334
  objectId: object.objectId,
1316
1335
  object: object.object,
1317
1336
  groupId: object.groupId,
1337
+ semanticGroupIds: [...object.semanticGroupIds],
1318
1338
  label: object.label,
1319
1339
  secondaryLabel: object.secondaryLabel,
1320
1340
  x: object.x,
@@ -1327,7 +1347,7 @@
1327
1347
  }
1328
1348
  return labels;
1329
1349
  }
1330
- function createSceneLayers(orbitVisuals, leaders, objects, labels) {
1350
+ function createSceneLayers(orbitVisuals, relations, leaders, objects, labels) {
1331
1351
  const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1332
1352
  const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1333
1353
  return [
@@ -1338,6 +1358,10 @@
1338
1358
  },
1339
1359
  { id: "orbits-back", renderIds: backOrbitIds },
1340
1360
  { id: "orbits-front", renderIds: frontOrbitIds },
1361
+ {
1362
+ id: "relations",
1363
+ renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId)
1364
+ },
1341
1365
  {
1342
1366
  id: "objects",
1343
1367
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
@@ -1402,6 +1426,36 @@
1402
1426
  }
1403
1427
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1404
1428
  }
1429
+ function createSceneSemanticGroups(document, objects) {
1430
+ return [...document.groups].map((group) => ({
1431
+ id: group.id,
1432
+ label: group.label,
1433
+ summary: group.summary,
1434
+ color: group.color,
1435
+ tags: [...group.tags],
1436
+ hidden: group.hidden,
1437
+ objectIds: objects.filter((object) => !object.hidden && object.semanticGroupIds.includes(group.id)).map((object) => object.objectId)
1438
+ })).sort((left, right) => left.label.localeCompare(right.label));
1439
+ }
1440
+ function createSceneRelations(document, objects) {
1441
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1442
+ return document.relations.map((relation) => {
1443
+ const from = objectMap.get(relation.from);
1444
+ const to = objectMap.get(relation.to);
1445
+ return {
1446
+ renderId: `${createRenderId(relation.id)}-relation`,
1447
+ relationId: relation.id,
1448
+ relation,
1449
+ fromObjectId: relation.from,
1450
+ toObjectId: relation.to,
1451
+ x1: from?.x ?? 0,
1452
+ y1: from?.y ?? 0,
1453
+ x2: to?.x ?? 0,
1454
+ y2: to?.y ?? 0,
1455
+ hidden: relation.hidden || !from || !to || from.hidden || to.hidden
1456
+ };
1457
+ }).sort((left, right) => left.relation.id.localeCompare(right.relation.id));
1458
+ }
1405
1459
  function createSceneViewpoints(document, projection, preset, relationships, objectMap) {
1406
1460
  const generatedOverview = createGeneratedOverviewViewpoint(document, projection, preset);
1407
1461
  const drafts = /* @__PURE__ */ new Map();
@@ -1419,7 +1473,7 @@
1419
1473
  }
1420
1474
  const field = fieldParts.join(".").toLowerCase();
1421
1475
  const draft = drafts.get(id) ?? { id };
1422
- applyViewpointField(draft, field, value, projection, preset, relationships, objectMap);
1476
+ applyViewpointField(draft, field, value, document, projection, preset, relationships, objectMap);
1423
1477
  drafts.set(id, draft);
1424
1478
  }
1425
1479
  const viewpoints = [...drafts.values()].map((draft) => finalizeViewpointDraft(draft, projection, preset, objectMap)).filter(Boolean);
@@ -1447,7 +1501,8 @@
1447
1501
  });
1448
1502
  }
1449
1503
  function createGeneratedOverviewViewpoint(document, projection, preset) {
1450
- const label = document.system?.properties.title ? `${String(document.system.properties.title)} Overview` : "Overview";
1504
+ const title = document.system?.title ?? document.system?.properties.title;
1505
+ const label = title ? `${String(title)} Overview` : "Overview";
1451
1506
  return {
1452
1507
  id: "overview",
1453
1508
  label,
@@ -1463,7 +1518,7 @@
1463
1518
  generated: true
1464
1519
  };
1465
1520
  }
1466
- function applyViewpointField(draft, field, value, projection, preset, relationships, objectMap) {
1521
+ function applyViewpointField(draft, field, value, document, projection, preset, relationships, objectMap) {
1467
1522
  const normalizedValue = value.trim();
1468
1523
  switch (field) {
1469
1524
  case "label":
@@ -1530,7 +1585,7 @@
1530
1585
  case "groups":
1531
1586
  draft.filter = {
1532
1587
  ...draft.filter ?? createEmptyViewpointFilter(),
1533
- groupIds: parseViewpointGroups(normalizedValue, relationships, objectMap)
1588
+ groupIds: parseViewpointGroups(normalizedValue, document, relationships, objectMap)
1534
1589
  };
1535
1590
  return;
1536
1591
  }
@@ -1603,7 +1658,7 @@
1603
1658
  next["orbits-front"] = enabled;
1604
1659
  continue;
1605
1660
  }
1606
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1661
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1607
1662
  next[rawLayer] = enabled;
1608
1663
  }
1609
1664
  }
@@ -1612,8 +1667,11 @@
1612
1667
  function parseViewpointObjectTypes(value) {
1613
1668
  return splitListValue(value).filter((entry) => entry === "star" || entry === "planet" || entry === "moon" || entry === "belt" || entry === "asteroid" || entry === "comet" || entry === "ring" || entry === "structure" || entry === "phenomenon");
1614
1669
  }
1615
- function parseViewpointGroups(value, relationships, objectMap) {
1670
+ function parseViewpointGroups(value, document, relationships, objectMap) {
1616
1671
  return splitListValue(value).map((entry) => {
1672
+ if (document.schemaVersion === "2.1" || document.groups.some((group) => group.id === entry)) {
1673
+ return entry;
1674
+ }
1617
1675
  if (entry.startsWith("wo-") && entry.endsWith("-group")) {
1618
1676
  return entry;
1619
1677
  }
@@ -1744,8 +1802,9 @@
1744
1802
  }
1745
1803
  const orbiting = [...context.orbitChildren.get(object.id) ?? []].sort(compareOrbiting);
1746
1804
  const orbitMetricContext = computeOrbitMetricContext(orbiting, parent.radius, context.spacingFactor, context.scaleModel);
1805
+ const orbitRadiiPx = resolveOrbitRadiiPx(orbiting, orbitMetricContext);
1747
1806
  orbiting.forEach((child, index) => {
1748
- const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, context);
1807
+ const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, orbitRadiiPx[index] ?? orbitMetricContext.innerPx, context);
1749
1808
  orbitDrafts.push({
1750
1809
  object: child,
1751
1810
  parentId: object.id,
@@ -1819,7 +1878,8 @@
1819
1878
  metricSpread: 0,
1820
1879
  innerPx,
1821
1880
  stepPx,
1822
- pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx)
1881
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
1882
+ minimumGapPx: stepPx * 0.42
1823
1883
  };
1824
1884
  }
1825
1885
  const minMetric = Math.min(...presentMetrics);
@@ -1832,10 +1892,11 @@
1832
1892
  metricSpread,
1833
1893
  innerPx,
1834
1894
  stepPx,
1835
- pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx)
1895
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
1896
+ minimumGapPx: stepPx * 0.42
1836
1897
  };
1837
1898
  }
1838
- function resolveOrbitGeometry(object, index, count, parent, metricContext, context) {
1899
+ function resolveOrbitGeometry(object, index, count, parent, metricContext, orbitRadiusPx, context) {
1839
1900
  const placement = object.placement;
1840
1901
  const band = object.type === "belt" || object.type === "ring";
1841
1902
  if (!placement || placement.mode !== "orbit") {
@@ -1853,7 +1914,7 @@
1853
1914
  };
1854
1915
  }
1855
1916
  const eccentricity = clampNumber(typeof placement.eccentricity === "number" ? placement.eccentricity : 0, 0, 0.92);
1856
- const semiMajor = resolveOrbitRadiusPx(object, index, metricContext);
1917
+ const semiMajor = orbitRadiusPx;
1857
1918
  const baseMinor = Math.max(semiMajor * Math.sqrt(1 - eccentricity * eccentricity), semiMajor * 0.18);
1858
1919
  const inclinationDeg = unitValueToDegrees(placement.inclination) ?? 0;
1859
1920
  const inclinationScale = context.projection === "isometric" ? Math.max(MIN_ISO_MINOR_SCALE, Math.cos(degreesToRadians(inclinationDeg))) * ISO_FLATTENING : 1;
@@ -1883,15 +1944,19 @@
1883
1944
  objectY: objectPoint.y
1884
1945
  };
1885
1946
  }
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;
1947
+ function resolveOrbitRadiusPx(metric, metricContext) {
1948
+ return metricContext.innerPx + metricContext.stepPx * log2(Math.max(metric, 0) + 1);
1949
+ }
1950
+ function resolveOrbitRadiiPx(objects, metricContext) {
1951
+ const radii = [];
1952
+ objects.forEach((object, index) => {
1953
+ const metric = orbitMetric(object);
1954
+ const fallbackRadius = metricContext.innerPx + index * metricContext.stepPx;
1955
+ const baseRadius = metric === null ? fallbackRadius : resolveOrbitRadiusPx(metric, metricContext);
1956
+ const minimumRadius = index === 0 ? metricContext.innerPx : (radii[index - 1] ?? metricContext.innerPx) + metricContext.minimumGapPx;
1957
+ radii.push(Math.max(baseRadius, minimumRadius));
1958
+ });
1959
+ return radii;
1895
1960
  }
1896
1961
  function orbitMetric(object) {
1897
1962
  if (!object.placement || object.placement.mode !== "orbit") {
@@ -1899,6 +1964,9 @@
1899
1964
  }
1900
1965
  return toDistanceMetric(object.placement.semiMajor ?? object.placement.distance ?? null);
1901
1966
  }
1967
+ function log2(value) {
1968
+ return Math.log(value) / Math.log(2);
1969
+ }
1902
1970
  function resolveOrbitPhase(phase, index, count) {
1903
1971
  const degreeValue = phase ? unitValueToDegrees(phase) : null;
1904
1972
  if (degreeValue !== null) {
@@ -2222,8 +2290,18 @@
2222
2290
  return value.value;
2223
2291
  case "km":
2224
2292
  return value.value / AU_IN_KM;
2293
+ case "m":
2294
+ return value.value / 1e3 / AU_IN_KM;
2295
+ case "ly":
2296
+ return value.value * LY_IN_AU;
2297
+ case "pc":
2298
+ return value.value * PC_IN_AU;
2299
+ case "kpc":
2300
+ return value.value * KPC_IN_AU;
2225
2301
  case "re":
2226
2302
  return value.value * EARTH_RADIUS_IN_KM / AU_IN_KM;
2303
+ case "rj":
2304
+ return value.value * JUPITER_RADIUS_IN_KM / AU_IN_KM;
2227
2305
  case "sol":
2228
2306
  return value.value * SOLAR_RADIUS_IN_KM / AU_IN_KM;
2229
2307
  default:
@@ -2377,8 +2455,11 @@
2377
2455
  return {
2378
2456
  format: "worldorbit",
2379
2457
  version: "2.0",
2458
+ schemaVersion: "2.0",
2380
2459
  sourceVersion: document.version,
2381
2460
  system,
2461
+ groups: structuredClone(document.groups ?? []),
2462
+ relations: structuredClone(document.relations ?? []),
2382
2463
  objects: document.objects.map(cloneWorldOrbitObject),
2383
2464
  diagnostics
2384
2465
  };
@@ -2390,13 +2471,20 @@
2390
2471
  const system = document.system ? {
2391
2472
  type: "system",
2392
2473
  id: document.system.id,
2474
+ title: document.system.title,
2475
+ description: document.system.description,
2476
+ epoch: document.system.epoch,
2477
+ referencePlane: document.system.referencePlane,
2393
2478
  properties: materializeDraftSystemProperties(document.system),
2394
2479
  info: materializeDraftSystemInfo(document.system)
2395
2480
  } : null;
2396
2481
  return {
2397
2482
  format: "worldorbit",
2398
2483
  version: "1.0",
2484
+ schemaVersion: document.version,
2399
2485
  system,
2486
+ groups: structuredClone(document.groups ?? []),
2487
+ relations: structuredClone(document.relations ?? []),
2400
2488
  objects: document.objects.map(cloneWorldOrbitObject)
2401
2489
  };
2402
2490
  }
@@ -2411,7 +2499,10 @@
2411
2499
  return {
2412
2500
  type: "system",
2413
2501
  id: document.system?.id ?? "WorldOrbit",
2414
- title: typeof document.system?.properties.title === "string" ? document.system.properties.title : null,
2502
+ title: document.system?.title ?? (typeof document.system?.properties.title === "string" ? document.system.properties.title : null),
2503
+ description: document.system?.description ?? null,
2504
+ epoch: document.system?.epoch ?? null,
2505
+ referencePlane: document.system?.referencePlane ?? null,
2415
2506
  defaults,
2416
2507
  atlasMetadata,
2417
2508
  viewpoints: scene.viewpoints.map(mapSceneViewpointToDraftViewpoint),
@@ -2538,6 +2629,17 @@
2538
2629
  function cloneWorldOrbitObject(object) {
2539
2630
  return {
2540
2631
  ...object,
2632
+ groups: object.groups ? [...object.groups] : void 0,
2633
+ resonance: object.resonance ? { ...object.resonance } : object.resonance,
2634
+ renderHints: object.renderHints ? { ...object.renderHints } : object.renderHints,
2635
+ deriveRules: object.deriveRules ? object.deriveRules.map((rule) => ({ ...rule })) : void 0,
2636
+ validationRules: object.validationRules ? object.validationRules.map((rule) => ({ ...rule })) : void 0,
2637
+ lockedFields: object.lockedFields ? [...object.lockedFields] : void 0,
2638
+ tolerances: object.tolerances ? object.tolerances.map((entry) => ({
2639
+ field: entry.field,
2640
+ 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
2641
+ })) : void 0,
2642
+ typedBlocks: object.typedBlocks ? Object.fromEntries(Object.entries(object.typedBlocks).map(([key, block]) => [key, { ...block ?? {} }])) : void 0,
2541
2643
  properties: cloneProperties(object.properties),
2542
2644
  placement: object.placement ? structuredClone(object.placement) : null,
2543
2645
  info: { ...object.info }
@@ -2582,71 +2684,80 @@
2582
2684
  if (system.defaults.units) {
2583
2685
  properties.units = system.defaults.units;
2584
2686
  }
2687
+ if (system.description) {
2688
+ properties.description = system.description;
2689
+ }
2690
+ if (system.epoch) {
2691
+ properties.epoch = system.epoch;
2692
+ }
2693
+ if (system.referencePlane) {
2694
+ properties.referencePlane = system.referencePlane;
2695
+ }
2585
2696
  return properties;
2586
2697
  }
2587
2698
  function materializeDraftSystemInfo(system) {
2588
- const info = {
2699
+ const info2 = {
2589
2700
  ...system.atlasMetadata
2590
2701
  };
2591
2702
  if (system.defaults.theme) {
2592
- info["atlas.theme"] = system.defaults.theme;
2703
+ info2["atlas.theme"] = system.defaults.theme;
2593
2704
  }
2594
2705
  for (const viewpoint of system.viewpoints) {
2595
2706
  const prefix = `viewpoint.${viewpoint.id}`;
2596
- info[`${prefix}.label`] = viewpoint.label;
2707
+ info2[`${prefix}.label`] = viewpoint.label;
2597
2708
  if (viewpoint.summary) {
2598
- info[`${prefix}.summary`] = viewpoint.summary;
2709
+ info2[`${prefix}.summary`] = viewpoint.summary;
2599
2710
  }
2600
2711
  if (viewpoint.focusObjectId) {
2601
- info[`${prefix}.focus`] = viewpoint.focusObjectId;
2712
+ info2[`${prefix}.focus`] = viewpoint.focusObjectId;
2602
2713
  }
2603
2714
  if (viewpoint.selectedObjectId) {
2604
- info[`${prefix}.select`] = viewpoint.selectedObjectId;
2715
+ info2[`${prefix}.select`] = viewpoint.selectedObjectId;
2605
2716
  }
2606
2717
  if (viewpoint.projection) {
2607
- info[`${prefix}.projection`] = viewpoint.projection;
2718
+ info2[`${prefix}.projection`] = viewpoint.projection;
2608
2719
  }
2609
2720
  if (viewpoint.preset) {
2610
- info[`${prefix}.preset`] = viewpoint.preset;
2721
+ info2[`${prefix}.preset`] = viewpoint.preset;
2611
2722
  }
2612
2723
  if (viewpoint.zoom !== null) {
2613
- info[`${prefix}.zoom`] = String(viewpoint.zoom);
2724
+ info2[`${prefix}.zoom`] = String(viewpoint.zoom);
2614
2725
  }
2615
2726
  if (viewpoint.rotationDeg !== 0) {
2616
- info[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2727
+ info2[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2617
2728
  }
2618
2729
  const serializedLayers = serializeViewpointLayers(viewpoint.layers);
2619
2730
  if (serializedLayers) {
2620
- info[`${prefix}.layers`] = serializedLayers;
2731
+ info2[`${prefix}.layers`] = serializedLayers;
2621
2732
  }
2622
2733
  if (viewpoint.filter?.query) {
2623
- info[`${prefix}.query`] = viewpoint.filter.query;
2734
+ info2[`${prefix}.query`] = viewpoint.filter.query;
2624
2735
  }
2625
2736
  if ((viewpoint.filter?.objectTypes.length ?? 0) > 0) {
2626
- info[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
2737
+ info2[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
2627
2738
  }
2628
2739
  if ((viewpoint.filter?.tags.length ?? 0) > 0) {
2629
- info[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
2740
+ info2[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
2630
2741
  }
2631
2742
  if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2632
- info[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2743
+ info2[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2633
2744
  }
2634
2745
  }
2635
2746
  for (const annotation of system.annotations) {
2636
2747
  const prefix = `annotation.${annotation.id}`;
2637
- info[`${prefix}.label`] = annotation.label;
2748
+ info2[`${prefix}.label`] = annotation.label;
2638
2749
  if (annotation.targetObjectId) {
2639
- info[`${prefix}.target`] = annotation.targetObjectId;
2750
+ info2[`${prefix}.target`] = annotation.targetObjectId;
2640
2751
  }
2641
- info[`${prefix}.body`] = annotation.body;
2752
+ info2[`${prefix}.body`] = annotation.body;
2642
2753
  if (annotation.tags.length > 0) {
2643
- info[`${prefix}.tags`] = annotation.tags.join(" ");
2754
+ info2[`${prefix}.tags`] = annotation.tags.join(" ");
2644
2755
  }
2645
2756
  if (annotation.sourceObjectId) {
2646
- info[`${prefix}.source`] = annotation.sourceObjectId;
2757
+ info2[`${prefix}.source`] = annotation.sourceObjectId;
2647
2758
  }
2648
2759
  }
2649
- return info;
2760
+ return info2;
2650
2761
  }
2651
2762
  function serializeViewpointLayers(layers) {
2652
2763
  const tokens = [];
@@ -2655,7 +2766,7 @@
2655
2766
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2656
2767
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2657
2768
  }
2658
- for (const key of ["background", "guides", "objects", "labels", "metadata"]) {
2769
+ for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
2659
2770
  if (layers[key] !== void 0) {
2660
2771
  tokens.push(layers[key] ? key : `-${key}`);
2661
2772
  }
@@ -2665,7 +2776,8 @@
2665
2776
  function convertAtlasDocumentToLegacyDraft(document) {
2666
2777
  return {
2667
2778
  ...document,
2668
- version: "2.0-draft"
2779
+ version: "2.0-draft",
2780
+ schemaVersion: "2.0-draft"
2669
2781
  };
2670
2782
  }
2671
2783
 
@@ -2707,19 +2819,28 @@
2707
2819
  ];
2708
2820
  function formatDocument(document, options = {}) {
2709
2821
  const schema = options.schema ?? "auto";
2710
- const useDraft = schema === "2.0" || schema === "2.0-draft" || document.version === "2.0" || document.version === "2.0-draft";
2822
+ const useDraft = schema === "2.0" || schema === "2.1" || schema === "2.0-draft" || document.version === "2.0" || document.version === "2.1" || document.version === "2.0-draft";
2711
2823
  if (useDraft) {
2712
2824
  if (schema === "2.0-draft") {
2713
- const legacyDraftDocument = document.version === "2.0-draft" ? document : document.version === "2.0" ? {
2825
+ const legacyDraftDocument = document.version === "2.0-draft" ? document : document.version === "2.0" || document.version === "2.1" ? {
2714
2826
  ...document,
2715
- version: "2.0-draft"
2827
+ version: "2.0-draft",
2828
+ schemaVersion: "2.0-draft"
2716
2829
  } : upgradeDocumentToDraftV2(document);
2717
2830
  return formatDraftDocument(legacyDraftDocument);
2718
2831
  }
2719
- const atlasDocument = document.version === "2.0" ? document : document.version === "2.0-draft" ? {
2832
+ const atlasDocument = document.version === "2.0" || document.version === "2.1" ? document : document.version === "2.0-draft" ? {
2720
2833
  ...document,
2721
- version: "2.0"
2834
+ version: "2.0",
2835
+ schemaVersion: "2.0"
2722
2836
  } : upgradeDocumentToV2(document);
2837
+ if (schema === "2.1" && atlasDocument.version !== "2.1") {
2838
+ return formatAtlasDocument({
2839
+ ...atlasDocument,
2840
+ version: "2.1",
2841
+ schemaVersion: "2.1"
2842
+ });
2843
+ }
2723
2844
  return formatAtlasDocument(atlasDocument);
2724
2845
  }
2725
2846
  const lines = [];
@@ -2737,10 +2858,18 @@
2737
2858
  return lines.join("\n");
2738
2859
  }
2739
2860
  function formatAtlasDocument(document) {
2740
- const lines = ["schema 2.0", ""];
2861
+ const lines = [`schema ${document.version}`, ""];
2741
2862
  if (document.system) {
2742
2863
  lines.push(...formatAtlasSystem(document.system));
2743
2864
  }
2865
+ for (const group of [...document.groups].sort(compareIdLike)) {
2866
+ lines.push("");
2867
+ lines.push(...formatAtlasGroup(group));
2868
+ }
2869
+ for (const relation of [...document.relations].sort(compareIdLike)) {
2870
+ lines.push("");
2871
+ lines.push(...formatAtlasRelation(relation));
2872
+ }
2744
2873
  const sortedObjects = [...document.objects].sort(compareObjects);
2745
2874
  if (sortedObjects.length > 0 && lines.at(-1) !== "") {
2746
2875
  lines.push("");
@@ -2756,12 +2885,21 @@
2756
2885
  function formatDraftDocument(document) {
2757
2886
  const legacy = document.version === "2.0-draft" ? document : {
2758
2887
  ...document,
2759
- version: "2.0-draft"
2888
+ version: "2.0-draft",
2889
+ schemaVersion: "2.0-draft"
2760
2890
  };
2761
2891
  const lines = ["schema 2.0-draft", ""];
2762
2892
  if (legacy.system) {
2763
2893
  lines.push(...formatAtlasSystem(legacy.system));
2764
2894
  }
2895
+ for (const group of [...legacy.groups].sort(compareIdLike)) {
2896
+ lines.push("");
2897
+ lines.push(...formatAtlasGroup(group));
2898
+ }
2899
+ for (const relation of [...legacy.relations].sort(compareIdLike)) {
2900
+ lines.push("");
2901
+ lines.push(...formatAtlasRelation(relation));
2902
+ }
2765
2903
  const sortedObjects = [...legacy.objects].sort(compareObjects);
2766
2904
  if (sortedObjects.length > 0 && lines.at(-1) !== "") {
2767
2905
  lines.push("");
@@ -2777,11 +2915,38 @@
2777
2915
  function formatSystem(system) {
2778
2916
  return formatLines("system", system.id, system.properties, null, system.info);
2779
2917
  }
2918
+ function formatLines(objectType, id, properties, placement, info2) {
2919
+ const lines = [`${objectType} ${id}`];
2920
+ const fieldLines = [...formatPlacement(placement), ...formatProperties(properties)];
2921
+ for (const fieldLine of fieldLines) {
2922
+ lines.push(` ${fieldLine}`);
2923
+ }
2924
+ const infoEntries = Object.entries(info2).sort(([left], [right]) => left.localeCompare(right));
2925
+ if (infoEntries.length > 0) {
2926
+ if (fieldLines.length > 0) {
2927
+ lines.push("");
2928
+ }
2929
+ lines.push(" info");
2930
+ for (const [key, value] of infoEntries) {
2931
+ lines.push(` ${key} ${quoteIfNeeded(value)}`);
2932
+ }
2933
+ }
2934
+ return lines;
2935
+ }
2780
2936
  function formatAtlasSystem(system) {
2781
2937
  const lines = [`system ${system.id}`];
2782
2938
  if (system.title) {
2783
2939
  lines.push(` title ${quoteIfNeeded(system.title)}`);
2784
2940
  }
2941
+ if (system.description) {
2942
+ lines.push(` description ${quoteIfNeeded(system.description)}`);
2943
+ }
2944
+ if (system.epoch) {
2945
+ lines.push(` epoch ${quoteIfNeeded(system.epoch)}`);
2946
+ }
2947
+ if (system.referencePlane) {
2948
+ lines.push(` referencePlane ${quoteIfNeeded(system.referencePlane)}`);
2949
+ }
2785
2950
  lines.push("");
2786
2951
  lines.push("defaults");
2787
2952
  lines.push(` view ${system.defaults.view}`);
@@ -2816,18 +2981,22 @@
2816
2981
  return lines;
2817
2982
  }
2818
2983
  function formatObject(object) {
2819
- return formatLines(object.type, object.id, object.properties, object.placement, object.info);
2984
+ return formatWorldOrbitObject(object.type, object.id, object);
2820
2985
  }
2821
2986
  function formatAtlasObject(object) {
2822
- return formatLines(`object ${object.type}`, object.id, object.properties, object.placement, object.info);
2987
+ return formatWorldOrbitObject(`object ${object.type}`, object.id, object);
2823
2988
  }
2824
- function formatLines(objectType, id, properties, placement, info) {
2989
+ function formatWorldOrbitObject(objectType, id, object) {
2825
2990
  const lines = [`${objectType} ${id}`];
2826
- const fieldLines = [...formatPlacement(placement), ...formatProperties(properties)];
2991
+ const fieldLines = [
2992
+ ...formatPlacement(object.placement),
2993
+ ...formatProperties(object.properties),
2994
+ ...formatObjectMetadata(object)
2995
+ ];
2827
2996
  for (const fieldLine of fieldLines) {
2828
2997
  lines.push(` ${fieldLine}`);
2829
2998
  }
2830
- const infoEntries = Object.entries(info).sort(([left], [right]) => left.localeCompare(right));
2999
+ const infoEntries = Object.entries(object.info).sort(([left], [right]) => left.localeCompare(right));
2831
3000
  if (infoEntries.length > 0) {
2832
3001
  if (fieldLines.length > 0) {
2833
3002
  lines.push("");
@@ -2837,6 +3006,16 @@
2837
3006
  lines.push(` ${key} ${quoteIfNeeded(value)}`);
2838
3007
  }
2839
3008
  }
3009
+ for (const blockName of ["climate", "habitability", "settlement"]) {
3010
+ const blockEntries = Object.entries(object.typedBlocks?.[blockName] ?? {}).sort(([left], [right]) => left.localeCompare(right));
3011
+ if (blockEntries.length > 0) {
3012
+ lines.push("");
3013
+ lines.push(` ${blockName}`);
3014
+ for (const [key, value] of blockEntries) {
3015
+ lines.push(` ${key} ${quoteIfNeeded(value)}`);
3016
+ }
3017
+ }
3018
+ }
2840
3019
  return lines;
2841
3020
  }
2842
3021
  function formatPlacement(placement) {
@@ -2865,6 +3044,46 @@
2865
3044
  function formatProperties(properties) {
2866
3045
  return Object.keys(properties).sort(compareFieldKeys).map((key) => `${key} ${formatValue(properties[key])}`);
2867
3046
  }
3047
+ function formatObjectMetadata(object) {
3048
+ const lines = [];
3049
+ if (object.groups?.length) {
3050
+ lines.push(`groups ${object.groups.join(" ")}`);
3051
+ }
3052
+ if (object.epoch) {
3053
+ lines.push(`epoch ${quoteIfNeeded(object.epoch)}`);
3054
+ }
3055
+ if (object.referencePlane) {
3056
+ lines.push(`referencePlane ${quoteIfNeeded(object.referencePlane)}`);
3057
+ }
3058
+ if (object.tidalLock !== void 0) {
3059
+ lines.push(`tidalLock ${object.tidalLock ? "true" : "false"}`);
3060
+ }
3061
+ if (object.renderHints?.renderLabel !== void 0) {
3062
+ lines.push(`renderLabel ${object.renderHints.renderLabel ? "true" : "false"}`);
3063
+ }
3064
+ if (object.renderHints?.renderOrbit !== void 0) {
3065
+ lines.push(`renderOrbit ${object.renderHints.renderOrbit ? "true" : "false"}`);
3066
+ }
3067
+ if (object.renderHints?.renderPriority !== void 0) {
3068
+ lines.push(`renderPriority ${object.renderHints.renderPriority}`);
3069
+ }
3070
+ if (object.resonance) {
3071
+ lines.push(`resonance ${object.resonance.targetObjectId} ${object.resonance.ratio}`);
3072
+ }
3073
+ for (const rule of object.deriveRules ?? []) {
3074
+ lines.push(`derive ${rule.field} ${rule.strategy}`);
3075
+ }
3076
+ for (const rule of object.validationRules ?? []) {
3077
+ lines.push(`validate ${rule.rule}`);
3078
+ }
3079
+ if (object.lockedFields?.length) {
3080
+ lines.push(`locked ${object.lockedFields.join(" ")}`);
3081
+ }
3082
+ for (const tolerance of object.tolerances ?? []) {
3083
+ lines.push(`tolerance ${tolerance.field} ${formatValue(tolerance.value)}`);
3084
+ }
3085
+ return lines;
3086
+ }
2868
3087
  function formatAtlasViewpoint(viewpoint) {
2869
3088
  const lines = [`viewpoint ${viewpoint.id}`, ` label ${quoteIfNeeded(viewpoint.label)}`];
2870
3089
  if (viewpoint.focusObjectId) {
@@ -2920,6 +3139,50 @@
2920
3139
  }
2921
3140
  return lines;
2922
3141
  }
3142
+ function formatAtlasGroup(group) {
3143
+ const lines = [`group ${group.id}`, ` label ${quoteIfNeeded(group.label)}`];
3144
+ if (group.summary) {
3145
+ lines.push(` summary ${quoteIfNeeded(group.summary)}`);
3146
+ }
3147
+ if (group.color) {
3148
+ lines.push(` color ${quoteIfNeeded(group.color)}`);
3149
+ }
3150
+ if (group.tags.length > 0) {
3151
+ lines.push(` tags ${group.tags.map(quoteIfNeeded).join(" ")}`);
3152
+ }
3153
+ if (group.hidden) {
3154
+ lines.push(" hidden true");
3155
+ }
3156
+ return lines;
3157
+ }
3158
+ function formatAtlasRelation(relation) {
3159
+ const lines = [`relation ${relation.id}`];
3160
+ if (relation.from) {
3161
+ lines.push(` from ${quoteIfNeeded(relation.from)}`);
3162
+ }
3163
+ if (relation.to) {
3164
+ lines.push(` to ${quoteIfNeeded(relation.to)}`);
3165
+ }
3166
+ if (relation.kind) {
3167
+ lines.push(` kind ${quoteIfNeeded(relation.kind)}`);
3168
+ }
3169
+ if (relation.label) {
3170
+ lines.push(` label ${quoteIfNeeded(relation.label)}`);
3171
+ }
3172
+ if (relation.summary) {
3173
+ lines.push(` summary ${quoteIfNeeded(relation.summary)}`);
3174
+ }
3175
+ if (relation.tags.length > 0) {
3176
+ lines.push(` tags ${relation.tags.map(quoteIfNeeded).join(" ")}`);
3177
+ }
3178
+ if (relation.color) {
3179
+ lines.push(` color ${quoteIfNeeded(relation.color)}`);
3180
+ }
3181
+ if (relation.hidden) {
3182
+ lines.push(" hidden true");
3183
+ }
3184
+ return lines;
3185
+ }
2923
3186
  function formatValue(value) {
2924
3187
  if (Array.isArray(value)) {
2925
3188
  return value.map((item) => quoteIfNeeded(item)).join(" ");
@@ -2961,7 +3224,7 @@
2961
3224
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2962
3225
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2963
3226
  }
2964
- for (const key of ["background", "guides", "objects", "labels", "metadata"]) {
3227
+ for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
2965
3228
  if (layers[key] !== void 0) {
2966
3229
  tokens.push(layers[key] ? key : `-${key}`);
2967
3230
  }
@@ -2986,6 +3249,9 @@
2986
3249
  return leftIndex - rightIndex;
2987
3250
  return left.id.localeCompare(right.id);
2988
3251
  }
3252
+ function compareIdLike(left, right) {
3253
+ return left.id.localeCompare(right.id);
3254
+ }
2989
3255
  function objectTypeIndex(objectType) {
2990
3256
  switch (objectType) {
2991
3257
  case "star":
@@ -3015,180 +3281,716 @@
3015
3281
  return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
3016
3282
  }
3017
3283
 
3018
- // packages/core/dist/draft-parse.js
3019
- function parseWorldOrbitAtlas(source) {
3020
- return parseAtlasSource(source, "2.0");
3284
+ // packages/core/dist/atlas-utils.js
3285
+ 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)?$/;
3286
+ var BOOLEAN_VALUES2 = /* @__PURE__ */ new Map([
3287
+ ["true", true],
3288
+ ["false", false],
3289
+ ["yes", true],
3290
+ ["no", false]
3291
+ ]);
3292
+ var URL_SCHEME_PATTERN2 = /^[A-Za-z][A-Za-z0-9+.-]*:/;
3293
+ function normalizeIdentifier2(value) {
3294
+ return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
3021
3295
  }
3022
- function parseWorldOrbitDraft(source) {
3023
- return parseAtlasSource(source, "2.0-draft");
3296
+ function humanizeIdentifier3(value) {
3297
+ return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
3024
3298
  }
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"');
3299
+ function parseAtlasUnitValue(input, location, fieldKey) {
3300
+ const match = input.match(UNIT_PATTERN2);
3301
+ if (!match) {
3302
+ throw WorldOrbitError.fromLocation(`Invalid unit value "${input}"`, location);
3076
3303
  }
3077
- const ast = {
3078
- type: "document",
3079
- objects: objectNodes
3304
+ const unitValue = {
3305
+ value: Number(match[1]),
3306
+ unit: match[2] ?? null
3080
3307
  };
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".'
3308
+ if (fieldKey) {
3309
+ const schema = getFieldSchema(fieldKey);
3310
+ if (schema?.unitFamily && !unitFamilyAllowsUnit(schema.unitFamily, unitValue.unit)) {
3311
+ throw WorldOrbitError.fromLocation(`Unit "${unitValue.unit ?? "none"}" is not valid for "${fieldKey}"`, location);
3094
3312
  }
3095
- ] : [];
3096
- return {
3097
- format: "worldorbit",
3098
- version: outputVersion,
3099
- sourceVersion: "1.0",
3100
- system,
3101
- objects: normalizedObjects,
3102
- diagnostics
3103
- };
3104
- }
3105
- 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);
3108
- }
3109
- return tokens[1].value.toLowerCase() === "2.0-draft" ? "2.0-draft" : "2.0";
3110
- }
3111
- function startTopLevelSection(tokens, line, system, objectNodes, viewpointIds, annotationIds, flags) {
3112
- const keyword = tokens[0]?.value.toLowerCase();
3113
- switch (keyword) {
3114
- case "system":
3115
- if (system) {
3116
- throw new WorldOrbitError('Atlas section "system" may only appear once', line, tokens[0].column);
3117
- }
3118
- return startSystemSection(tokens, line);
3119
- case "defaults":
3120
- if (!system) {
3121
- throw new WorldOrbitError('Atlas section "defaults" requires a preceding system declaration', line, tokens[0].column);
3122
- }
3123
- if (flags.sawDefaults) {
3124
- throw new WorldOrbitError('Atlas section "defaults" may only appear once', line, tokens[0].column);
3125
- }
3126
- return {
3127
- kind: "defaults",
3128
- system,
3129
- seenFields: /* @__PURE__ */ new Set()
3130
- };
3131
- case "atlas":
3132
- if (!system) {
3133
- throw new WorldOrbitError('Atlas section "atlas" requires a preceding system declaration', line, tokens[0].column);
3134
- }
3135
- if (flags.sawAtlas) {
3136
- throw new WorldOrbitError('Atlas section "atlas" may only appear once', line, tokens[0].column);
3137
- }
3138
- return {
3139
- kind: "atlas",
3140
- system,
3141
- inMetadata: false,
3142
- metadataIndent: null
3143
- };
3144
- case "viewpoint":
3145
- if (!system) {
3146
- throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
3147
- }
3148
- return startViewpointSection(tokens, line, system, viewpointIds);
3149
- case "annotation":
3150
- if (!system) {
3151
- throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
3152
- }
3153
- return startAnnotationSection(tokens, line, system, annotationIds);
3154
- case "object":
3155
- return startObjectSection(tokens, line, objectNodes);
3156
- default:
3157
- throw new WorldOrbitError(`Unknown atlas section "${tokens[0]?.value ?? ""}"`, line, tokens[0]?.column ?? 1);
3158
3313
  }
3314
+ return unitValue;
3159
3315
  }
3160
- function startSystemSection(tokens, line) {
3161
- if (tokens.length !== 2) {
3162
- throw new WorldOrbitError("Invalid atlas system declaration", line, tokens[0]?.column ?? 1);
3316
+ function tryParseAtlasUnitValue(input) {
3317
+ const match = input.match(UNIT_PATTERN2);
3318
+ if (!match) {
3319
+ return null;
3163
3320
  }
3164
- const system = {
3165
- type: "system",
3166
- id: tokens[1].value,
3167
- title: null,
3168
- defaults: {
3169
- view: "topdown",
3170
- scale: null,
3171
- units: null,
3172
- preset: null,
3173
- theme: null
3174
- },
3175
- atlasMetadata: {},
3176
- viewpoints: [],
3177
- annotations: []
3178
- };
3179
3321
  return {
3180
- kind: "system",
3181
- system,
3182
- seenFields: /* @__PURE__ */ new Set()
3322
+ value: Number(match[1]),
3323
+ unit: match[2] ?? null
3183
3324
  };
3184
3325
  }
3185
- function startViewpointSection(tokens, line, system, viewpointIds) {
3186
- if (tokens.length !== 2) {
3187
- throw new WorldOrbitError("Invalid viewpoint declaration", line, tokens[0]?.column ?? 1);
3326
+ function parseAtlasNumber(input, key, location) {
3327
+ const value = Number(input);
3328
+ if (!Number.isFinite(value)) {
3329
+ throw WorldOrbitError.fromLocation(`Invalid numeric value "${input}" for "${key}"`, location);
3188
3330
  }
3189
- const id = normalizeIdentifier2(tokens[1].value);
3190
- if (!id) {
3191
- throw new WorldOrbitError("Viewpoint id must not be empty", line, tokens[1].column);
3331
+ return value;
3332
+ }
3333
+ function parseAtlasBoolean(input, key, location) {
3334
+ const parsed = BOOLEAN_VALUES2.get(input.toLowerCase());
3335
+ if (parsed === void 0) {
3336
+ throw WorldOrbitError.fromLocation(`Invalid boolean value "${input}" for "${key}"`, location);
3337
+ }
3338
+ return parsed;
3339
+ }
3340
+ function parseAtlasAtReference(target, location) {
3341
+ if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
3342
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
3343
+ }
3344
+ const pairedMatch = target.match(/^([A-Za-z0-9._-]+)-([A-Za-z0-9._-]+):(L[1-5])$/);
3345
+ if (pairedMatch) {
3346
+ return {
3347
+ kind: "lagrange",
3348
+ primary: pairedMatch[1],
3349
+ secondary: pairedMatch[2],
3350
+ point: pairedMatch[3]
3351
+ };
3352
+ }
3353
+ const simpleMatch = target.match(/^([A-Za-z0-9._-]+):(L[1-5])$/);
3354
+ if (simpleMatch) {
3355
+ return {
3356
+ kind: "lagrange",
3357
+ primary: simpleMatch[1],
3358
+ secondary: null,
3359
+ point: simpleMatch[2]
3360
+ };
3361
+ }
3362
+ if (/^[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
3363
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
3364
+ }
3365
+ const anchorMatch = target.match(/^([A-Za-z0-9._-]+):([A-Za-z0-9._-]+)$/);
3366
+ if (anchorMatch) {
3367
+ return {
3368
+ kind: "anchor",
3369
+ objectId: anchorMatch[1],
3370
+ anchor: anchorMatch[2]
3371
+ };
3372
+ }
3373
+ return {
3374
+ kind: "named",
3375
+ name: target
3376
+ };
3377
+ }
3378
+ function validateAtlasImageSource(value, location) {
3379
+ if (!value) {
3380
+ throw WorldOrbitError.fromLocation('Field "image" must not be empty', location);
3381
+ }
3382
+ if (value.startsWith("//")) {
3383
+ throw WorldOrbitError.fromLocation('Field "image" must use a relative path, root-relative path, or an http/https URL', location);
3384
+ }
3385
+ const schemeMatch = value.match(URL_SCHEME_PATTERN2);
3386
+ if (!schemeMatch) {
3387
+ return;
3388
+ }
3389
+ const scheme = schemeMatch[0].slice(0, -1).toLowerCase();
3390
+ if (scheme !== "http" && scheme !== "https") {
3391
+ throw WorldOrbitError.fromLocation(`Field "image" does not support the "${scheme}" scheme`, location);
3392
+ }
3393
+ }
3394
+ function normalizeLegacyScalarValue(key, values, location) {
3395
+ const schema = getFieldSchema(key);
3396
+ if (!schema) {
3397
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
3398
+ }
3399
+ if (schema.arity === "single" && values.length !== 1) {
3400
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
3401
+ }
3402
+ switch (schema.kind) {
3403
+ case "list":
3404
+ return values;
3405
+ case "boolean":
3406
+ return parseAtlasBoolean(singleAtlasValue(values, key, location), key, location);
3407
+ case "number":
3408
+ return parseAtlasNumber(singleAtlasValue(values, key, location), key, location);
3409
+ case "unit":
3410
+ return parseAtlasUnitValue(singleAtlasValue(values, key, location), location, key);
3411
+ case "string": {
3412
+ const value = values.join(" ").trim();
3413
+ if (key === "image") {
3414
+ validateAtlasImageSource(value, location);
3415
+ }
3416
+ return value;
3417
+ }
3418
+ }
3419
+ }
3420
+ function ensureAtlasFieldSupported(key, objectType, location) {
3421
+ const schema = getFieldSchema(key);
3422
+ if (!schema) {
3423
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
3424
+ }
3425
+ if (!schema.objectTypes.includes(objectType)) {
3426
+ throw WorldOrbitError.fromLocation(`Field "${key}" is not valid on "${objectType}"`, location);
3427
+ }
3428
+ }
3429
+ function singleAtlasValue(values, key, location) {
3430
+ if (values.length !== 1) {
3431
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
3432
+ }
3433
+ return values[0];
3434
+ }
3435
+
3436
+ // packages/core/dist/atlas-validate.js
3437
+ var SURFACE_TARGET_TYPES2 = /* @__PURE__ */ new Set(["star", "planet", "moon", "asteroid", "comet"]);
3438
+ var EARTH_MASSES_PER_SOLAR = 332946.0487;
3439
+ var JUPITER_MASSES_PER_SOLAR = 1047.3486;
3440
+ var AU_IN_KM2 = 1495978707e-1;
3441
+ var EARTH_RADIUS_IN_KM2 = 6371;
3442
+ var SOLAR_RADIUS_IN_KM2 = 695700;
3443
+ var LY_IN_AU2 = 63241.077;
3444
+ var PC_IN_AU2 = 206264.806;
3445
+ var KPC_IN_AU2 = 206264806;
3446
+ function collectAtlasDiagnostics(document, sourceSchemaVersion) {
3447
+ const diagnostics = [];
3448
+ const objectMap = new Map(document.objects.map((object) => [object.id, object]));
3449
+ const groupIds = new Set(document.groups.map((group) => group.id));
3450
+ if (!document.system) {
3451
+ diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
3452
+ }
3453
+ const knownIds = /* @__PURE__ */ new Map();
3454
+ for (const [kind, ids] of [
3455
+ ["group", document.groups.map((group) => group.id)],
3456
+ ["viewpoint", document.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
3457
+ ["annotation", document.system?.annotations.map((annotation) => annotation.id) ?? []],
3458
+ ["relation", document.relations.map((relation) => relation.id)],
3459
+ ["object", document.objects.map((object) => object.id)]
3460
+ ]) {
3461
+ for (const id of ids) {
3462
+ const previous = knownIds.get(id);
3463
+ if (previous) {
3464
+ diagnostics.push(error("validate.id.duplicate", `Duplicate ${kind} id "${id}" already used by ${previous}.`));
3465
+ } else {
3466
+ knownIds.set(id, kind);
3467
+ }
3468
+ }
3469
+ }
3470
+ for (const relation of document.relations) {
3471
+ validateRelation(relation, objectMap, diagnostics);
3472
+ }
3473
+ for (const viewpoint of document.system?.viewpoints ?? []) {
3474
+ validateViewpointFilter(viewpoint.filter, groupIds, sourceSchemaVersion, diagnostics, viewpoint.id);
3475
+ }
3476
+ for (const object of document.objects) {
3477
+ validateObject(object, document.system, objectMap, groupIds, diagnostics);
3478
+ }
3479
+ return diagnostics;
3480
+ }
3481
+ function validateRelation(relation, objectMap, diagnostics) {
3482
+ if (!relation.from) {
3483
+ diagnostics.push(error("validate.relation.from.required", `Relation "${relation.id}" is missing a "from" target.`));
3484
+ } else if (!objectMap.has(relation.from)) {
3485
+ diagnostics.push(error("validate.relation.from.unknown", `Unknown relation source "${relation.from}" on "${relation.id}".`));
3486
+ }
3487
+ if (!relation.to) {
3488
+ diagnostics.push(error("validate.relation.to.required", `Relation "${relation.id}" is missing a "to" target.`));
3489
+ } else if (!objectMap.has(relation.to)) {
3490
+ diagnostics.push(error("validate.relation.to.unknown", `Unknown relation target "${relation.to}" on "${relation.id}".`));
3491
+ }
3492
+ if (!relation.kind) {
3493
+ diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
3494
+ }
3495
+ }
3496
+ function validateViewpointFilter(filter, groupIds, sourceSchemaVersion, diagnostics, viewpointId) {
3497
+ if (!filter || sourceSchemaVersion !== "2.1") {
3498
+ return;
3499
+ }
3500
+ for (const groupId of filter.groupIds) {
3501
+ if (!groupIds.has(groupId)) {
3502
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`));
3503
+ }
3504
+ }
3505
+ }
3506
+ function validateObject(object, system, objectMap, groupIds, diagnostics) {
3507
+ const placement = object.placement;
3508
+ const orbitPlacement = placement?.mode === "orbit" ? placement : null;
3509
+ const parentObject = placement?.mode === "orbit" ? objectMap.get(placement.target) ?? null : null;
3510
+ if (object.groups) {
3511
+ for (const groupId of object.groups) {
3512
+ if (!groupIds.has(groupId)) {
3513
+ diagnostics.push(warn("validate.group.unknown", `Unknown group "${groupId}" on "${object.id}".`, object.id, "groups"));
3514
+ }
3515
+ }
3516
+ }
3517
+ if (orbitPlacement) {
3518
+ if (!objectMap.has(orbitPlacement.target)) {
3519
+ diagnostics.push(error("validate.orbit.target.unknown", `Unknown placement target "${orbitPlacement.target}" on "${object.id}".`, object.id, "orbit"));
3520
+ }
3521
+ if (orbitPlacement.distance && orbitPlacement.semiMajor) {
3522
+ diagnostics.push(error("validate.orbit.distanceConflict", `Object "${object.id}" cannot declare both "distance" and "semiMajor".`, object.id, "distance"));
3523
+ }
3524
+ if (orbitPlacement.phase && !object.epoch && !system?.epoch) {
3525
+ diagnostics.push(warn("validate.phase.epochMissing", `Object "${object.id}" sets "phase" without an object or system epoch.`, object.id, "phase"));
3526
+ }
3527
+ if (orbitPlacement.inclination && !object.referencePlane && !system?.referencePlane) {
3528
+ diagnostics.push(warn("validate.inclination.referencePlaneMissing", `Object "${object.id}" sets "inclination" without an object or system reference plane.`, object.id, "inclination"));
3529
+ }
3530
+ if (orbitPlacement.period && !massInSolar(parentObject?.properties.mass)) {
3531
+ diagnostics.push(warn("validate.period.massMissing", `Object "${object.id}" sets "period" but its central mass cannot be derived.`, object.id, "period"));
3532
+ }
3533
+ }
3534
+ if (placement?.mode === "surface") {
3535
+ const target = objectMap.get(placement.target);
3536
+ if (!target) {
3537
+ diagnostics.push(error("validate.surface.target.unknown", `Unknown placement target "${placement.target}" on "${object.id}".`, object.id, "surface"));
3538
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
3539
+ diagnostics.push(error("validate.surface.target.invalid", `Surface target "${placement.target}" on "${object.id}" is not surface-capable.`, object.id, "surface"));
3540
+ }
3541
+ }
3542
+ if (placement?.mode === "at") {
3543
+ if (object.type !== "structure" && object.type !== "phenomenon") {
3544
+ diagnostics.push(error("validate.at.objectType", `Only structures and phenomena may use "at" placement; found "${object.type}" on "${object.id}".`, object.id, "at"));
3545
+ }
3546
+ if (!validateAtTarget(object, objectMap, diagnostics)) {
3547
+ diagnostics.push(error("validate.at.target.unknown", `Unknown at-reference target "${placement.target}" on "${object.id}".`, object.id, "at"));
3548
+ }
3549
+ }
3550
+ if (object.resonance) {
3551
+ const target = objectMap.get(object.resonance.targetObjectId);
3552
+ if (!target) {
3553
+ diagnostics.push(error("validate.resonance.target.unknown", `Unknown resonance target "${object.resonance.targetObjectId}" on "${object.id}".`, object.id, "resonance"));
3554
+ } else if (object.placement?.mode !== "orbit" || target.placement?.mode !== "orbit" || object.placement.target !== target.placement.target) {
3555
+ diagnostics.push(warn("validate.resonance.orbitMismatch", `Resonance target "${object.resonance.targetObjectId}" on "${object.id}" does not share a compatible orbital parent.`, object.id, "resonance"));
3556
+ }
3557
+ }
3558
+ for (const rule of object.deriveRules ?? []) {
3559
+ if (rule.field !== "period" || rule.strategy !== "kepler") {
3560
+ diagnostics.push(warn("validate.derive.unsupported", `Unsupported derive rule "${rule.field} ${rule.strategy}" on "${object.id}".`, object.id, "derive"));
3561
+ continue;
3562
+ }
3563
+ const derivedPeriodDays = keplerPeriodDays(object, parentObject);
3564
+ if (derivedPeriodDays === null) {
3565
+ diagnostics.push(warn("validate.derive.inputsMissing", `Object "${object.id}" requests "derive period kepler" but lacks enough input data.`, object.id, "derive"));
3566
+ continue;
3567
+ }
3568
+ if (!orbitPlacement?.period) {
3569
+ diagnostics.push(info("validate.derive.period.available", `Object "${object.id}" can derive a Kepler period of ${formatDays(derivedPeriodDays)}.`, object.id, "derive"));
3570
+ }
3571
+ }
3572
+ for (const rule of object.validationRules ?? []) {
3573
+ if (rule.rule !== "kepler") {
3574
+ diagnostics.push(warn("validate.rule.unsupported", `Unsupported validation rule "${rule.rule}" on "${object.id}".`, object.id, "validate"));
3575
+ continue;
3576
+ }
3577
+ const actualPeriodDays = durationInDays(orbitPlacement?.period);
3578
+ const derivedPeriodDays = keplerPeriodDays(object, parentObject);
3579
+ if (actualPeriodDays === null || derivedPeriodDays === null) {
3580
+ continue;
3581
+ }
3582
+ const toleranceDays = toleranceForField(object, "period");
3583
+ if (Math.abs(actualPeriodDays - derivedPeriodDays) > toleranceDays) {
3584
+ diagnostics.push(error("validate.kepler.mismatch", `Object "${object.id}" fails Kepler validation for "period".`, object.id, "validate"));
3585
+ }
3586
+ }
3587
+ }
3588
+ function validateAtTarget(object, objectMap, diagnostics) {
3589
+ const reference = object.placement?.mode === "at" ? object.placement.reference : null;
3590
+ if (!reference) {
3591
+ return true;
3592
+ }
3593
+ if (reference.kind === "named") {
3594
+ return objectMap.has(reference.name);
3595
+ }
3596
+ if (reference.kind === "anchor") {
3597
+ if (!objectMap.has(reference.objectId)) {
3598
+ diagnostics.push(error("validate.anchor.target.unknown", `Unknown anchor target "${reference.objectId}" on "${object.id}".`, object.id, "at"));
3599
+ return false;
3600
+ }
3601
+ return true;
3602
+ }
3603
+ if (!objectMap.has(reference.primary)) {
3604
+ diagnostics.push(error("validate.lagrange.primary.unknown", `Unknown Lagrange reference "${reference.primary}" on "${object.id}".`, object.id, "at"));
3605
+ return false;
3606
+ }
3607
+ if (reference.secondary && !objectMap.has(reference.secondary)) {
3608
+ diagnostics.push(error("validate.lagrange.secondary.unknown", `Unknown Lagrange reference "${reference.secondary}" on "${object.id}".`, object.id, "at"));
3609
+ return false;
3610
+ }
3611
+ return true;
3612
+ }
3613
+ function keplerPeriodDays(object, parentObject) {
3614
+ const placement = object.placement;
3615
+ if (!placement || placement.mode !== "orbit") {
3616
+ return null;
3617
+ }
3618
+ const semiMajorAu = distanceInAu(placement.semiMajor ?? placement.distance);
3619
+ const centralMassSolar = massInSolar(parentObject?.properties.mass);
3620
+ if (semiMajorAu === null || centralMassSolar === null || centralMassSolar <= 0) {
3621
+ return null;
3622
+ }
3623
+ const periodYears = Math.sqrt(semiMajorAu ** 3 / centralMassSolar);
3624
+ return periodYears * 365.25;
3625
+ }
3626
+ function distanceInAu(value) {
3627
+ if (!value)
3628
+ return null;
3629
+ switch (value.unit) {
3630
+ case null:
3631
+ case "au":
3632
+ return value.value;
3633
+ case "km":
3634
+ return value.value / AU_IN_KM2;
3635
+ case "m":
3636
+ return value.value / (AU_IN_KM2 * 1e3);
3637
+ case "ly":
3638
+ return value.value * LY_IN_AU2;
3639
+ case "pc":
3640
+ return value.value * PC_IN_AU2;
3641
+ case "kpc":
3642
+ return value.value * KPC_IN_AU2;
3643
+ case "re":
3644
+ return value.value * EARTH_RADIUS_IN_KM2 / AU_IN_KM2;
3645
+ case "sol":
3646
+ return value.value * SOLAR_RADIUS_IN_KM2 / AU_IN_KM2;
3647
+ default:
3648
+ return null;
3649
+ }
3650
+ }
3651
+ function massInSolar(value) {
3652
+ if (!value || typeof value !== "object" || !("value" in value)) {
3653
+ return null;
3654
+ }
3655
+ const unitValue = value;
3656
+ switch (unitValue.unit) {
3657
+ case null:
3658
+ case "sol":
3659
+ return unitValue.value;
3660
+ case "me":
3661
+ return unitValue.value / EARTH_MASSES_PER_SOLAR;
3662
+ case "mj":
3663
+ return unitValue.value / JUPITER_MASSES_PER_SOLAR;
3664
+ default:
3665
+ return null;
3666
+ }
3667
+ }
3668
+ function durationInDays(value) {
3669
+ if (!value)
3670
+ return null;
3671
+ switch (value.unit) {
3672
+ case null:
3673
+ case "d":
3674
+ return value.value;
3675
+ case "s":
3676
+ return value.value / 86400;
3677
+ case "min":
3678
+ return value.value / 1440;
3679
+ case "h":
3680
+ return value.value / 24;
3681
+ case "y":
3682
+ return value.value * 365.25;
3683
+ case "ky":
3684
+ return value.value * 365250;
3685
+ case "my":
3686
+ return value.value * 36525e4;
3687
+ case "gy":
3688
+ return value.value * 36525e7;
3689
+ default:
3690
+ return null;
3691
+ }
3692
+ }
3693
+ function toleranceForField(object, field) {
3694
+ const tolerance = object.tolerances?.find((entry) => entry.field === field)?.value;
3695
+ if (typeof tolerance === "number") {
3696
+ return tolerance;
3697
+ }
3698
+ if (tolerance && typeof tolerance === "object" && "value" in tolerance) {
3699
+ return durationInDays(tolerance) ?? 0;
3700
+ }
3701
+ return 0;
3702
+ }
3703
+ function formatDays(days) {
3704
+ return `${Math.round(days * 100) / 100}d`;
3705
+ }
3706
+ function error(code, message, objectId, field) {
3707
+ return { code, severity: "error", source: "validate", message, objectId, field };
3708
+ }
3709
+ function warn(code, message, objectId, field) {
3710
+ return { code, severity: "warning", source: "validate", message, objectId, field };
3711
+ }
3712
+ function info(code, message, objectId, field) {
3713
+ return { code, severity: "info", source: "validate", message, objectId, field };
3714
+ }
3715
+
3716
+ // packages/core/dist/draft-parse.js
3717
+ var STRUCTURED_TYPED_BLOCKS = /* @__PURE__ */ new Set([
3718
+ "climate",
3719
+ "habitability",
3720
+ "settlement"
3721
+ ]);
3722
+ var DRAFT_OBJECT_FIELD_SPECS = /* @__PURE__ */ new Map();
3723
+ for (const key of [
3724
+ "orbit",
3725
+ "distance",
3726
+ "semiMajor",
3727
+ "eccentricity",
3728
+ "period",
3729
+ "angle",
3730
+ "inclination",
3731
+ "phase",
3732
+ "at",
3733
+ "surface",
3734
+ "free",
3735
+ "kind",
3736
+ "class",
3737
+ "culture",
3738
+ "tags",
3739
+ "color",
3740
+ "image",
3741
+ "hidden",
3742
+ "radius",
3743
+ "mass",
3744
+ "density",
3745
+ "gravity",
3746
+ "temperature",
3747
+ "albedo",
3748
+ "atmosphere",
3749
+ "inner",
3750
+ "outer",
3751
+ "on",
3752
+ "source",
3753
+ "cycle"
3754
+ ]) {
3755
+ const schema = getFieldSchema(key);
3756
+ if (schema) {
3757
+ DRAFT_OBJECT_FIELD_SPECS.set(key, {
3758
+ key,
3759
+ version: "2.0",
3760
+ inlineMode: schema.arity === "multiple" ? "multiple" : "single",
3761
+ allowRepeat: false,
3762
+ legacySchema: schema
3763
+ });
3764
+ }
3765
+ }
3766
+ for (const spec of [
3767
+ { key: "groups", inlineMode: "multiple", allowRepeat: false },
3768
+ { key: "epoch", inlineMode: "single", allowRepeat: false },
3769
+ { key: "referencePlane", inlineMode: "single", allowRepeat: false },
3770
+ { key: "tidalLock", inlineMode: "single", allowRepeat: false },
3771
+ { key: "renderLabel", inlineMode: "single", allowRepeat: false },
3772
+ { key: "renderOrbit", inlineMode: "single", allowRepeat: false },
3773
+ { key: "renderPriority", inlineMode: "single", allowRepeat: false },
3774
+ { key: "resonance", inlineMode: "pair", allowRepeat: false },
3775
+ { key: "derive", inlineMode: "pair", allowRepeat: true },
3776
+ { key: "validate", inlineMode: "single", allowRepeat: true },
3777
+ { key: "locked", inlineMode: "multiple", allowRepeat: false },
3778
+ { key: "tolerance", inlineMode: "pair", allowRepeat: true }
3779
+ ]) {
3780
+ DRAFT_OBJECT_FIELD_SPECS.set(spec.key, {
3781
+ key: spec.key,
3782
+ version: "2.1",
3783
+ inlineMode: spec.inlineMode,
3784
+ allowRepeat: spec.allowRepeat
3785
+ });
3786
+ }
3787
+ var DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
3788
+ function parseWorldOrbitAtlas(source) {
3789
+ return parseAtlasSource(source);
3790
+ }
3791
+ function parseWorldOrbitDraft(source) {
3792
+ return parseAtlasSource(source, "2.0-draft");
3793
+ }
3794
+ function parseAtlasSource(source, forcedOutputVersion) {
3795
+ const prepared = preprocessAtlasSource(source);
3796
+ const lines = prepared.source.split(/\r?\n/);
3797
+ const diagnostics = [];
3798
+ let sawSchemaHeader = false;
3799
+ let sourceSchemaVersion = "2.0";
3800
+ let system = null;
3801
+ let section = null;
3802
+ const objectNodes = [];
3803
+ const groups = [];
3804
+ const relations = [];
3805
+ let sawDefaults = false;
3806
+ let sawAtlas = false;
3807
+ const viewpointIds = /* @__PURE__ */ new Set();
3808
+ const annotationIds = /* @__PURE__ */ new Set();
3809
+ const groupIds = /* @__PURE__ */ new Set();
3810
+ const relationIds = /* @__PURE__ */ new Set();
3811
+ for (let index = 0; index < lines.length; index++) {
3812
+ const rawLine = lines[index];
3813
+ const lineNumber = index + 1;
3814
+ if (!rawLine.trim()) {
3815
+ continue;
3816
+ }
3817
+ const indent = getIndent(rawLine);
3818
+ const tokens = tokenizeLineDetailed(rawLine.slice(indent), {
3819
+ line: lineNumber,
3820
+ columnOffset: indent
3821
+ });
3822
+ if (tokens.length === 0) {
3823
+ continue;
3824
+ }
3825
+ if (!sawSchemaHeader) {
3826
+ sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
3827
+ sawSchemaHeader = true;
3828
+ if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
3829
+ diagnostics.push({
3830
+ code: "parse.schema21.commentCompatibility",
3831
+ severity: "warning",
3832
+ source: "parse",
3833
+ message: `Comments require schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
3834
+ line: prepared.comments[0].line,
3835
+ column: prepared.comments[0].column
3836
+ });
3837
+ }
3838
+ continue;
3839
+ }
3840
+ if (indent === 0) {
3841
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, { sawDefaults, sawAtlas });
3842
+ if (section.kind === "system") {
3843
+ system = section.system;
3844
+ } else if (section.kind === "defaults") {
3845
+ sawDefaults = true;
3846
+ } else if (section.kind === "atlas") {
3847
+ sawAtlas = true;
3848
+ }
3849
+ continue;
3850
+ }
3851
+ if (!section) {
3852
+ throw new WorldOrbitError("Indented line without parent atlas section", lineNumber, indent + 1);
3853
+ }
3854
+ handleSectionLine(section, indent, tokens, lineNumber);
3855
+ }
3856
+ if (!sawSchemaHeader) {
3857
+ throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
3858
+ }
3859
+ const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
3860
+ const outputVersion = forcedOutputVersion ?? (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
3861
+ const baseDocument = {
3862
+ format: "worldorbit",
3863
+ sourceVersion: "1.0",
3864
+ system,
3865
+ groups,
3866
+ relations,
3867
+ objects,
3868
+ diagnostics
3869
+ };
3870
+ if (outputVersion === "2.0-draft") {
3871
+ const document2 = {
3872
+ ...baseDocument,
3873
+ version: "2.0-draft",
3874
+ schemaVersion: "2.0-draft"
3875
+ };
3876
+ document2.diagnostics.push(...collectAtlasDiagnostics(document2, sourceSchemaVersion));
3877
+ return document2;
3878
+ }
3879
+ const document = {
3880
+ ...baseDocument,
3881
+ version: outputVersion,
3882
+ schemaVersion: outputVersion
3883
+ };
3884
+ if (sourceSchemaVersion === "2.0-draft") {
3885
+ document.diagnostics.push({
3886
+ code: "load.schema.deprecatedDraft",
3887
+ severity: "warning",
3888
+ source: "upgrade",
3889
+ message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".'
3890
+ });
3891
+ }
3892
+ document.diagnostics.push(...collectAtlasDiagnostics(document, sourceSchemaVersion));
3893
+ return document;
3894
+ }
3895
+ function assertDraftSchemaHeader(tokens, line) {
3896
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
3897
+ throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
3898
+ }
3899
+ const version = tokens[1].value.toLowerCase();
3900
+ return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
3901
+ }
3902
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, flags) {
3903
+ const keyword = tokens[0]?.value.toLowerCase();
3904
+ switch (keyword) {
3905
+ case "system":
3906
+ if (system) {
3907
+ throw new WorldOrbitError('Atlas section "system" may only appear once', line, tokens[0].column);
3908
+ }
3909
+ return startSystemSection(tokens, line, sourceSchemaVersion, diagnostics);
3910
+ case "defaults":
3911
+ if (!system) {
3912
+ throw new WorldOrbitError('Atlas section "defaults" requires a preceding system declaration', line, tokens[0].column);
3913
+ }
3914
+ if (flags.sawDefaults) {
3915
+ throw new WorldOrbitError('Atlas section "defaults" may only appear once', line, tokens[0].column);
3916
+ }
3917
+ return {
3918
+ kind: "defaults",
3919
+ system,
3920
+ seenFields: /* @__PURE__ */ new Set()
3921
+ };
3922
+ case "atlas":
3923
+ if (!system) {
3924
+ throw new WorldOrbitError('Atlas section "atlas" requires a preceding system declaration', line, tokens[0].column);
3925
+ }
3926
+ if (flags.sawAtlas) {
3927
+ throw new WorldOrbitError('Atlas section "atlas" may only appear once', line, tokens[0].column);
3928
+ }
3929
+ return {
3930
+ kind: "atlas",
3931
+ system,
3932
+ inMetadata: false,
3933
+ metadataIndent: null
3934
+ };
3935
+ case "viewpoint":
3936
+ if (!system) {
3937
+ throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
3938
+ }
3939
+ return startViewpointSection(tokens, line, system, viewpointIds);
3940
+ case "annotation":
3941
+ if (!system) {
3942
+ throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
3943
+ }
3944
+ return startAnnotationSection(tokens, line, system, annotationIds);
3945
+ case "group":
3946
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "group", { line, column: tokens[0].column });
3947
+ return startGroupSection(tokens, line, groups, groupIds);
3948
+ case "relation":
3949
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
3950
+ return startRelationSection(tokens, line, relations, relationIds);
3951
+ case "object":
3952
+ return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
3953
+ default:
3954
+ throw new WorldOrbitError(`Unknown atlas section "${tokens[0]?.value ?? ""}"`, line, tokens[0]?.column ?? 1);
3955
+ }
3956
+ }
3957
+ function startSystemSection(tokens, line, sourceSchemaVersion, diagnostics) {
3958
+ if (tokens.length !== 2) {
3959
+ throw new WorldOrbitError("Invalid atlas system declaration", line, tokens[0]?.column ?? 1);
3960
+ }
3961
+ const system = {
3962
+ type: "system",
3963
+ id: tokens[1].value,
3964
+ title: null,
3965
+ description: null,
3966
+ epoch: null,
3967
+ referencePlane: null,
3968
+ defaults: {
3969
+ view: "topdown",
3970
+ scale: null,
3971
+ units: null,
3972
+ preset: null,
3973
+ theme: null
3974
+ },
3975
+ atlasMetadata: {},
3976
+ viewpoints: [],
3977
+ annotations: []
3978
+ };
3979
+ return {
3980
+ kind: "system",
3981
+ system,
3982
+ sourceSchemaVersion,
3983
+ diagnostics,
3984
+ seenFields: /* @__PURE__ */ new Set()
3985
+ };
3986
+ }
3987
+ function startViewpointSection(tokens, line, system, viewpointIds) {
3988
+ if (tokens.length !== 2) {
3989
+ throw new WorldOrbitError("Invalid viewpoint declaration", line, tokens[0]?.column ?? 1);
3990
+ }
3991
+ const id = normalizeIdentifier2(tokens[1].value);
3992
+ if (!id) {
3993
+ throw new WorldOrbitError("Viewpoint id must not be empty", line, tokens[1].column);
3192
3994
  }
3193
3995
  if (viewpointIds.has(id)) {
3194
3996
  throw new WorldOrbitError(`Duplicate viewpoint id "${id}"`, line, tokens[1].column);
@@ -3244,7 +4046,64 @@
3244
4046
  seenFields: /* @__PURE__ */ new Set()
3245
4047
  };
3246
4048
  }
3247
- function startObjectSection(tokens, line, objectNodes) {
4049
+ function startGroupSection(tokens, line, groups, groupIds) {
4050
+ if (tokens.length !== 2) {
4051
+ throw new WorldOrbitError("Invalid group declaration", line, tokens[0]?.column ?? 1);
4052
+ }
4053
+ const id = normalizeIdentifier2(tokens[1].value);
4054
+ if (!id) {
4055
+ throw new WorldOrbitError("Group id must not be empty", line, tokens[1].column);
4056
+ }
4057
+ if (groupIds.has(id)) {
4058
+ throw new WorldOrbitError(`Duplicate group id "${id}"`, line, tokens[1].column);
4059
+ }
4060
+ const group = {
4061
+ id,
4062
+ label: humanizeIdentifier3(id),
4063
+ summary: "",
4064
+ color: null,
4065
+ tags: [],
4066
+ hidden: false
4067
+ };
4068
+ groups.push(group);
4069
+ groupIds.add(id);
4070
+ return {
4071
+ kind: "group",
4072
+ group,
4073
+ seenFields: /* @__PURE__ */ new Set()
4074
+ };
4075
+ }
4076
+ function startRelationSection(tokens, line, relations, relationIds) {
4077
+ if (tokens.length !== 2) {
4078
+ throw new WorldOrbitError("Invalid relation declaration", line, tokens[0]?.column ?? 1);
4079
+ }
4080
+ const id = normalizeIdentifier2(tokens[1].value);
4081
+ if (!id) {
4082
+ throw new WorldOrbitError("Relation id must not be empty", line, tokens[1].column);
4083
+ }
4084
+ if (relationIds.has(id)) {
4085
+ throw new WorldOrbitError(`Duplicate relation id "${id}"`, line, tokens[1].column);
4086
+ }
4087
+ const relation = {
4088
+ id,
4089
+ from: "",
4090
+ to: "",
4091
+ kind: "",
4092
+ label: null,
4093
+ summary: null,
4094
+ tags: [],
4095
+ color: null,
4096
+ hidden: false
4097
+ };
4098
+ relations.push(relation);
4099
+ relationIds.add(id);
4100
+ return {
4101
+ kind: "relation",
4102
+ relation,
4103
+ seenFields: /* @__PURE__ */ new Set()
4104
+ };
4105
+ }
4106
+ function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
3248
4107
  if (tokens.length < 3) {
3249
4108
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
3250
4109
  }
@@ -3255,12 +4114,11 @@
3255
4114
  throw new WorldOrbitError(`Unknown object type "${objectTypeToken.value}"`, line, objectTypeToken.column);
3256
4115
  }
3257
4116
  const objectNode = {
3258
- type: "object",
3259
4117
  objectType,
3260
- name: idToken.value,
3261
- inlineFields: parseInlineFields2(tokens.slice(3), line),
3262
- blockFields: [],
4118
+ id: idToken.value,
4119
+ fields: parseInlineObjectFields(tokens.slice(3), line, objectType, sourceSchemaVersion, diagnostics),
3263
4120
  infoEntries: [],
4121
+ typedBlockEntries: {},
3264
4122
  location: {
3265
4123
  line,
3266
4124
  column: objectTypeToken.column
@@ -3270,8 +4128,12 @@
3270
4128
  return {
3271
4129
  kind: "object",
3272
4130
  objectNode,
3273
- inInfoBlock: false,
3274
- infoIndent: null
4131
+ sourceSchemaVersion,
4132
+ diagnostics,
4133
+ activeBlock: null,
4134
+ blockIndent: null,
4135
+ seenInfoKeys: /* @__PURE__ */ new Set(),
4136
+ seenTypedBlockKeys: {}
3275
4137
  };
3276
4138
  }
3277
4139
  function handleSectionLine(section, indent, tokens, line) {
@@ -3291,6 +4153,12 @@
3291
4153
  case "annotation":
3292
4154
  applyAnnotationField(section, tokens, line);
3293
4155
  return;
4156
+ case "group":
4157
+ applyGroupField(section, tokens, line);
4158
+ return;
4159
+ case "relation":
4160
+ applyRelationField(section, tokens, line);
4161
+ return;
3294
4162
  case "object":
3295
4163
  applyObjectField(section, indent, tokens, line);
3296
4164
  return;
@@ -3298,10 +4166,35 @@
3298
4166
  }
3299
4167
  function applySystemField(section, tokens, line) {
3300
4168
  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);
4169
+ const value = joinFieldValue(tokens, line);
4170
+ switch (key) {
4171
+ case "title":
4172
+ section.system.title = value;
4173
+ return;
4174
+ case "description":
4175
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
4176
+ line,
4177
+ column: tokens[0].column
4178
+ });
4179
+ section.system.description = value;
4180
+ return;
4181
+ case "epoch":
4182
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
4183
+ line,
4184
+ column: tokens[0].column
4185
+ });
4186
+ section.system.epoch = value;
4187
+ return;
4188
+ case "referenceplane":
4189
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "referencePlane", {
4190
+ line,
4191
+ column: tokens[0].column
4192
+ });
4193
+ section.system.referencePlane = value;
4194
+ return;
4195
+ default:
4196
+ throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
3303
4197
  }
3304
- section.system.title = joinFieldValue(tokens, line);
3305
4198
  }
3306
4199
  function applyDefaultsField(section, tokens, line) {
3307
4200
  const key = requireUniqueField(tokens, section.seenFields, line);
@@ -3332,14 +4225,11 @@
3332
4225
  section.metadataIndent = null;
3333
4226
  }
3334
4227
  if (section.inMetadata) {
3335
- if (tokens.length < 2) {
3336
- throw new WorldOrbitError("Invalid atlas metadata entry", line, tokens[0]?.column ?? 1);
3337
- }
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);
4228
+ const entry = parseInfoLikeEntry(tokens, line, "Invalid atlas metadata entry");
4229
+ if (entry.key in section.system.atlasMetadata) {
4230
+ throw new WorldOrbitError(`Duplicate atlas metadata key "${entry.key}"`, line, tokens[0].column);
3341
4231
  }
3342
- section.system.atlasMetadata[key] = joinFieldValue(tokens, line);
4232
+ section.system.atlasMetadata[entry.key] = entry.value;
3343
4233
  return;
3344
4234
  }
3345
4235
  if (tokens.length === 1 && tokens[0].value.toLowerCase() === "metadata") {
@@ -3412,50 +4302,131 @@
3412
4302
  filter.objectTypes = parseObjectTypeTokens(tokens.slice(1), line);
3413
4303
  break;
3414
4304
  case "tags":
3415
- filter.tags = parseTokenList(tokens.slice(1), line, "tags");
3416
- break;
3417
- case "groups":
3418
- filter.groupIds = parseTokenList(tokens.slice(1), line, "groups");
3419
- break;
4305
+ filter.tags = parseTokenList(tokens.slice(1), line, "tags");
4306
+ break;
4307
+ case "groups":
4308
+ filter.groupIds = parseTokenList(tokens.slice(1), line, "groups");
4309
+ break;
4310
+ default:
4311
+ throw new WorldOrbitError(`Unknown viewpoint filter field "${tokens[0].value}"`, line, tokens[0].column);
4312
+ }
4313
+ section.viewpoint.filter = filter;
4314
+ }
4315
+ function applyAnnotationField(section, tokens, line) {
4316
+ const key = requireUniqueField(tokens, section.seenFields, line);
4317
+ switch (key) {
4318
+ case "label":
4319
+ section.annotation.label = joinFieldValue(tokens, line);
4320
+ return;
4321
+ case "target":
4322
+ section.annotation.targetObjectId = joinFieldValue(tokens, line);
4323
+ return;
4324
+ case "body":
4325
+ section.annotation.body = joinFieldValue(tokens, line);
4326
+ return;
4327
+ case "tags":
4328
+ section.annotation.tags = parseTokenList(tokens.slice(1), line, "tags");
4329
+ return;
4330
+ default:
4331
+ throw new WorldOrbitError(`Unknown annotation field "${tokens[0].value}"`, line, tokens[0].column);
4332
+ }
4333
+ }
4334
+ function applyGroupField(section, tokens, line) {
4335
+ const key = requireUniqueField(tokens, section.seenFields, line);
4336
+ switch (key) {
4337
+ case "label":
4338
+ section.group.label = joinFieldValue(tokens, line);
4339
+ return;
4340
+ case "summary":
4341
+ section.group.summary = joinFieldValue(tokens, line);
4342
+ return;
4343
+ case "color":
4344
+ section.group.color = joinFieldValue(tokens, line);
4345
+ return;
4346
+ case "tags":
4347
+ section.group.tags = parseTokenList(tokens.slice(1), line, "tags");
4348
+ return;
4349
+ case "hidden":
4350
+ section.group.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4351
+ line,
4352
+ column: tokens[0].column
4353
+ });
4354
+ return;
3420
4355
  default:
3421
- throw new WorldOrbitError(`Unknown viewpoint filter field "${tokens[0].value}"`, line, tokens[0].column);
4356
+ throw new WorldOrbitError(`Unknown group field "${tokens[0].value}"`, line, tokens[0].column);
3422
4357
  }
3423
- section.viewpoint.filter = filter;
3424
4358
  }
3425
- function applyAnnotationField(section, tokens, line) {
4359
+ function applyRelationField(section, tokens, line) {
3426
4360
  const key = requireUniqueField(tokens, section.seenFields, line);
3427
4361
  switch (key) {
3428
- case "label":
3429
- section.annotation.label = joinFieldValue(tokens, line);
4362
+ case "from":
4363
+ section.relation.from = joinFieldValue(tokens, line);
3430
4364
  return;
3431
- case "target":
3432
- section.annotation.targetObjectId = joinFieldValue(tokens, line);
4365
+ case "to":
4366
+ section.relation.to = joinFieldValue(tokens, line);
3433
4367
  return;
3434
- case "body":
3435
- section.annotation.body = joinFieldValue(tokens, line);
4368
+ case "kind":
4369
+ section.relation.kind = joinFieldValue(tokens, line);
4370
+ return;
4371
+ case "label":
4372
+ section.relation.label = joinFieldValue(tokens, line);
4373
+ return;
4374
+ case "summary":
4375
+ section.relation.summary = joinFieldValue(tokens, line);
3436
4376
  return;
3437
4377
  case "tags":
3438
- section.annotation.tags = parseTokenList(tokens.slice(1), line, "tags");
4378
+ section.relation.tags = parseTokenList(tokens.slice(1), line, "tags");
4379
+ return;
4380
+ case "color":
4381
+ section.relation.color = joinFieldValue(tokens, line);
4382
+ return;
4383
+ case "hidden":
4384
+ section.relation.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4385
+ line,
4386
+ column: tokens[0].column
4387
+ });
3439
4388
  return;
3440
4389
  default:
3441
- throw new WorldOrbitError(`Unknown annotation field "${tokens[0].value}"`, line, tokens[0].column);
4390
+ throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
3442
4391
  }
3443
4392
  }
3444
4393
  function applyObjectField(section, indent, tokens, line) {
3445
- if (tokens.length === 1 && tokens[0].value === "info") {
3446
- section.inInfoBlock = true;
3447
- section.infoIndent = indent;
3448
- return;
3449
- }
3450
- if (section.inInfoBlock && indent <= (section.infoIndent ?? 0)) {
3451
- section.inInfoBlock = false;
3452
- section.infoIndent = null;
4394
+ if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
4395
+ section.activeBlock = null;
4396
+ section.blockIndent = null;
4397
+ }
4398
+ if (tokens.length === 1) {
4399
+ const blockName = tokens[0].value.toLowerCase();
4400
+ if (blockName === "info" || STRUCTURED_TYPED_BLOCKS.has(blockName)) {
4401
+ if (blockName !== "info") {
4402
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, blockName, { line, column: tokens[0].column });
4403
+ }
4404
+ section.activeBlock = blockName;
4405
+ section.blockIndent = indent;
4406
+ return;
4407
+ }
3453
4408
  }
3454
- if (section.inInfoBlock) {
3455
- section.objectNode.infoEntries.push(parseInfoEntry2(tokens, line));
4409
+ if (section.activeBlock) {
4410
+ const entry = parseInfoLikeEntry(tokens, line, `Invalid ${section.activeBlock} entry`);
4411
+ if (section.activeBlock === "info") {
4412
+ if (section.seenInfoKeys.has(entry.key)) {
4413
+ throw new WorldOrbitError(`Duplicate info key "${entry.key}"`, line, tokens[0].column);
4414
+ }
4415
+ section.seenInfoKeys.add(entry.key);
4416
+ section.objectNode.infoEntries.push(entry);
4417
+ return;
4418
+ }
4419
+ const typedBlock = section.activeBlock;
4420
+ const seenKeys = section.seenTypedBlockKeys[typedBlock] ?? (section.seenTypedBlockKeys[typedBlock] = /* @__PURE__ */ new Set());
4421
+ if (seenKeys.has(entry.key)) {
4422
+ throw new WorldOrbitError(`Duplicate ${typedBlock} key "${entry.key}"`, line, tokens[0].column);
4423
+ }
4424
+ seenKeys.add(entry.key);
4425
+ const entries = section.objectNode.typedBlockEntries[typedBlock] ?? (section.objectNode.typedBlockEntries[typedBlock] = []);
4426
+ entries.push(entry);
3456
4427
  return;
3457
4428
  }
3458
- section.objectNode.blockFields.push(parseField2(tokens, line));
4429
+ section.objectNode.fields.push(parseObjectField(tokens, line, section.objectNode.objectType, section.sourceSchemaVersion, section.diagnostics));
3459
4430
  }
3460
4431
  function requireUniqueField(tokens, seenFields, line) {
3461
4432
  if (tokens.length < 2) {
@@ -3475,50 +4446,40 @@
3475
4446
  return tokens.slice(1).map((token) => token.value).join(" ").trim();
3476
4447
  }
3477
4448
  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
- });
3488
- }
3489
- function parseTokenList(tokens, line, field) {
3490
- if (tokens.length === 0) {
3491
- throw new WorldOrbitError(`Missing value for field "${field}"`, line);
3492
- }
3493
- return tokens.map((token) => token.value);
4449
+ 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");
3494
4450
  }
3495
4451
  function parseLayerTokens(tokens, line) {
3496
- if (tokens.length === 0) {
3497
- throw new WorldOrbitError('Missing value for field "layers"', line);
3498
- }
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;
4452
+ const layers = {};
4453
+ for (const token of parseTokenList(tokens, line, "layers")) {
4454
+ const enabled = !token.startsWith("-") && !token.startsWith("!");
4455
+ const raw = token.replace(/^[-!]+/, "").toLowerCase();
4456
+ if (raw === "orbits") {
4457
+ layers["orbits-back"] = enabled;
4458
+ layers["orbits-front"] = enabled;
3506
4459
  continue;
3507
4460
  }
3508
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
3509
- next[rawLayer] = enabled;
3510
- continue;
4461
+ if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "objects" || raw === "labels" || raw === "metadata") {
4462
+ layers[raw] = enabled;
3511
4463
  }
3512
- throw new WorldOrbitError(`Unknown layer token "${token.value}"`, line, token.column);
3513
4464
  }
3514
- return next;
4465
+ return layers;
4466
+ }
4467
+ function parseTokenList(tokens, line, fieldName) {
4468
+ if (tokens.length === 0) {
4469
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, 1);
4470
+ }
4471
+ const values = tokens.map((token) => token.value).filter(Boolean);
4472
+ if (values.length === 0) {
4473
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, tokens[0]?.column ?? 1);
4474
+ }
4475
+ return values;
3515
4476
  }
3516
4477
  function parseProjectionValue(value, line, column) {
3517
4478
  const normalized = value.toLowerCase();
3518
- if (normalized === "topdown" || normalized === "isometric") {
3519
- return normalized;
4479
+ if (normalized !== "topdown" && normalized !== "isometric") {
4480
+ throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
3520
4481
  }
3521
- throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
4482
+ return normalized;
3522
4483
  }
3523
4484
  function parsePresetValue(value, line, column) {
3524
4485
  const normalized = value.toLowerCase();
@@ -3528,16 +4489,16 @@
3528
4489
  throw new WorldOrbitError(`Unknown render preset "${value}"`, line, column);
3529
4490
  }
3530
4491
  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);
4492
+ const parsed = parseFiniteNumber2(value, line, column, field);
4493
+ if (parsed <= 0) {
4494
+ throw new WorldOrbitError(`Field "${field}" must be greater than zero`, line, column);
3534
4495
  }
3535
4496
  return parsed;
3536
4497
  }
3537
4498
  function parseFiniteNumber2(value, line, column, field) {
3538
4499
  const parsed = Number(value);
3539
4500
  if (!Number.isFinite(parsed)) {
3540
- throw new WorldOrbitError(`Field "${field}" expects a finite number`, line, column);
4501
+ throw new WorldOrbitError(`Invalid numeric value "${value}" for "${field}"`, line, column);
3541
4502
  }
3542
4503
  return parsed;
3543
4504
  }
@@ -3549,28 +4510,43 @@
3549
4510
  groupIds: []
3550
4511
  };
3551
4512
  }
3552
- function parseInlineFields2(tokens, line) {
4513
+ function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
3553
4514
  const fields = [];
3554
4515
  let index = 0;
3555
4516
  while (index < tokens.length) {
3556
4517
  const keyToken = tokens[index];
3557
- const schema = getFieldSchema(keyToken.value);
3558
- if (!schema) {
4518
+ const spec = getDraftObjectFieldSpec(keyToken.value);
4519
+ if (!spec) {
3559
4520
  throw new WorldOrbitError(`Unknown field "${keyToken.value}"`, line, keyToken.column);
3560
4521
  }
4522
+ if (spec.version === "2.1") {
4523
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, keyToken.value, {
4524
+ line,
4525
+ column: keyToken.column
4526
+ });
4527
+ }
3561
4528
  index++;
3562
4529
  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 {
4530
+ if (spec.inlineMode === "single") {
3569
4531
  const nextToken = tokens[index];
3570
4532
  if (nextToken) {
3571
4533
  valueTokens.push(nextToken);
3572
4534
  index++;
3573
4535
  }
4536
+ } else if (spec.inlineMode === "pair") {
4537
+ for (let count = 0; count < 2; count++) {
4538
+ const nextToken = tokens[index];
4539
+ if (!nextToken) {
4540
+ break;
4541
+ }
4542
+ valueTokens.push(nextToken);
4543
+ index++;
4544
+ }
4545
+ } else {
4546
+ while (index < tokens.length && !DRAFT_OBJECT_FIELD_KEYS.has(tokens[index].value)) {
4547
+ valueTokens.push(tokens[index]);
4548
+ index++;
4549
+ }
3574
4550
  }
3575
4551
  if (valueTokens.length === 0) {
3576
4552
  throw new WorldOrbitError(`Missing value for field "${keyToken.value}"`, line, keyToken.column);
@@ -3582,25 +4558,35 @@
3582
4558
  location: { line, column: keyToken.column }
3583
4559
  });
3584
4560
  }
4561
+ validateDraftObjectFieldCompatibility(fields, objectType);
3585
4562
  return fields;
3586
4563
  }
3587
- function parseField2(tokens, line) {
4564
+ function parseObjectField(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
3588
4565
  if (tokens.length < 2) {
3589
4566
  throw new WorldOrbitError("Invalid field line", line, tokens[0]?.column ?? 1);
3590
4567
  }
3591
- if (!getFieldSchema(tokens[0].value)) {
4568
+ const spec = getDraftObjectFieldSpec(tokens[0].value);
4569
+ if (!spec) {
3592
4570
  throw new WorldOrbitError(`Unknown field "${tokens[0].value}"`, line, tokens[0].column);
3593
4571
  }
3594
- return {
4572
+ if (spec.version === "2.1") {
4573
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, tokens[0].value, {
4574
+ line,
4575
+ column: tokens[0].column
4576
+ });
4577
+ }
4578
+ const field = {
3595
4579
  type: "field",
3596
4580
  key: tokens[0].value,
3597
4581
  values: tokens.slice(1).map((token) => token.value),
3598
4582
  location: { line, column: tokens[0].column }
3599
4583
  };
4584
+ validateDraftObjectFieldCompatibility([field], objectType);
4585
+ return field;
3600
4586
  }
3601
- function parseInfoEntry2(tokens, line) {
4587
+ function parseInfoLikeEntry(tokens, line, errorMessage) {
3602
4588
  if (tokens.length < 2) {
3603
- throw new WorldOrbitError("Invalid info entry", line, tokens[0]?.column ?? 1);
4589
+ throw new WorldOrbitError(errorMessage, line, tokens[0]?.column ?? 1);
3604
4590
  }
3605
4591
  return {
3606
4592
  type: "info-entry",
@@ -3609,23 +4595,356 @@
3609
4595
  location: { line, column: tokens[0].column }
3610
4596
  };
3611
4597
  }
3612
- function normalizeIdentifier2(value) {
3613
- return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
4598
+ function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
4599
+ const fieldMap = collectDraftFields(node.fields);
4600
+ const placement = extractDraftPlacement(node.objectType, fieldMap);
4601
+ const properties = normalizeDraftProperties(node.objectType, fieldMap);
4602
+ const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
4603
+ const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
4604
+ const referencePlane = parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0]);
4605
+ const tidalLock = fieldMap.has("tidalLock") ? parseAtlasBoolean(singleFieldValue2(fieldMap.get("tidalLock")[0]), "tidalLock", fieldMap.get("tidalLock")[0].location) : void 0;
4606
+ const resonance = fieldMap.has("resonance") ? parseResonanceField(fieldMap.get("resonance")[0]) : void 0;
4607
+ const renderHints = extractRenderHints(fieldMap);
4608
+ const deriveRules = fieldMap.get("derive")?.map((field) => parseDeriveField(field));
4609
+ const validationRules = fieldMap.get("validate")?.map((field) => ({
4610
+ rule: singleFieldValue2(field)
4611
+ }));
4612
+ const lockedFields = fieldMap.has("locked") ? [...new Set(fieldMap.get("locked").flatMap((field) => field.values))] : void 0;
4613
+ const tolerances = fieldMap.get("tolerance")?.map((field) => parseToleranceField(field));
4614
+ const typedBlocks = normalizeTypedBlocks(node.typedBlockEntries);
4615
+ const info2 = normalizeInfoEntries(node.infoEntries, "info");
4616
+ const object = {
4617
+ type: node.objectType,
4618
+ id: node.id,
4619
+ properties,
4620
+ placement,
4621
+ info: info2
4622
+ };
4623
+ if (groups.length > 0)
4624
+ object.groups = groups;
4625
+ if (epoch)
4626
+ object.epoch = epoch;
4627
+ if (referencePlane)
4628
+ object.referencePlane = referencePlane;
4629
+ if (tidalLock !== void 0)
4630
+ object.tidalLock = tidalLock;
4631
+ if (resonance)
4632
+ object.resonance = resonance;
4633
+ if (renderHints)
4634
+ object.renderHints = renderHints;
4635
+ if (deriveRules?.length)
4636
+ object.deriveRules = deriveRules;
4637
+ if (validationRules?.length)
4638
+ object.validationRules = validationRules;
4639
+ if (lockedFields?.length)
4640
+ object.lockedFields = lockedFields;
4641
+ if (tolerances?.length)
4642
+ object.tolerances = tolerances;
4643
+ if (typedBlocks && Object.keys(typedBlocks).length > 0)
4644
+ object.typedBlocks = typedBlocks;
4645
+ if (sourceSchemaVersion !== "2.1") {
4646
+ if (object.groups || object.epoch || object.referencePlane || object.tidalLock !== void 0 || object.resonance || object.renderHints || object.deriveRules?.length || object.validationRules?.length || object.lockedFields?.length || object.tolerances?.length || object.typedBlocks) {
4647
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
4648
+ }
4649
+ }
4650
+ return object;
3614
4651
  }
3615
- function humanizeIdentifier3(value) {
3616
- return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
4652
+ function collectDraftFields(fields) {
4653
+ const grouped = /* @__PURE__ */ new Map();
4654
+ for (const field of fields) {
4655
+ const spec = getDraftObjectFieldSpec(field.key);
4656
+ if (!spec) {
4657
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
4658
+ }
4659
+ if (!spec.allowRepeat && grouped.has(field.key)) {
4660
+ throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
4661
+ }
4662
+ const existing = grouped.get(field.key) ?? [];
4663
+ existing.push(field);
4664
+ grouped.set(field.key, existing);
4665
+ }
4666
+ return grouped;
4667
+ }
4668
+ function extractDraftPlacement(objectType, fieldMap) {
4669
+ const orbitField = fieldMap.get("orbit")?.[0];
4670
+ const atField = fieldMap.get("at")?.[0];
4671
+ const surfaceField = fieldMap.get("surface")?.[0];
4672
+ const freeField = fieldMap.get("free")?.[0];
4673
+ const count = [orbitField, atField, surfaceField, freeField].filter(Boolean).length;
4674
+ if (count > 1) {
4675
+ const conflictingField = orbitField ?? atField ?? surfaceField ?? freeField;
4676
+ throw WorldOrbitError.fromLocation("Object has multiple placement modes", conflictingField?.location);
4677
+ }
4678
+ if (orbitField) {
4679
+ return {
4680
+ mode: "orbit",
4681
+ target: singleFieldValue2(orbitField),
4682
+ distance: parseOptionalUnitField(fieldMap.get("distance")?.[0], "distance"),
4683
+ semiMajor: parseOptionalUnitField(fieldMap.get("semiMajor")?.[0], "semiMajor"),
4684
+ eccentricity: parseOptionalNumberField(fieldMap.get("eccentricity")?.[0], "eccentricity"),
4685
+ period: parseOptionalUnitField(fieldMap.get("period")?.[0], "period"),
4686
+ angle: parseOptionalUnitField(fieldMap.get("angle")?.[0], "angle"),
4687
+ inclination: parseOptionalUnitField(fieldMap.get("inclination")?.[0], "inclination"),
4688
+ phase: parseOptionalUnitField(fieldMap.get("phase")?.[0], "phase")
4689
+ };
4690
+ }
4691
+ if (atField) {
4692
+ const target = singleFieldValue2(atField);
4693
+ return {
4694
+ mode: "at",
4695
+ target,
4696
+ reference: parseAtlasAtReference(target, atField.location)
4697
+ };
4698
+ }
4699
+ if (surfaceField) {
4700
+ return {
4701
+ mode: "surface",
4702
+ target: singleFieldValue2(surfaceField)
4703
+ };
4704
+ }
4705
+ if (freeField) {
4706
+ const raw = singleFieldValue2(freeField);
4707
+ const distance = tryParseAtlasUnitValue(raw);
4708
+ return {
4709
+ mode: "free",
4710
+ distance: distance ?? void 0,
4711
+ descriptor: distance ? void 0 : raw
4712
+ };
4713
+ }
4714
+ return null;
4715
+ }
4716
+ function normalizeDraftProperties(objectType, fieldMap) {
4717
+ const properties = {};
4718
+ for (const [key, fields] of fieldMap.entries()) {
4719
+ const field = fields[0];
4720
+ const spec = getDraftObjectFieldSpec(key);
4721
+ if (!field || !spec?.legacySchema || spec.legacySchema.placement) {
4722
+ continue;
4723
+ }
4724
+ ensureAtlasFieldSupported(key, objectType, field.location);
4725
+ properties[key] = normalizeLegacyScalarValue(key, field.values, field.location);
4726
+ }
4727
+ return properties;
4728
+ }
4729
+ function normalizeInfoEntries(entries, label) {
4730
+ const normalized = {};
4731
+ for (const entry of entries) {
4732
+ if (entry.key in normalized) {
4733
+ throw WorldOrbitError.fromLocation(`Duplicate ${label} key "${entry.key}"`, entry.location);
4734
+ }
4735
+ normalized[entry.key] = entry.value;
4736
+ }
4737
+ return normalized;
4738
+ }
4739
+ function normalizeTypedBlocks(typedBlockEntries) {
4740
+ const typedBlocks = {};
4741
+ for (const blockName of Object.keys(typedBlockEntries)) {
4742
+ const entries = typedBlockEntries[blockName];
4743
+ if (entries?.length) {
4744
+ typedBlocks[blockName] = normalizeInfoEntries(entries, blockName);
4745
+ }
4746
+ }
4747
+ return typedBlocks;
4748
+ }
4749
+ function extractRenderHints(fieldMap) {
4750
+ const renderHints = {};
4751
+ const renderLabelField = fieldMap.get("renderLabel")?.[0];
4752
+ const renderOrbitField = fieldMap.get("renderOrbit")?.[0];
4753
+ const renderPriorityField = fieldMap.get("renderPriority")?.[0];
4754
+ if (renderLabelField) {
4755
+ renderHints.renderLabel = parseAtlasBoolean(singleFieldValue2(renderLabelField), "renderLabel", renderLabelField.location);
4756
+ }
4757
+ if (renderOrbitField) {
4758
+ renderHints.renderOrbit = parseAtlasBoolean(singleFieldValue2(renderOrbitField), "renderOrbit", renderOrbitField.location);
4759
+ }
4760
+ if (renderPriorityField) {
4761
+ renderHints.renderPriority = parseAtlasNumber(singleFieldValue2(renderPriorityField), "renderPriority", renderPriorityField.location);
4762
+ }
4763
+ return Object.keys(renderHints).length > 0 ? renderHints : void 0;
4764
+ }
4765
+ function parseResonanceField(field) {
4766
+ if (field.values.length !== 2) {
4767
+ throw WorldOrbitError.fromLocation('Field "resonance" expects "<targetObjectId> <ratio>"', field.location);
4768
+ }
4769
+ const ratio = field.values[1];
4770
+ if (!/^\d+:\d+$/.test(ratio)) {
4771
+ throw WorldOrbitError.fromLocation(`Invalid resonance ratio "${ratio}"`, field.location);
4772
+ }
4773
+ return {
4774
+ targetObjectId: field.values[0],
4775
+ ratio
4776
+ };
4777
+ }
4778
+ function parseDeriveField(field) {
4779
+ if (field.values.length !== 2) {
4780
+ throw WorldOrbitError.fromLocation('Field "derive" expects "<field> <strategy>"', field.location);
4781
+ }
4782
+ return {
4783
+ field: field.values[0],
4784
+ strategy: field.values[1]
4785
+ };
4786
+ }
4787
+ function parseToleranceField(field) {
4788
+ if (field.values.length !== 2) {
4789
+ throw WorldOrbitError.fromLocation('Field "tolerance" expects "<field> <value>"', field.location);
4790
+ }
4791
+ const rawValue = field.values[1];
4792
+ const unitValue = tryParseAtlasUnitValue(rawValue);
4793
+ const numericValue = Number(rawValue);
4794
+ return {
4795
+ field: field.values[0],
4796
+ value: unitValue ?? (Number.isFinite(numericValue) ? numericValue : rawValue)
4797
+ };
4798
+ }
4799
+ function parseOptionalTokenList(field) {
4800
+ return field ? [...new Set(field.values)] : [];
4801
+ }
4802
+ function parseOptionalJoinedValue(field) {
4803
+ if (!field) {
4804
+ return null;
4805
+ }
4806
+ return field.values.join(" ").trim() || null;
4807
+ }
4808
+ function parseOptionalUnitField(field, key) {
4809
+ return field ? parseAtlasUnitValue(singleFieldValue2(field), field.location, key) : void 0;
4810
+ }
4811
+ function parseOptionalNumberField(field, key) {
4812
+ return field ? parseAtlasNumber(singleFieldValue2(field), key, field.location) : void 0;
4813
+ }
4814
+ function singleFieldValue2(field) {
4815
+ return singleAtlasValue(field.values, field.key, field.location);
4816
+ }
4817
+ function getDraftObjectFieldSpec(key) {
4818
+ return DRAFT_OBJECT_FIELD_SPECS.get(key);
4819
+ }
4820
+ function validateDraftObjectFieldCompatibility(fields, objectType) {
4821
+ for (const field of fields) {
4822
+ const spec = getDraftObjectFieldSpec(field.key);
4823
+ if (!spec) {
4824
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
4825
+ }
4826
+ if (spec.legacySchema) {
4827
+ ensureAtlasFieldSupported(field.key, objectType, field.location);
4828
+ continue;
4829
+ }
4830
+ if ((field.key === "renderLabel" || field.key === "renderOrbit" || field.key === "tidalLock") && field.values.length !== 1) {
4831
+ throw WorldOrbitError.fromLocation(`Field "${field.key}" expects exactly one value`, field.location);
4832
+ }
4833
+ }
4834
+ }
4835
+ function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
4836
+ if (sourceSchemaVersion === "2.1") {
4837
+ return;
4838
+ }
4839
+ diagnostics.push({
4840
+ code: "parse.schema21.featureCompatibility",
4841
+ severity: "warning",
4842
+ source: "parse",
4843
+ message: `Feature "${featureName}" requires schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
4844
+ line: location.line,
4845
+ column: location.column
4846
+ });
4847
+ }
4848
+ function preprocessAtlasSource(source) {
4849
+ const chars = [...source];
4850
+ const comments = [];
4851
+ let inString = false;
4852
+ let inBlockComment = false;
4853
+ let blockCommentStart = null;
4854
+ let line = 1;
4855
+ let column = 1;
4856
+ for (let index = 0; index < chars.length; index++) {
4857
+ const ch = chars[index];
4858
+ const next = chars[index + 1];
4859
+ if (inBlockComment) {
4860
+ if (ch === "*" && next === "/") {
4861
+ chars[index] = " ";
4862
+ chars[index + 1] = " ";
4863
+ inBlockComment = false;
4864
+ blockCommentStart = null;
4865
+ index++;
4866
+ column += 2;
4867
+ continue;
4868
+ }
4869
+ if (ch !== "\n" && ch !== "\r") {
4870
+ chars[index] = " ";
4871
+ }
4872
+ if (ch === "\n") {
4873
+ line++;
4874
+ column = 1;
4875
+ } else {
4876
+ column++;
4877
+ }
4878
+ continue;
4879
+ }
4880
+ if (!inString && ch === "/" && next === "*") {
4881
+ comments.push({ kind: "block", line, column });
4882
+ chars[index] = " ";
4883
+ chars[index + 1] = " ";
4884
+ inBlockComment = true;
4885
+ blockCommentStart = { line, column };
4886
+ index++;
4887
+ column += 2;
4888
+ continue;
4889
+ }
4890
+ if (!inString && ch === "#" && !isHexColorLiteral(chars, index)) {
4891
+ comments.push({ kind: "line", line, column });
4892
+ chars[index] = " ";
4893
+ let inner = index + 1;
4894
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
4895
+ chars[inner] = " ";
4896
+ inner++;
4897
+ }
4898
+ column += inner - index;
4899
+ index = inner - 1;
4900
+ continue;
4901
+ }
4902
+ if (ch === '"' && chars[index - 1] !== "\\") {
4903
+ inString = !inString;
4904
+ }
4905
+ if (ch === "\n") {
4906
+ line++;
4907
+ column = 1;
4908
+ } else {
4909
+ column++;
4910
+ }
4911
+ }
4912
+ if (inBlockComment) {
4913
+ throw WorldOrbitError.fromLocation("Unclosed block comment", blockCommentStart ?? void 0);
4914
+ }
4915
+ return {
4916
+ source: chars.join(""),
4917
+ comments
4918
+ };
4919
+ }
4920
+ function isHexColorLiteral(chars, start) {
4921
+ let index = start + 1;
4922
+ let length = 0;
4923
+ while (index < chars.length && /[0-9a-f]/i.test(chars[index] ?? "")) {
4924
+ index++;
4925
+ length++;
4926
+ }
4927
+ if (![3, 4, 6, 8].includes(length)) {
4928
+ return false;
4929
+ }
4930
+ const next = chars[index];
4931
+ return next === void 0 || next === " " || next === " " || next === "\r" || next === "\n";
3617
4932
  }
3618
4933
 
3619
4934
  // packages/core/dist/atlas-edit.js
3620
- function createEmptyAtlasDocument(systemId = "WorldOrbit") {
4935
+ function createEmptyAtlasDocument(systemId = "WorldOrbit", version = "2.0") {
3621
4936
  return {
3622
4937
  format: "worldorbit",
3623
- version: "2.0",
4938
+ version,
4939
+ schemaVersion: version,
3624
4940
  sourceVersion: "1.0",
3625
4941
  system: {
3626
4942
  type: "system",
3627
4943
  id: systemId,
3628
4944
  title: systemId,
4945
+ description: null,
4946
+ epoch: null,
4947
+ referencePlane: null,
3629
4948
  defaults: {
3630
4949
  view: "topdown",
3631
4950
  scale: null,
@@ -3637,6 +4956,8 @@
3637
4956
  viewpoints: [],
3638
4957
  annotations: []
3639
4958
  },
4959
+ groups: [],
4960
+ relations: [],
3640
4961
  objects: [],
3641
4962
  diagnostics: []
3642
4963
  };
@@ -3650,14 +4971,20 @@
3650
4971
  for (const key of Object.keys(document.system.atlasMetadata).sort()) {
3651
4972
  paths.push({ kind: "metadata", key });
3652
4973
  }
3653
- for (const viewpoint of [...document.system.viewpoints].sort(compareIdLike)) {
4974
+ for (const viewpoint of [...document.system.viewpoints].sort(compareIdLike2)) {
3654
4975
  paths.push({ kind: "viewpoint", id: viewpoint.id });
3655
4976
  }
3656
- for (const annotation of [...document.system.annotations].sort(compareIdLike)) {
4977
+ for (const annotation of [...document.system.annotations].sort(compareIdLike2)) {
3657
4978
  paths.push({ kind: "annotation", id: annotation.id });
3658
4979
  }
3659
4980
  }
3660
- for (const object of [...document.objects].sort(compareIdLike)) {
4981
+ for (const group of [...document.groups].sort(compareIdLike2)) {
4982
+ paths.push({ kind: "group", id: group.id });
4983
+ }
4984
+ for (const relation of [...document.relations].sort(compareIdLike2)) {
4985
+ paths.push({ kind: "relation", id: relation.id });
4986
+ }
4987
+ for (const object of [...document.objects].sort(compareIdLike2)) {
3661
4988
  paths.push({ kind: "object", id: object.id });
3662
4989
  }
3663
4990
  return paths;
@@ -3670,12 +4997,16 @@
3670
4997
  return document.system?.defaults ?? null;
3671
4998
  case "metadata":
3672
4999
  return path.key ? document.system?.atlasMetadata[path.key] ?? null : null;
5000
+ case "group":
5001
+ return path.id ? findGroup(document, path.id) : null;
3673
5002
  case "object":
3674
5003
  return path.id ? findObject(document, path.id) : null;
3675
5004
  case "viewpoint":
3676
5005
  return path.id ? findViewpoint(document.system, path.id) : null;
3677
5006
  case "annotation":
3678
5007
  return path.id ? findAnnotation(document.system, path.id) : null;
5008
+ case "relation":
5009
+ return path.id ? findRelation(document, path.id) : null;
3679
5010
  }
3680
5011
  }
3681
5012
  function upsertAtlasDocumentNode(document, path, value) {
@@ -3701,6 +5032,12 @@
3701
5032
  system.atlasMetadata[path.key] = String(value);
3702
5033
  }
3703
5034
  return next;
5035
+ case "group":
5036
+ if (!path.id) {
5037
+ throw new Error('Group updates require an "id" value.');
5038
+ }
5039
+ upsertById(next.groups, value);
5040
+ return next;
3704
5041
  case "object":
3705
5042
  if (!path.id) {
3706
5043
  throw new Error('Object updates require an "id" value.');
@@ -3719,6 +5056,12 @@
3719
5056
  }
3720
5057
  upsertById(system.annotations, value);
3721
5058
  return next;
5059
+ case "relation":
5060
+ if (!path.id) {
5061
+ throw new Error('Relation updates require an "id" value.');
5062
+ }
5063
+ upsertById(next.relations, value);
5064
+ return next;
3722
5065
  }
3723
5066
  }
3724
5067
  function updateAtlasDocumentNode(document, path, updater) {
@@ -3738,6 +5081,11 @@
3738
5081
  next.objects = next.objects.filter((object) => object.id !== path.id);
3739
5082
  }
3740
5083
  return next;
5084
+ case "group":
5085
+ if (path.id) {
5086
+ next.groups = next.groups.filter((group) => group.id !== path.id);
5087
+ }
5088
+ return next;
3741
5089
  case "viewpoint":
3742
5090
  if (path.id) {
3743
5091
  system.viewpoints = system.viewpoints.filter((viewpoint) => viewpoint.id !== path.id);
@@ -3748,6 +5096,11 @@
3748
5096
  system.annotations = system.annotations.filter((annotation) => annotation.id !== path.id);
3749
5097
  }
3750
5098
  return next;
5099
+ case "relation":
5100
+ if (path.id) {
5101
+ next.relations = next.relations.filter((relation) => relation.id !== path.id);
5102
+ }
5103
+ return next;
3751
5104
  default:
3752
5105
  return next;
3753
5106
  }
@@ -3765,6 +5118,15 @@
3765
5118
  id: diagnostic.objectId
3766
5119
  };
3767
5120
  }
5121
+ if (diagnostic.field?.startsWith("group.")) {
5122
+ const parts = diagnostic.field.split(".");
5123
+ if (parts[1] && findGroup(document, parts[1])) {
5124
+ return {
5125
+ kind: "group",
5126
+ id: parts[1]
5127
+ };
5128
+ }
5129
+ }
3768
5130
  if (diagnostic.field?.startsWith("viewpoint.")) {
3769
5131
  const parts = diagnostic.field.split(".");
3770
5132
  if (parts[1] && findViewpoint(document.system, parts[1])) {
@@ -3783,6 +5145,15 @@
3783
5145
  };
3784
5146
  }
3785
5147
  }
5148
+ if (diagnostic.field?.startsWith("relation.")) {
5149
+ const parts = diagnostic.field.split(".");
5150
+ if (parts[1] && findRelation(document, parts[1])) {
5151
+ return {
5152
+ kind: "relation",
5153
+ id: parts[1]
5154
+ };
5155
+ }
5156
+ }
3786
5157
  if (diagnostic.field && diagnostic.field in ensureSystem(document).atlasMetadata) {
3787
5158
  return {
3788
5159
  kind: "metadata",
@@ -3792,9 +5163,11 @@
3792
5163
  return null;
3793
5164
  }
3794
5165
  function validateAtlasDocumentWithDiagnostics(document) {
3795
- const materialized = materializeAtlasDocument(document);
3796
- const result = validateDocumentWithDiagnostics(materialized);
3797
- return resolveAtlasDiagnostics(document, result.diagnostics);
5166
+ const diagnostics = [
5167
+ ...document.diagnostics,
5168
+ ...collectAtlasDiagnostics(document, document.version)
5169
+ ];
5170
+ return resolveAtlasDiagnostics(document, diagnostics);
3798
5171
  }
3799
5172
  function ensureSystem(document) {
3800
5173
  if (document.system) {
@@ -3806,6 +5179,12 @@
3806
5179
  function findObject(document, objectId) {
3807
5180
  return document.objects.find((object) => object.id === objectId) ?? null;
3808
5181
  }
5182
+ function findGroup(document, groupId) {
5183
+ return document.groups.find((group) => group.id === groupId) ?? null;
5184
+ }
5185
+ function findRelation(document, relationId) {
5186
+ return document.relations.find((relation) => relation.id === relationId) ?? null;
5187
+ }
3809
5188
  function findViewpoint(system, viewpointId) {
3810
5189
  return system?.viewpoints.find((viewpoint) => viewpoint.id === viewpointId) ?? null;
3811
5190
  }
@@ -3816,20 +5195,21 @@
3816
5195
  const index = items.findIndex((item) => item.id === value.id);
3817
5196
  if (index === -1) {
3818
5197
  items.push(value);
3819
- items.sort(compareIdLike);
5198
+ items.sort(compareIdLike2);
3820
5199
  return;
3821
5200
  }
3822
5201
  items[index] = value;
3823
5202
  }
3824
- function compareIdLike(left, right) {
5203
+ function compareIdLike2(left, right) {
3825
5204
  return left.id.localeCompare(right.id);
3826
5205
  }
3827
5206
 
3828
5207
  // packages/core/dist/load.js
3829
- var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0)?$/i;
5208
+ var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1)?$/i;
5209
+ var ATLAS_SCHEMA_21_PATTERN = /^schema\s+2\.1$/i;
3830
5210
  var LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
3831
5211
  function detectWorldOrbitSchemaVersion(source) {
3832
- for (const line of source.split(/\r?\n/)) {
5212
+ for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
3833
5213
  const trimmed = line.trim();
3834
5214
  if (!trimmed) {
3835
5215
  continue;
@@ -3837,6 +5217,9 @@
3837
5217
  if (LEGACY_DRAFT_SCHEMA_PATTERN.test(trimmed)) {
3838
5218
  return "2.0-draft";
3839
5219
  }
5220
+ if (ATLAS_SCHEMA_21_PATTERN.test(trimmed)) {
5221
+ return "2.1";
5222
+ }
3840
5223
  if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
3841
5224
  return "2.0";
3842
5225
  }
@@ -3844,6 +5227,49 @@
3844
5227
  }
3845
5228
  return "1.0";
3846
5229
  }
5230
+ function stripCommentsForSchemaDetection(source) {
5231
+ const chars = [...source];
5232
+ let inString = false;
5233
+ let inBlockComment = false;
5234
+ for (let index = 0; index < chars.length; index++) {
5235
+ const ch = chars[index];
5236
+ const next = chars[index + 1];
5237
+ if (inBlockComment) {
5238
+ if (ch === "*" && next === "/") {
5239
+ chars[index] = " ";
5240
+ chars[index + 1] = " ";
5241
+ inBlockComment = false;
5242
+ index++;
5243
+ continue;
5244
+ }
5245
+ if (ch !== "\n" && ch !== "\r") {
5246
+ chars[index] = " ";
5247
+ }
5248
+ continue;
5249
+ }
5250
+ if (!inString && ch === "/" && next === "*") {
5251
+ chars[index] = " ";
5252
+ chars[index + 1] = " ";
5253
+ inBlockComment = true;
5254
+ index++;
5255
+ continue;
5256
+ }
5257
+ if (!inString && ch === "#") {
5258
+ chars[index] = " ";
5259
+ let inner = index + 1;
5260
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
5261
+ chars[inner] = " ";
5262
+ inner++;
5263
+ }
5264
+ index = inner - 1;
5265
+ continue;
5266
+ }
5267
+ if (ch === '"' && chars[index - 1] !== "\\") {
5268
+ inString = !inString;
5269
+ }
5270
+ }
5271
+ return chars.join("");
5272
+ }
3847
5273
  function loadWorldOrbitSource(source) {
3848
5274
  const result = loadWorldOrbitSourceWithDiagnostics(source);
3849
5275
  if (!result.ok || !result.value) {
@@ -3854,36 +5280,36 @@
3854
5280
  }
3855
5281
  function loadWorldOrbitSourceWithDiagnostics(source) {
3856
5282
  const schemaVersion = detectWorldOrbitSchemaVersion(source);
3857
- if (schemaVersion === "2.0" || schemaVersion === "2.0-draft") {
5283
+ if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1") {
3858
5284
  return loadAtlasSourceWithDiagnostics(source, schemaVersion);
3859
5285
  }
3860
5286
  let ast;
3861
5287
  try {
3862
5288
  ast = parseWorldOrbit(source);
3863
- } catch (error) {
5289
+ } catch (error2) {
3864
5290
  return {
3865
5291
  ok: false,
3866
5292
  value: null,
3867
- diagnostics: [diagnosticFromError(error, "parse")]
5293
+ diagnostics: [diagnosticFromError(error2, "parse")]
3868
5294
  };
3869
5295
  }
3870
5296
  let document;
3871
5297
  try {
3872
5298
  document = normalizeDocument(ast);
3873
- } catch (error) {
5299
+ } catch (error2) {
3874
5300
  return {
3875
5301
  ok: false,
3876
5302
  value: null,
3877
- diagnostics: [diagnosticFromError(error, "normalize")]
5303
+ diagnostics: [diagnosticFromError(error2, "normalize")]
3878
5304
  };
3879
5305
  }
3880
5306
  try {
3881
5307
  validateDocument(document);
3882
- } catch (error) {
5308
+ } catch (error2) {
3883
5309
  return {
3884
5310
  ok: false,
3885
5311
  value: null,
3886
- diagnostics: [diagnosticFromError(error, "validate")]
5312
+ diagnostics: [diagnosticFromError(error2, "validate")]
3887
5313
  };
3888
5314
  }
3889
5315
  return {
@@ -3903,30 +5329,29 @@
3903
5329
  let atlasDocument;
3904
5330
  try {
3905
5331
  atlasDocument = parseWorldOrbitAtlas(source);
3906
- } catch (error) {
5332
+ } catch (error2) {
3907
5333
  return {
3908
5334
  ok: false,
3909
5335
  value: null,
3910
- diagnostics: [diagnosticFromError(error, "parse", "load.atlas.failed")]
5336
+ diagnostics: [diagnosticFromError(error2, "parse", "load.atlas.failed")]
3911
5337
  };
3912
5338
  }
3913
- let document;
3914
- try {
3915
- document = materializeAtlasDocument(atlasDocument);
3916
- } catch (error) {
5339
+ const atlasDiagnostics = [...atlasDocument.diagnostics];
5340
+ if (atlasDiagnostics.some((diagnostic) => diagnostic.severity === "error")) {
3917
5341
  return {
3918
5342
  ok: false,
3919
5343
  value: null,
3920
- diagnostics: [diagnosticFromError(error, "normalize", "load.atlas.materialize.failed")]
5344
+ diagnostics: atlasDiagnostics
3921
5345
  };
3922
5346
  }
5347
+ let document;
3923
5348
  try {
3924
- validateDocument(document);
3925
- } catch (error) {
5349
+ document = materializeAtlasDocument(atlasDocument);
5350
+ } catch (error2) {
3926
5351
  return {
3927
5352
  ok: false,
3928
5353
  value: null,
3929
- diagnostics: [diagnosticFromError(error, "validate", "load.atlas.validate.failed")]
5354
+ diagnostics: [diagnosticFromError(error2, "normalize", "load.atlas.materialize.failed")]
3930
5355
  };
3931
5356
  }
3932
5357
  const loaded = {
@@ -3935,12 +5360,12 @@
3935
5360
  document,
3936
5361
  atlasDocument,
3937
5362
  draftDocument: atlasDocument,
3938
- diagnostics: [...atlasDocument.diagnostics]
5363
+ diagnostics: atlasDiagnostics
3939
5364
  };
3940
5365
  return {
3941
5366
  ok: true,
3942
5367
  value: loaded,
3943
- diagnostics: [...atlasDocument.diagnostics]
5368
+ diagnostics: atlasDiagnostics
3944
5369
  };
3945
5370
  }
3946
5371