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.
@@ -19,8 +19,8 @@ var WorldOrbit = (() => {
19
19
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
20
 
21
21
  // packages/core/dist/index.js
22
- var dist_exports = {};
23
- __export(dist_exports, {
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
24
  WORLDORBIT_FIELD_KEYS: () => WORLDORBIT_FIELD_KEYS,
25
25
  WORLDORBIT_FIELD_SCHEMAS: () => WORLDORBIT_FIELD_SCHEMAS,
26
26
  WORLDORBIT_OBJECT_TYPES: () => WORLDORBIT_OBJECT_TYPES,
@@ -373,13 +373,13 @@ var WorldOrbit = (() => {
373
373
  function unitFamilyAllowsUnit(family, unit) {
374
374
  switch (family) {
375
375
  case "distance":
376
- return unit === null || ["au", "km", "re", "sol"].includes(unit);
376
+ return unit === null || ["au", "km", "m", "ly", "pc", "kpc", "re", "sol"].includes(unit);
377
377
  case "radius":
378
- return unit === null || ["km", "re", "sol"].includes(unit);
378
+ return unit === null || ["km", "m", "re", "rj", "sol"].includes(unit);
379
379
  case "mass":
380
- return unit === null || ["me", "sol"].includes(unit);
380
+ return unit === null || ["me", "mj", "sol"].includes(unit);
381
381
  case "duration":
382
- return unit === null || ["h", "d", "y"].includes(unit);
382
+ return unit === null || ["s", "min", "h", "d", "y", "ky", "my", "gy"].includes(unit);
383
383
  case "angle":
384
384
  return unit === null || unit === "deg";
385
385
  case "generic":
@@ -586,7 +586,7 @@ var WorldOrbit = (() => {
586
586
  }
587
587
 
588
588
  // packages/core/dist/normalize.js
589
- var UNIT_PATTERN = /^(-?\d+(?:\.\d+)?)(au|km|re|sol|me|d|y|h|deg)?$/;
589
+ 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)?$/;
590
590
  var BOOLEAN_VALUES = /* @__PURE__ */ new Map([
591
591
  ["true", true],
592
592
  ["false", false],
@@ -611,7 +611,10 @@ var WorldOrbit = (() => {
611
611
  return {
612
612
  format: "worldorbit",
613
613
  version: "1.0",
614
+ schemaVersion: "1.0",
614
615
  system,
616
+ groups: [],
617
+ relations: [],
615
618
  objects
616
619
  };
617
620
  }
@@ -621,13 +624,17 @@ var WorldOrbit = (() => {
621
624
  const fieldMap = collectFields(mergedFields);
622
625
  const placement = extractPlacement(node.objectType, fieldMap);
623
626
  const properties = normalizeProperties(fieldMap);
624
- const info = normalizeInfo(node.infoEntries);
627
+ const info2 = normalizeInfo(node.infoEntries);
625
628
  if (node.objectType === "system") {
626
629
  return {
627
630
  type: "system",
628
631
  id: node.name,
632
+ title: typeof properties.title === "string" ? properties.title : null,
633
+ description: null,
634
+ epoch: null,
635
+ referencePlane: null,
629
636
  properties,
630
- info
637
+ info: info2
631
638
  };
632
639
  }
633
640
  return {
@@ -635,7 +642,7 @@ var WorldOrbit = (() => {
635
642
  id: node.name,
636
643
  properties,
637
644
  placement,
638
- info
645
+ info: info2
639
646
  };
640
647
  }
641
648
  function validateFieldCompatibility(objectType, fields) {
@@ -765,14 +772,14 @@ var WorldOrbit = (() => {
765
772
  }
766
773
  }
767
774
  function normalizeInfo(entries) {
768
- const info = {};
775
+ const info2 = {};
769
776
  for (const entry of entries) {
770
- if (entry.key in info) {
777
+ if (entry.key in info2) {
771
778
  throw WorldOrbitError.fromLocation(`Duplicate info key "${entry.key}"`, entry.location);
772
779
  }
773
- info[entry.key] = entry.value;
780
+ info2[entry.key] = entry.value;
774
781
  }
775
- return info;
782
+ return info2;
776
783
  }
777
784
  function parseAtReference(target, location) {
778
785
  if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
@@ -946,38 +953,38 @@ var WorldOrbit = (() => {
946
953
  function createDiagnostic(diagnostic) {
947
954
  return { ...diagnostic };
948
955
  }
949
- function diagnosticFromError(error, source, code = `${source}.failed`) {
950
- if (error instanceof WorldOrbitError) {
956
+ function diagnosticFromError(error2, source, code = `${source}.failed`) {
957
+ if (error2 instanceof WorldOrbitError) {
951
958
  return {
952
959
  code,
953
960
  severity: "error",
954
961
  source,
955
- message: error.message,
956
- line: error.line,
957
- column: error.column
962
+ message: error2.message,
963
+ line: error2.line,
964
+ column: error2.column
958
965
  };
959
966
  }
960
- if (error instanceof Error) {
967
+ if (error2 instanceof Error) {
961
968
  return {
962
969
  code,
963
970
  severity: "error",
964
971
  source,
965
- message: error.message
972
+ message: error2.message
966
973
  };
967
974
  }
968
975
  return {
969
976
  code,
970
977
  severity: "error",
971
978
  source,
972
- message: String(error)
979
+ message: String(error2)
973
980
  };
974
981
  }
975
982
  function parseWithDiagnostics(source) {
976
983
  let ast;
977
984
  try {
978
985
  ast = parseWorldOrbit(source);
979
- } catch (error) {
980
- const diagnostic = diagnosticFromError(error, "parse");
986
+ } catch (error2) {
987
+ const diagnostic = diagnosticFromError(error2, "parse");
981
988
  return {
982
989
  ok: false,
983
990
  value: null,
@@ -987,20 +994,20 @@ var WorldOrbit = (() => {
987
994
  let document;
988
995
  try {
989
996
  document = normalizeDocument(ast);
990
- } catch (error) {
997
+ } catch (error2) {
991
998
  return {
992
999
  ok: false,
993
1000
  value: null,
994
- diagnostics: [diagnosticFromError(error, "normalize")]
1001
+ diagnostics: [diagnosticFromError(error2, "normalize")]
995
1002
  };
996
1003
  }
997
1004
  try {
998
1005
  validateDocument(document);
999
- } catch (error) {
1006
+ } catch (error2) {
1000
1007
  return {
1001
1008
  ok: false,
1002
1009
  value: null,
1003
- diagnostics: [diagnosticFromError(error, "validate")]
1010
+ diagnostics: [diagnosticFromError(error2, "validate")]
1004
1011
  };
1005
1012
  }
1006
1013
  return {
@@ -1019,11 +1026,11 @@ var WorldOrbit = (() => {
1019
1026
  value: normalizeDocument(ast),
1020
1027
  diagnostics: []
1021
1028
  };
1022
- } catch (error) {
1029
+ } catch (error2) {
1023
1030
  return {
1024
1031
  ok: false,
1025
1032
  value: null,
1026
- diagnostics: [diagnosticFromError(error, "normalize")]
1033
+ diagnostics: [diagnosticFromError(error2, "normalize")]
1027
1034
  };
1028
1035
  }
1029
1036
  }
@@ -1035,11 +1042,11 @@ var WorldOrbit = (() => {
1035
1042
  value: document,
1036
1043
  diagnostics: []
1037
1044
  };
1038
- } catch (error) {
1045
+ } catch (error2) {
1039
1046
  return {
1040
1047
  ok: false,
1041
1048
  value: null,
1042
- diagnostics: [diagnosticFromError(error, "validate")]
1049
+ diagnostics: [diagnosticFromError(error2, "validate")]
1043
1050
  };
1044
1051
  }
1045
1052
  }
@@ -1047,7 +1054,11 @@ var WorldOrbit = (() => {
1047
1054
  // packages/core/dist/scene.js
1048
1055
  var AU_IN_KM = 1495978707e-1;
1049
1056
  var EARTH_RADIUS_IN_KM = 6371;
1057
+ var JUPITER_RADIUS_IN_KM = 71492;
1050
1058
  var SOLAR_RADIUS_IN_KM = 695700;
1059
+ var LY_IN_AU = 63241.077;
1060
+ var PC_IN_AU = 206264.806;
1061
+ var KPC_IN_AU = 206264806;
1051
1062
  var ISO_FLATTENING = 0.68;
1052
1063
  var MIN_ISO_MINOR_SCALE = 0.2;
1053
1064
  var ARC_SAMPLE_COUNT = 28;
@@ -1167,8 +1178,10 @@ var WorldOrbit = (() => {
1167
1178
  const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
1168
1179
  const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
1169
1180
  const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
1170
- const layers = createSceneLayers(orbitVisuals, leaders, objects, labels);
1181
+ const relations = createSceneRelations(document, objects);
1182
+ const layers = createSceneLayers(orbitVisuals, relations, leaders, objects, labels);
1171
1183
  const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
1184
+ const semanticGroups = createSceneSemanticGroups(document, objects);
1172
1185
  const viewpoints = createSceneViewpoints(document, projection, frame.preset, relationships, objectMap);
1173
1186
  const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
1174
1187
  return {
@@ -1178,7 +1191,7 @@ var WorldOrbit = (() => {
1178
1191
  renderPreset: frame.preset,
1179
1192
  projection,
1180
1193
  scaleModel,
1181
- title: String(document.system?.properties.title ?? document.system?.id ?? "WorldOrbit") || "WorldOrbit",
1194
+ title: String(document.system?.title ?? document.system?.properties.title ?? document.system?.id ?? "WorldOrbit") || "WorldOrbit",
1182
1195
  subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
1183
1196
  systemId,
1184
1197
  viewMode: projection,
@@ -1194,9 +1207,11 @@ var WorldOrbit = (() => {
1194
1207
  contentBounds,
1195
1208
  layers,
1196
1209
  groups,
1210
+ semanticGroups,
1197
1211
  viewpoints,
1198
1212
  objects,
1199
1213
  orbitVisuals,
1214
+ relations,
1200
1215
  leaders,
1201
1216
  labels
1202
1217
  };
@@ -1306,6 +1321,7 @@ var WorldOrbit = (() => {
1306
1321
  }
1307
1322
  function createSceneObject(position, scaleModel, relationships) {
1308
1323
  const { object, x, y, radius, sortKey, anchorX, anchorY } = position;
1324
+ const renderPriority = object.renderHints?.renderPriority ?? 0;
1309
1325
  return {
1310
1326
  renderId: createRenderId(object.id),
1311
1327
  objectId: object.id,
@@ -1314,11 +1330,12 @@ var WorldOrbit = (() => {
1314
1330
  ancestorIds: relationships.ancestorIds.get(object.id) ?? [],
1315
1331
  childIds: relationships.childIds.get(object.id) ?? [],
1316
1332
  groupId: relationships.groupIds.get(object.id) ?? null,
1333
+ semanticGroupIds: [...object.groups ?? []],
1317
1334
  x,
1318
1335
  y,
1319
1336
  radius,
1320
1337
  visualRadius: visualExtentForObject(object, radius, scaleModel),
1321
- sortKey,
1338
+ sortKey: sortKey + renderPriority * 1e-3,
1322
1339
  anchorX,
1323
1340
  anchorY,
1324
1341
  label: object.id,
@@ -1335,6 +1352,7 @@ var WorldOrbit = (() => {
1335
1352
  object: draft.object,
1336
1353
  parentId: draft.parentId,
1337
1354
  groupId,
1355
+ semanticGroupIds: [...draft.object.groups ?? []],
1338
1356
  kind: draft.kind,
1339
1357
  cx: draft.cx,
1340
1358
  cy: draft.cy,
@@ -1346,7 +1364,7 @@ var WorldOrbit = (() => {
1346
1364
  bandThickness: draft.bandThickness,
1347
1365
  frontArcPath: draft.frontArcPath,
1348
1366
  backArcPath: draft.backArcPath,
1349
- hidden: draft.object.properties.hidden === true
1367
+ hidden: draft.object.properties.hidden === true || draft.object.renderHints?.renderOrbit === false
1350
1368
  };
1351
1369
  }
1352
1370
  function createLeaderLine(draft) {
@@ -1355,6 +1373,7 @@ var WorldOrbit = (() => {
1355
1373
  objectId: draft.object.id,
1356
1374
  object: draft.object,
1357
1375
  groupId: draft.groupId,
1376
+ semanticGroupIds: [...draft.object.groups ?? []],
1358
1377
  x1: draft.x1,
1359
1378
  y1: draft.y1,
1360
1379
  x2: draft.x2,
@@ -1366,7 +1385,7 @@ var WorldOrbit = (() => {
1366
1385
  function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1367
1386
  const labels = [];
1368
1387
  const occupied = [];
1369
- const visibleObjects = [...objects].filter((object) => !object.hidden).sort((left, right) => left.sortKey - right.sortKey);
1388
+ const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort((left, right) => left.sortKey - right.sortKey);
1370
1389
  for (const object of visibleObjects) {
1371
1390
  const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1372
1391
  const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
@@ -1386,6 +1405,7 @@ var WorldOrbit = (() => {
1386
1405
  objectId: object.objectId,
1387
1406
  object: object.object,
1388
1407
  groupId: object.groupId,
1408
+ semanticGroupIds: [...object.semanticGroupIds],
1389
1409
  label: object.label,
1390
1410
  secondaryLabel: object.secondaryLabel,
1391
1411
  x: object.x,
@@ -1398,7 +1418,7 @@ var WorldOrbit = (() => {
1398
1418
  }
1399
1419
  return labels;
1400
1420
  }
1401
- function createSceneLayers(orbitVisuals, leaders, objects, labels) {
1421
+ function createSceneLayers(orbitVisuals, relations, leaders, objects, labels) {
1402
1422
  const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1403
1423
  const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1404
1424
  return [
@@ -1409,6 +1429,10 @@ var WorldOrbit = (() => {
1409
1429
  },
1410
1430
  { id: "orbits-back", renderIds: backOrbitIds },
1411
1431
  { id: "orbits-front", renderIds: frontOrbitIds },
1432
+ {
1433
+ id: "relations",
1434
+ renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId)
1435
+ },
1412
1436
  {
1413
1437
  id: "objects",
1414
1438
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
@@ -1473,6 +1497,36 @@ var WorldOrbit = (() => {
1473
1497
  }
1474
1498
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1475
1499
  }
1500
+ function createSceneSemanticGroups(document, objects) {
1501
+ return [...document.groups].map((group) => ({
1502
+ id: group.id,
1503
+ label: group.label,
1504
+ summary: group.summary,
1505
+ color: group.color,
1506
+ tags: [...group.tags],
1507
+ hidden: group.hidden,
1508
+ objectIds: objects.filter((object) => !object.hidden && object.semanticGroupIds.includes(group.id)).map((object) => object.objectId)
1509
+ })).sort((left, right) => left.label.localeCompare(right.label));
1510
+ }
1511
+ function createSceneRelations(document, objects) {
1512
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1513
+ return document.relations.map((relation) => {
1514
+ const from = objectMap.get(relation.from);
1515
+ const to = objectMap.get(relation.to);
1516
+ return {
1517
+ renderId: `${createRenderId(relation.id)}-relation`,
1518
+ relationId: relation.id,
1519
+ relation,
1520
+ fromObjectId: relation.from,
1521
+ toObjectId: relation.to,
1522
+ x1: from?.x ?? 0,
1523
+ y1: from?.y ?? 0,
1524
+ x2: to?.x ?? 0,
1525
+ y2: to?.y ?? 0,
1526
+ hidden: relation.hidden || !from || !to || from.hidden || to.hidden
1527
+ };
1528
+ }).sort((left, right) => left.relation.id.localeCompare(right.relation.id));
1529
+ }
1476
1530
  function createSceneViewpoints(document, projection, preset, relationships, objectMap) {
1477
1531
  const generatedOverview = createGeneratedOverviewViewpoint(document, projection, preset);
1478
1532
  const drafts = /* @__PURE__ */ new Map();
@@ -1490,7 +1544,7 @@ var WorldOrbit = (() => {
1490
1544
  }
1491
1545
  const field = fieldParts.join(".").toLowerCase();
1492
1546
  const draft = drafts.get(id) ?? { id };
1493
- applyViewpointField(draft, field, value, projection, preset, relationships, objectMap);
1547
+ applyViewpointField(draft, field, value, document, projection, preset, relationships, objectMap);
1494
1548
  drafts.set(id, draft);
1495
1549
  }
1496
1550
  const viewpoints = [...drafts.values()].map((draft) => finalizeViewpointDraft(draft, projection, preset, objectMap)).filter(Boolean);
@@ -1518,7 +1572,8 @@ var WorldOrbit = (() => {
1518
1572
  });
1519
1573
  }
1520
1574
  function createGeneratedOverviewViewpoint(document, projection, preset) {
1521
- const label = document.system?.properties.title ? `${String(document.system.properties.title)} Overview` : "Overview";
1575
+ const title = document.system?.title ?? document.system?.properties.title;
1576
+ const label = title ? `${String(title)} Overview` : "Overview";
1522
1577
  return {
1523
1578
  id: "overview",
1524
1579
  label,
@@ -1534,7 +1589,7 @@ var WorldOrbit = (() => {
1534
1589
  generated: true
1535
1590
  };
1536
1591
  }
1537
- function applyViewpointField(draft, field, value, projection, preset, relationships, objectMap) {
1592
+ function applyViewpointField(draft, field, value, document, projection, preset, relationships, objectMap) {
1538
1593
  const normalizedValue = value.trim();
1539
1594
  switch (field) {
1540
1595
  case "label":
@@ -1601,7 +1656,7 @@ var WorldOrbit = (() => {
1601
1656
  case "groups":
1602
1657
  draft.filter = {
1603
1658
  ...draft.filter ?? createEmptyViewpointFilter(),
1604
- groupIds: parseViewpointGroups(normalizedValue, relationships, objectMap)
1659
+ groupIds: parseViewpointGroups(normalizedValue, document, relationships, objectMap)
1605
1660
  };
1606
1661
  return;
1607
1662
  }
@@ -1674,7 +1729,7 @@ var WorldOrbit = (() => {
1674
1729
  next["orbits-front"] = enabled;
1675
1730
  continue;
1676
1731
  }
1677
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1732
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1678
1733
  next[rawLayer] = enabled;
1679
1734
  }
1680
1735
  }
@@ -1683,8 +1738,11 @@ var WorldOrbit = (() => {
1683
1738
  function parseViewpointObjectTypes(value) {
1684
1739
  return splitListValue(value).filter((entry) => entry === "star" || entry === "planet" || entry === "moon" || entry === "belt" || entry === "asteroid" || entry === "comet" || entry === "ring" || entry === "structure" || entry === "phenomenon");
1685
1740
  }
1686
- function parseViewpointGroups(value, relationships, objectMap) {
1741
+ function parseViewpointGroups(value, document, relationships, objectMap) {
1687
1742
  return splitListValue(value).map((entry) => {
1743
+ if (document.schemaVersion === "2.1" || document.groups.some((group) => group.id === entry)) {
1744
+ return entry;
1745
+ }
1688
1746
  if (entry.startsWith("wo-") && entry.endsWith("-group")) {
1689
1747
  return entry;
1690
1748
  }
@@ -1815,8 +1873,9 @@ var WorldOrbit = (() => {
1815
1873
  }
1816
1874
  const orbiting = [...context.orbitChildren.get(object.id) ?? []].sort(compareOrbiting);
1817
1875
  const orbitMetricContext = computeOrbitMetricContext(orbiting, parent.radius, context.spacingFactor, context.scaleModel);
1876
+ const orbitRadiiPx = resolveOrbitRadiiPx(orbiting, orbitMetricContext);
1818
1877
  orbiting.forEach((child, index) => {
1819
- const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, context);
1878
+ const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, orbitRadiiPx[index] ?? orbitMetricContext.innerPx, context);
1820
1879
  orbitDrafts.push({
1821
1880
  object: child,
1822
1881
  parentId: object.id,
@@ -1890,7 +1949,8 @@ var WorldOrbit = (() => {
1890
1949
  metricSpread: 0,
1891
1950
  innerPx,
1892
1951
  stepPx,
1893
- pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx)
1952
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
1953
+ minimumGapPx: stepPx * 0.42
1894
1954
  };
1895
1955
  }
1896
1956
  const minMetric = Math.min(...presentMetrics);
@@ -1903,10 +1963,11 @@ var WorldOrbit = (() => {
1903
1963
  metricSpread,
1904
1964
  innerPx,
1905
1965
  stepPx,
1906
- pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx)
1966
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
1967
+ minimumGapPx: stepPx * 0.42
1907
1968
  };
1908
1969
  }
1909
- function resolveOrbitGeometry(object, index, count, parent, metricContext, context) {
1970
+ function resolveOrbitGeometry(object, index, count, parent, metricContext, orbitRadiusPx, context) {
1910
1971
  const placement = object.placement;
1911
1972
  const band = object.type === "belt" || object.type === "ring";
1912
1973
  if (!placement || placement.mode !== "orbit") {
@@ -1924,7 +1985,7 @@ var WorldOrbit = (() => {
1924
1985
  };
1925
1986
  }
1926
1987
  const eccentricity = clampNumber(typeof placement.eccentricity === "number" ? placement.eccentricity : 0, 0, 0.92);
1927
- const semiMajor = resolveOrbitRadiusPx(object, index, metricContext);
1988
+ const semiMajor = orbitRadiusPx;
1928
1989
  const baseMinor = Math.max(semiMajor * Math.sqrt(1 - eccentricity * eccentricity), semiMajor * 0.18);
1929
1990
  const inclinationDeg = unitValueToDegrees(placement.inclination) ?? 0;
1930
1991
  const inclinationScale = context.projection === "isometric" ? Math.max(MIN_ISO_MINOR_SCALE, Math.cos(degreesToRadians(inclinationDeg))) * ISO_FLATTENING : 1;
@@ -1954,15 +2015,19 @@ var WorldOrbit = (() => {
1954
2015
  objectY: objectPoint.y
1955
2016
  };
1956
2017
  }
1957
- function resolveOrbitRadiusPx(object, index, metricContext) {
1958
- const metric = orbitMetric(object);
1959
- if (metric === null) {
1960
- return metricContext.innerPx + index * metricContext.stepPx;
1961
- }
1962
- if (metricContext.metricSpread > 0) {
1963
- return metricContext.innerPx + (metric - metricContext.minMetric) / metricContext.metricSpread * metricContext.pixelSpread;
1964
- }
1965
- return metricContext.innerPx + Math.log10(metric + 1) * metricContext.stepPx;
2018
+ function resolveOrbitRadiusPx(metric, metricContext) {
2019
+ return metricContext.innerPx + metricContext.stepPx * log2(Math.max(metric, 0) + 1);
2020
+ }
2021
+ function resolveOrbitRadiiPx(objects, metricContext) {
2022
+ const radii = [];
2023
+ objects.forEach((object, index) => {
2024
+ const metric = orbitMetric(object);
2025
+ const fallbackRadius = metricContext.innerPx + index * metricContext.stepPx;
2026
+ const baseRadius = metric === null ? fallbackRadius : resolveOrbitRadiusPx(metric, metricContext);
2027
+ const minimumRadius = index === 0 ? metricContext.innerPx : (radii[index - 1] ?? metricContext.innerPx) + metricContext.minimumGapPx;
2028
+ radii.push(Math.max(baseRadius, minimumRadius));
2029
+ });
2030
+ return radii;
1966
2031
  }
1967
2032
  function orbitMetric(object) {
1968
2033
  if (!object.placement || object.placement.mode !== "orbit") {
@@ -1970,6 +2035,9 @@ var WorldOrbit = (() => {
1970
2035
  }
1971
2036
  return toDistanceMetric(object.placement.semiMajor ?? object.placement.distance ?? null);
1972
2037
  }
2038
+ function log2(value) {
2039
+ return Math.log(value) / Math.log(2);
2040
+ }
1973
2041
  function resolveOrbitPhase(phase, index, count) {
1974
2042
  const degreeValue = phase ? unitValueToDegrees(phase) : null;
1975
2043
  if (degreeValue !== null) {
@@ -2293,8 +2361,18 @@ var WorldOrbit = (() => {
2293
2361
  return value.value;
2294
2362
  case "km":
2295
2363
  return value.value / AU_IN_KM;
2364
+ case "m":
2365
+ return value.value / 1e3 / AU_IN_KM;
2366
+ case "ly":
2367
+ return value.value * LY_IN_AU;
2368
+ case "pc":
2369
+ return value.value * PC_IN_AU;
2370
+ case "kpc":
2371
+ return value.value * KPC_IN_AU;
2296
2372
  case "re":
2297
2373
  return value.value * EARTH_RADIUS_IN_KM / AU_IN_KM;
2374
+ case "rj":
2375
+ return value.value * JUPITER_RADIUS_IN_KM / AU_IN_KM;
2298
2376
  case "sol":
2299
2377
  return value.value * SOLAR_RADIUS_IN_KM / AU_IN_KM;
2300
2378
  default:
@@ -2448,8 +2526,11 @@ var WorldOrbit = (() => {
2448
2526
  return {
2449
2527
  format: "worldorbit",
2450
2528
  version: "2.0",
2529
+ schemaVersion: "2.0",
2451
2530
  sourceVersion: document.version,
2452
2531
  system,
2532
+ groups: structuredClone(document.groups ?? []),
2533
+ relations: structuredClone(document.relations ?? []),
2453
2534
  objects: document.objects.map(cloneWorldOrbitObject),
2454
2535
  diagnostics
2455
2536
  };
@@ -2461,13 +2542,20 @@ var WorldOrbit = (() => {
2461
2542
  const system = document.system ? {
2462
2543
  type: "system",
2463
2544
  id: document.system.id,
2545
+ title: document.system.title,
2546
+ description: document.system.description,
2547
+ epoch: document.system.epoch,
2548
+ referencePlane: document.system.referencePlane,
2464
2549
  properties: materializeDraftSystemProperties(document.system),
2465
2550
  info: materializeDraftSystemInfo(document.system)
2466
2551
  } : null;
2467
2552
  return {
2468
2553
  format: "worldorbit",
2469
2554
  version: "1.0",
2555
+ schemaVersion: document.version,
2470
2556
  system,
2557
+ groups: structuredClone(document.groups ?? []),
2558
+ relations: structuredClone(document.relations ?? []),
2471
2559
  objects: document.objects.map(cloneWorldOrbitObject)
2472
2560
  };
2473
2561
  }
@@ -2482,7 +2570,10 @@ var WorldOrbit = (() => {
2482
2570
  return {
2483
2571
  type: "system",
2484
2572
  id: document.system?.id ?? "WorldOrbit",
2485
- title: typeof document.system?.properties.title === "string" ? document.system.properties.title : null,
2573
+ title: document.system?.title ?? (typeof document.system?.properties.title === "string" ? document.system.properties.title : null),
2574
+ description: document.system?.description ?? null,
2575
+ epoch: document.system?.epoch ?? null,
2576
+ referencePlane: document.system?.referencePlane ?? null,
2486
2577
  defaults,
2487
2578
  atlasMetadata,
2488
2579
  viewpoints: scene.viewpoints.map(mapSceneViewpointToDraftViewpoint),
@@ -2609,6 +2700,17 @@ var WorldOrbit = (() => {
2609
2700
  function cloneWorldOrbitObject(object) {
2610
2701
  return {
2611
2702
  ...object,
2703
+ groups: object.groups ? [...object.groups] : void 0,
2704
+ resonance: object.resonance ? { ...object.resonance } : object.resonance,
2705
+ renderHints: object.renderHints ? { ...object.renderHints } : object.renderHints,
2706
+ deriveRules: object.deriveRules ? object.deriveRules.map((rule) => ({ ...rule })) : void 0,
2707
+ validationRules: object.validationRules ? object.validationRules.map((rule) => ({ ...rule })) : void 0,
2708
+ lockedFields: object.lockedFields ? [...object.lockedFields] : void 0,
2709
+ tolerances: object.tolerances ? object.tolerances.map((entry) => ({
2710
+ field: entry.field,
2711
+ 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
2712
+ })) : void 0,
2713
+ typedBlocks: object.typedBlocks ? Object.fromEntries(Object.entries(object.typedBlocks).map(([key, block]) => [key, { ...block ?? {} }])) : void 0,
2612
2714
  properties: cloneProperties(object.properties),
2613
2715
  placement: object.placement ? structuredClone(object.placement) : null,
2614
2716
  info: { ...object.info }
@@ -2653,71 +2755,80 @@ var WorldOrbit = (() => {
2653
2755
  if (system.defaults.units) {
2654
2756
  properties.units = system.defaults.units;
2655
2757
  }
2758
+ if (system.description) {
2759
+ properties.description = system.description;
2760
+ }
2761
+ if (system.epoch) {
2762
+ properties.epoch = system.epoch;
2763
+ }
2764
+ if (system.referencePlane) {
2765
+ properties.referencePlane = system.referencePlane;
2766
+ }
2656
2767
  return properties;
2657
2768
  }
2658
2769
  function materializeDraftSystemInfo(system) {
2659
- const info = {
2770
+ const info2 = {
2660
2771
  ...system.atlasMetadata
2661
2772
  };
2662
2773
  if (system.defaults.theme) {
2663
- info["atlas.theme"] = system.defaults.theme;
2774
+ info2["atlas.theme"] = system.defaults.theme;
2664
2775
  }
2665
2776
  for (const viewpoint of system.viewpoints) {
2666
2777
  const prefix = `viewpoint.${viewpoint.id}`;
2667
- info[`${prefix}.label`] = viewpoint.label;
2778
+ info2[`${prefix}.label`] = viewpoint.label;
2668
2779
  if (viewpoint.summary) {
2669
- info[`${prefix}.summary`] = viewpoint.summary;
2780
+ info2[`${prefix}.summary`] = viewpoint.summary;
2670
2781
  }
2671
2782
  if (viewpoint.focusObjectId) {
2672
- info[`${prefix}.focus`] = viewpoint.focusObjectId;
2783
+ info2[`${prefix}.focus`] = viewpoint.focusObjectId;
2673
2784
  }
2674
2785
  if (viewpoint.selectedObjectId) {
2675
- info[`${prefix}.select`] = viewpoint.selectedObjectId;
2786
+ info2[`${prefix}.select`] = viewpoint.selectedObjectId;
2676
2787
  }
2677
2788
  if (viewpoint.projection) {
2678
- info[`${prefix}.projection`] = viewpoint.projection;
2789
+ info2[`${prefix}.projection`] = viewpoint.projection;
2679
2790
  }
2680
2791
  if (viewpoint.preset) {
2681
- info[`${prefix}.preset`] = viewpoint.preset;
2792
+ info2[`${prefix}.preset`] = viewpoint.preset;
2682
2793
  }
2683
2794
  if (viewpoint.zoom !== null) {
2684
- info[`${prefix}.zoom`] = String(viewpoint.zoom);
2795
+ info2[`${prefix}.zoom`] = String(viewpoint.zoom);
2685
2796
  }
2686
2797
  if (viewpoint.rotationDeg !== 0) {
2687
- info[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2798
+ info2[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2688
2799
  }
2689
2800
  const serializedLayers = serializeViewpointLayers(viewpoint.layers);
2690
2801
  if (serializedLayers) {
2691
- info[`${prefix}.layers`] = serializedLayers;
2802
+ info2[`${prefix}.layers`] = serializedLayers;
2692
2803
  }
2693
2804
  if (viewpoint.filter?.query) {
2694
- info[`${prefix}.query`] = viewpoint.filter.query;
2805
+ info2[`${prefix}.query`] = viewpoint.filter.query;
2695
2806
  }
2696
2807
  if ((viewpoint.filter?.objectTypes.length ?? 0) > 0) {
2697
- info[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
2808
+ info2[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
2698
2809
  }
2699
2810
  if ((viewpoint.filter?.tags.length ?? 0) > 0) {
2700
- info[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
2811
+ info2[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
2701
2812
  }
2702
2813
  if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2703
- info[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2814
+ info2[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2704
2815
  }
2705
2816
  }
2706
2817
  for (const annotation of system.annotations) {
2707
2818
  const prefix = `annotation.${annotation.id}`;
2708
- info[`${prefix}.label`] = annotation.label;
2819
+ info2[`${prefix}.label`] = annotation.label;
2709
2820
  if (annotation.targetObjectId) {
2710
- info[`${prefix}.target`] = annotation.targetObjectId;
2821
+ info2[`${prefix}.target`] = annotation.targetObjectId;
2711
2822
  }
2712
- info[`${prefix}.body`] = annotation.body;
2823
+ info2[`${prefix}.body`] = annotation.body;
2713
2824
  if (annotation.tags.length > 0) {
2714
- info[`${prefix}.tags`] = annotation.tags.join(" ");
2825
+ info2[`${prefix}.tags`] = annotation.tags.join(" ");
2715
2826
  }
2716
2827
  if (annotation.sourceObjectId) {
2717
- info[`${prefix}.source`] = annotation.sourceObjectId;
2828
+ info2[`${prefix}.source`] = annotation.sourceObjectId;
2718
2829
  }
2719
2830
  }
2720
- return info;
2831
+ return info2;
2721
2832
  }
2722
2833
  function serializeViewpointLayers(layers) {
2723
2834
  const tokens = [];
@@ -2726,7 +2837,7 @@ var WorldOrbit = (() => {
2726
2837
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2727
2838
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2728
2839
  }
2729
- for (const key of ["background", "guides", "objects", "labels", "metadata"]) {
2840
+ for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
2730
2841
  if (layers[key] !== void 0) {
2731
2842
  tokens.push(layers[key] ? key : `-${key}`);
2732
2843
  }
@@ -2736,7 +2847,8 @@ var WorldOrbit = (() => {
2736
2847
  function convertAtlasDocumentToLegacyDraft(document) {
2737
2848
  return {
2738
2849
  ...document,
2739
- version: "2.0-draft"
2850
+ version: "2.0-draft",
2851
+ schemaVersion: "2.0-draft"
2740
2852
  };
2741
2853
  }
2742
2854
 
@@ -2778,19 +2890,28 @@ var WorldOrbit = (() => {
2778
2890
  ];
2779
2891
  function formatDocument(document, options = {}) {
2780
2892
  const schema = options.schema ?? "auto";
2781
- const useDraft = schema === "2.0" || schema === "2.0-draft" || document.version === "2.0" || document.version === "2.0-draft";
2893
+ const useDraft = schema === "2.0" || schema === "2.1" || schema === "2.0-draft" || document.version === "2.0" || document.version === "2.1" || document.version === "2.0-draft";
2782
2894
  if (useDraft) {
2783
2895
  if (schema === "2.0-draft") {
2784
- const legacyDraftDocument = document.version === "2.0-draft" ? document : document.version === "2.0" ? {
2896
+ const legacyDraftDocument = document.version === "2.0-draft" ? document : document.version === "2.0" || document.version === "2.1" ? {
2785
2897
  ...document,
2786
- version: "2.0-draft"
2898
+ version: "2.0-draft",
2899
+ schemaVersion: "2.0-draft"
2787
2900
  } : upgradeDocumentToDraftV2(document);
2788
2901
  return formatDraftDocument(legacyDraftDocument);
2789
2902
  }
2790
- const atlasDocument = document.version === "2.0" ? document : document.version === "2.0-draft" ? {
2903
+ const atlasDocument = document.version === "2.0" || document.version === "2.1" ? document : document.version === "2.0-draft" ? {
2791
2904
  ...document,
2792
- version: "2.0"
2905
+ version: "2.0",
2906
+ schemaVersion: "2.0"
2793
2907
  } : upgradeDocumentToV2(document);
2908
+ if (schema === "2.1" && atlasDocument.version !== "2.1") {
2909
+ return formatAtlasDocument({
2910
+ ...atlasDocument,
2911
+ version: "2.1",
2912
+ schemaVersion: "2.1"
2913
+ });
2914
+ }
2794
2915
  return formatAtlasDocument(atlasDocument);
2795
2916
  }
2796
2917
  const lines = [];
@@ -2808,10 +2929,18 @@ var WorldOrbit = (() => {
2808
2929
  return lines.join("\n");
2809
2930
  }
2810
2931
  function formatAtlasDocument(document) {
2811
- const lines = ["schema 2.0", ""];
2932
+ const lines = [`schema ${document.version}`, ""];
2812
2933
  if (document.system) {
2813
2934
  lines.push(...formatAtlasSystem(document.system));
2814
2935
  }
2936
+ for (const group of [...document.groups].sort(compareIdLike)) {
2937
+ lines.push("");
2938
+ lines.push(...formatAtlasGroup(group));
2939
+ }
2940
+ for (const relation of [...document.relations].sort(compareIdLike)) {
2941
+ lines.push("");
2942
+ lines.push(...formatAtlasRelation(relation));
2943
+ }
2815
2944
  const sortedObjects = [...document.objects].sort(compareObjects);
2816
2945
  if (sortedObjects.length > 0 && lines.at(-1) !== "") {
2817
2946
  lines.push("");
@@ -2827,12 +2956,21 @@ var WorldOrbit = (() => {
2827
2956
  function formatDraftDocument(document) {
2828
2957
  const legacy = document.version === "2.0-draft" ? document : {
2829
2958
  ...document,
2830
- version: "2.0-draft"
2959
+ version: "2.0-draft",
2960
+ schemaVersion: "2.0-draft"
2831
2961
  };
2832
2962
  const lines = ["schema 2.0-draft", ""];
2833
2963
  if (legacy.system) {
2834
2964
  lines.push(...formatAtlasSystem(legacy.system));
2835
2965
  }
2966
+ for (const group of [...legacy.groups].sort(compareIdLike)) {
2967
+ lines.push("");
2968
+ lines.push(...formatAtlasGroup(group));
2969
+ }
2970
+ for (const relation of [...legacy.relations].sort(compareIdLike)) {
2971
+ lines.push("");
2972
+ lines.push(...formatAtlasRelation(relation));
2973
+ }
2836
2974
  const sortedObjects = [...legacy.objects].sort(compareObjects);
2837
2975
  if (sortedObjects.length > 0 && lines.at(-1) !== "") {
2838
2976
  lines.push("");
@@ -2848,11 +2986,38 @@ var WorldOrbit = (() => {
2848
2986
  function formatSystem(system) {
2849
2987
  return formatLines("system", system.id, system.properties, null, system.info);
2850
2988
  }
2989
+ function formatLines(objectType, id, properties, placement, info2) {
2990
+ const lines = [`${objectType} ${id}`];
2991
+ const fieldLines = [...formatPlacement(placement), ...formatProperties(properties)];
2992
+ for (const fieldLine of fieldLines) {
2993
+ lines.push(` ${fieldLine}`);
2994
+ }
2995
+ const infoEntries = Object.entries(info2).sort(([left], [right]) => left.localeCompare(right));
2996
+ if (infoEntries.length > 0) {
2997
+ if (fieldLines.length > 0) {
2998
+ lines.push("");
2999
+ }
3000
+ lines.push(" info");
3001
+ for (const [key, value] of infoEntries) {
3002
+ lines.push(` ${key} ${quoteIfNeeded(value)}`);
3003
+ }
3004
+ }
3005
+ return lines;
3006
+ }
2851
3007
  function formatAtlasSystem(system) {
2852
3008
  const lines = [`system ${system.id}`];
2853
3009
  if (system.title) {
2854
3010
  lines.push(` title ${quoteIfNeeded(system.title)}`);
2855
3011
  }
3012
+ if (system.description) {
3013
+ lines.push(` description ${quoteIfNeeded(system.description)}`);
3014
+ }
3015
+ if (system.epoch) {
3016
+ lines.push(` epoch ${quoteIfNeeded(system.epoch)}`);
3017
+ }
3018
+ if (system.referencePlane) {
3019
+ lines.push(` referencePlane ${quoteIfNeeded(system.referencePlane)}`);
3020
+ }
2856
3021
  lines.push("");
2857
3022
  lines.push("defaults");
2858
3023
  lines.push(` view ${system.defaults.view}`);
@@ -2887,18 +3052,22 @@ var WorldOrbit = (() => {
2887
3052
  return lines;
2888
3053
  }
2889
3054
  function formatObject(object) {
2890
- return formatLines(object.type, object.id, object.properties, object.placement, object.info);
3055
+ return formatWorldOrbitObject(object.type, object.id, object);
2891
3056
  }
2892
3057
  function formatAtlasObject(object) {
2893
- return formatLines(`object ${object.type}`, object.id, object.properties, object.placement, object.info);
3058
+ return formatWorldOrbitObject(`object ${object.type}`, object.id, object);
2894
3059
  }
2895
- function formatLines(objectType, id, properties, placement, info) {
3060
+ function formatWorldOrbitObject(objectType, id, object) {
2896
3061
  const lines = [`${objectType} ${id}`];
2897
- const fieldLines = [...formatPlacement(placement), ...formatProperties(properties)];
3062
+ const fieldLines = [
3063
+ ...formatPlacement(object.placement),
3064
+ ...formatProperties(object.properties),
3065
+ ...formatObjectMetadata(object)
3066
+ ];
2898
3067
  for (const fieldLine of fieldLines) {
2899
3068
  lines.push(` ${fieldLine}`);
2900
3069
  }
2901
- const infoEntries = Object.entries(info).sort(([left], [right]) => left.localeCompare(right));
3070
+ const infoEntries = Object.entries(object.info).sort(([left], [right]) => left.localeCompare(right));
2902
3071
  if (infoEntries.length > 0) {
2903
3072
  if (fieldLines.length > 0) {
2904
3073
  lines.push("");
@@ -2908,6 +3077,16 @@ var WorldOrbit = (() => {
2908
3077
  lines.push(` ${key} ${quoteIfNeeded(value)}`);
2909
3078
  }
2910
3079
  }
3080
+ for (const blockName of ["climate", "habitability", "settlement"]) {
3081
+ const blockEntries = Object.entries(object.typedBlocks?.[blockName] ?? {}).sort(([left], [right]) => left.localeCompare(right));
3082
+ if (blockEntries.length > 0) {
3083
+ lines.push("");
3084
+ lines.push(` ${blockName}`);
3085
+ for (const [key, value] of blockEntries) {
3086
+ lines.push(` ${key} ${quoteIfNeeded(value)}`);
3087
+ }
3088
+ }
3089
+ }
2911
3090
  return lines;
2912
3091
  }
2913
3092
  function formatPlacement(placement) {
@@ -2936,6 +3115,46 @@ var WorldOrbit = (() => {
2936
3115
  function formatProperties(properties) {
2937
3116
  return Object.keys(properties).sort(compareFieldKeys).map((key) => `${key} ${formatValue(properties[key])}`);
2938
3117
  }
3118
+ function formatObjectMetadata(object) {
3119
+ const lines = [];
3120
+ if (object.groups?.length) {
3121
+ lines.push(`groups ${object.groups.join(" ")}`);
3122
+ }
3123
+ if (object.epoch) {
3124
+ lines.push(`epoch ${quoteIfNeeded(object.epoch)}`);
3125
+ }
3126
+ if (object.referencePlane) {
3127
+ lines.push(`referencePlane ${quoteIfNeeded(object.referencePlane)}`);
3128
+ }
3129
+ if (object.tidalLock !== void 0) {
3130
+ lines.push(`tidalLock ${object.tidalLock ? "true" : "false"}`);
3131
+ }
3132
+ if (object.renderHints?.renderLabel !== void 0) {
3133
+ lines.push(`renderLabel ${object.renderHints.renderLabel ? "true" : "false"}`);
3134
+ }
3135
+ if (object.renderHints?.renderOrbit !== void 0) {
3136
+ lines.push(`renderOrbit ${object.renderHints.renderOrbit ? "true" : "false"}`);
3137
+ }
3138
+ if (object.renderHints?.renderPriority !== void 0) {
3139
+ lines.push(`renderPriority ${object.renderHints.renderPriority}`);
3140
+ }
3141
+ if (object.resonance) {
3142
+ lines.push(`resonance ${object.resonance.targetObjectId} ${object.resonance.ratio}`);
3143
+ }
3144
+ for (const rule of object.deriveRules ?? []) {
3145
+ lines.push(`derive ${rule.field} ${rule.strategy}`);
3146
+ }
3147
+ for (const rule of object.validationRules ?? []) {
3148
+ lines.push(`validate ${rule.rule}`);
3149
+ }
3150
+ if (object.lockedFields?.length) {
3151
+ lines.push(`locked ${object.lockedFields.join(" ")}`);
3152
+ }
3153
+ for (const tolerance of object.tolerances ?? []) {
3154
+ lines.push(`tolerance ${tolerance.field} ${formatValue(tolerance.value)}`);
3155
+ }
3156
+ return lines;
3157
+ }
2939
3158
  function formatAtlasViewpoint(viewpoint) {
2940
3159
  const lines = [`viewpoint ${viewpoint.id}`, ` label ${quoteIfNeeded(viewpoint.label)}`];
2941
3160
  if (viewpoint.focusObjectId) {
@@ -2991,6 +3210,50 @@ var WorldOrbit = (() => {
2991
3210
  }
2992
3211
  return lines;
2993
3212
  }
3213
+ function formatAtlasGroup(group) {
3214
+ const lines = [`group ${group.id}`, ` label ${quoteIfNeeded(group.label)}`];
3215
+ if (group.summary) {
3216
+ lines.push(` summary ${quoteIfNeeded(group.summary)}`);
3217
+ }
3218
+ if (group.color) {
3219
+ lines.push(` color ${quoteIfNeeded(group.color)}`);
3220
+ }
3221
+ if (group.tags.length > 0) {
3222
+ lines.push(` tags ${group.tags.map(quoteIfNeeded).join(" ")}`);
3223
+ }
3224
+ if (group.hidden) {
3225
+ lines.push(" hidden true");
3226
+ }
3227
+ return lines;
3228
+ }
3229
+ function formatAtlasRelation(relation) {
3230
+ const lines = [`relation ${relation.id}`];
3231
+ if (relation.from) {
3232
+ lines.push(` from ${quoteIfNeeded(relation.from)}`);
3233
+ }
3234
+ if (relation.to) {
3235
+ lines.push(` to ${quoteIfNeeded(relation.to)}`);
3236
+ }
3237
+ if (relation.kind) {
3238
+ lines.push(` kind ${quoteIfNeeded(relation.kind)}`);
3239
+ }
3240
+ if (relation.label) {
3241
+ lines.push(` label ${quoteIfNeeded(relation.label)}`);
3242
+ }
3243
+ if (relation.summary) {
3244
+ lines.push(` summary ${quoteIfNeeded(relation.summary)}`);
3245
+ }
3246
+ if (relation.tags.length > 0) {
3247
+ lines.push(` tags ${relation.tags.map(quoteIfNeeded).join(" ")}`);
3248
+ }
3249
+ if (relation.color) {
3250
+ lines.push(` color ${quoteIfNeeded(relation.color)}`);
3251
+ }
3252
+ if (relation.hidden) {
3253
+ lines.push(" hidden true");
3254
+ }
3255
+ return lines;
3256
+ }
2994
3257
  function formatValue(value) {
2995
3258
  if (Array.isArray(value)) {
2996
3259
  return value.map((item) => quoteIfNeeded(item)).join(" ");
@@ -3032,7 +3295,7 @@ var WorldOrbit = (() => {
3032
3295
  if (orbitFront !== void 0 || orbitBack !== void 0) {
3033
3296
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
3034
3297
  }
3035
- for (const key of ["background", "guides", "objects", "labels", "metadata"]) {
3298
+ for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
3036
3299
  if (layers[key] !== void 0) {
3037
3300
  tokens.push(layers[key] ? key : `-${key}`);
3038
3301
  }
@@ -3057,6 +3320,9 @@ var WorldOrbit = (() => {
3057
3320
  return leftIndex - rightIndex;
3058
3321
  return left.id.localeCompare(right.id);
3059
3322
  }
3323
+ function compareIdLike(left, right) {
3324
+ return left.id.localeCompare(right.id);
3325
+ }
3060
3326
  function objectTypeIndex(objectType) {
3061
3327
  switch (objectType) {
3062
3328
  case "star":
@@ -3086,180 +3352,716 @@ var WorldOrbit = (() => {
3086
3352
  return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
3087
3353
  }
3088
3354
 
3089
- // packages/core/dist/draft-parse.js
3090
- function parseWorldOrbitAtlas(source) {
3091
- return parseAtlasSource(source, "2.0");
3355
+ // packages/core/dist/atlas-utils.js
3356
+ 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)?$/;
3357
+ var BOOLEAN_VALUES2 = /* @__PURE__ */ new Map([
3358
+ ["true", true],
3359
+ ["false", false],
3360
+ ["yes", true],
3361
+ ["no", false]
3362
+ ]);
3363
+ var URL_SCHEME_PATTERN2 = /^[A-Za-z][A-Za-z0-9+.-]*:/;
3364
+ function normalizeIdentifier2(value) {
3365
+ return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
3092
3366
  }
3093
- function parseWorldOrbitDraft(source) {
3094
- return parseAtlasSource(source, "2.0-draft");
3367
+ function humanizeIdentifier3(value) {
3368
+ return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
3095
3369
  }
3096
- function parseAtlasSource(source, outputVersion) {
3097
- const lines = source.split(/\r?\n/);
3098
- let sawSchemaHeader = false;
3099
- let schemaVersion = "2.0";
3100
- let system = null;
3101
- let section = null;
3102
- const objectNodes = [];
3103
- let sawDefaults = false;
3104
- let sawAtlas = false;
3105
- const viewpointIds = /* @__PURE__ */ new Set();
3106
- const annotationIds = /* @__PURE__ */ new Set();
3107
- for (let index = 0; index < lines.length; index++) {
3108
- const rawLine = lines[index];
3109
- const lineNumber = index + 1;
3110
- if (!rawLine.trim()) {
3111
- continue;
3112
- }
3113
- const indent = getIndent(rawLine);
3114
- const tokens = tokenizeLineDetailed(rawLine.slice(indent), {
3115
- line: lineNumber,
3116
- columnOffset: indent
3117
- });
3118
- if (tokens.length === 0) {
3119
- continue;
3120
- }
3121
- if (!sawSchemaHeader) {
3122
- schemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
3123
- sawSchemaHeader = true;
3124
- continue;
3125
- }
3126
- if (indent === 0) {
3127
- section = startTopLevelSection(tokens, lineNumber, system, objectNodes, viewpointIds, annotationIds, {
3128
- sawDefaults,
3129
- sawAtlas
3130
- });
3131
- if (section.kind === "system") {
3132
- system = section.system;
3133
- } else if (section.kind === "defaults") {
3134
- sawDefaults = true;
3135
- } else if (section.kind === "atlas") {
3136
- sawAtlas = true;
3137
- }
3138
- continue;
3139
- }
3140
- if (!section) {
3141
- throw new WorldOrbitError("Indented line without parent atlas section", lineNumber, indent + 1);
3142
- }
3143
- handleSectionLine(section, indent, tokens, lineNumber);
3144
- }
3145
- if (!sawSchemaHeader) {
3146
- throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
3370
+ function parseAtlasUnitValue(input, location, fieldKey) {
3371
+ const match = input.match(UNIT_PATTERN2);
3372
+ if (!match) {
3373
+ throw WorldOrbitError.fromLocation(`Invalid unit value "${input}"`, location);
3147
3374
  }
3148
- const ast = {
3149
- type: "document",
3150
- objects: objectNodes
3375
+ const unitValue = {
3376
+ value: Number(match[1]),
3377
+ unit: match[2] ?? null
3151
3378
  };
3152
- const normalizedObjects = normalizeDocument(ast).objects;
3153
- validateDocument({
3154
- format: "worldorbit",
3155
- version: "1.0",
3156
- system: null,
3157
- objects: normalizedObjects
3158
- });
3159
- const diagnostics = schemaVersion === "2.0-draft" && outputVersion === "2.0" ? [
3160
- {
3161
- code: "load.schema.deprecatedDraft",
3162
- severity: "warning",
3163
- source: "upgrade",
3164
- message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".'
3379
+ if (fieldKey) {
3380
+ const schema = getFieldSchema(fieldKey);
3381
+ if (schema?.unitFamily && !unitFamilyAllowsUnit(schema.unitFamily, unitValue.unit)) {
3382
+ throw WorldOrbitError.fromLocation(`Unit "${unitValue.unit ?? "none"}" is not valid for "${fieldKey}"`, location);
3165
3383
  }
3166
- ] : [];
3167
- return {
3168
- format: "worldorbit",
3169
- version: outputVersion,
3170
- sourceVersion: "1.0",
3171
- system,
3172
- objects: normalizedObjects,
3173
- diagnostics
3174
- };
3175
- }
3176
- function assertDraftSchemaHeader(tokens, line) {
3177
- if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || tokens[1].value.toLowerCase() !== "2.0-draft" && tokens[1].value.toLowerCase() !== "2.0") {
3178
- throw new WorldOrbitError('Expected atlas header "schema 2.0" or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
3179
- }
3180
- return tokens[1].value.toLowerCase() === "2.0-draft" ? "2.0-draft" : "2.0";
3181
- }
3182
- function startTopLevelSection(tokens, line, system, objectNodes, viewpointIds, annotationIds, flags) {
3183
- const keyword = tokens[0]?.value.toLowerCase();
3184
- switch (keyword) {
3185
- case "system":
3186
- if (system) {
3187
- throw new WorldOrbitError('Atlas section "system" may only appear once', line, tokens[0].column);
3188
- }
3189
- return startSystemSection(tokens, line);
3190
- case "defaults":
3191
- if (!system) {
3192
- throw new WorldOrbitError('Atlas section "defaults" requires a preceding system declaration', line, tokens[0].column);
3193
- }
3194
- if (flags.sawDefaults) {
3195
- throw new WorldOrbitError('Atlas section "defaults" may only appear once', line, tokens[0].column);
3196
- }
3197
- return {
3198
- kind: "defaults",
3199
- system,
3200
- seenFields: /* @__PURE__ */ new Set()
3201
- };
3202
- case "atlas":
3203
- if (!system) {
3204
- throw new WorldOrbitError('Atlas section "atlas" requires a preceding system declaration', line, tokens[0].column);
3205
- }
3206
- if (flags.sawAtlas) {
3207
- throw new WorldOrbitError('Atlas section "atlas" may only appear once', line, tokens[0].column);
3208
- }
3209
- return {
3210
- kind: "atlas",
3211
- system,
3212
- inMetadata: false,
3213
- metadataIndent: null
3214
- };
3215
- case "viewpoint":
3216
- if (!system) {
3217
- throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
3218
- }
3219
- return startViewpointSection(tokens, line, system, viewpointIds);
3220
- case "annotation":
3221
- if (!system) {
3222
- throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
3223
- }
3224
- return startAnnotationSection(tokens, line, system, annotationIds);
3225
- case "object":
3226
- return startObjectSection(tokens, line, objectNodes);
3227
- default:
3228
- throw new WorldOrbitError(`Unknown atlas section "${tokens[0]?.value ?? ""}"`, line, tokens[0]?.column ?? 1);
3229
3384
  }
3385
+ return unitValue;
3230
3386
  }
3231
- function startSystemSection(tokens, line) {
3232
- if (tokens.length !== 2) {
3233
- throw new WorldOrbitError("Invalid atlas system declaration", line, tokens[0]?.column ?? 1);
3387
+ function tryParseAtlasUnitValue(input) {
3388
+ const match = input.match(UNIT_PATTERN2);
3389
+ if (!match) {
3390
+ return null;
3234
3391
  }
3235
- const system = {
3236
- type: "system",
3237
- id: tokens[1].value,
3238
- title: null,
3239
- defaults: {
3240
- view: "topdown",
3241
- scale: null,
3242
- units: null,
3243
- preset: null,
3244
- theme: null
3245
- },
3246
- atlasMetadata: {},
3247
- viewpoints: [],
3248
- annotations: []
3249
- };
3250
3392
  return {
3251
- kind: "system",
3252
- system,
3253
- seenFields: /* @__PURE__ */ new Set()
3393
+ value: Number(match[1]),
3394
+ unit: match[2] ?? null
3254
3395
  };
3255
3396
  }
3256
- function startViewpointSection(tokens, line, system, viewpointIds) {
3257
- if (tokens.length !== 2) {
3258
- throw new WorldOrbitError("Invalid viewpoint declaration", line, tokens[0]?.column ?? 1);
3397
+ function parseAtlasNumber(input, key, location) {
3398
+ const value = Number(input);
3399
+ if (!Number.isFinite(value)) {
3400
+ throw WorldOrbitError.fromLocation(`Invalid numeric value "${input}" for "${key}"`, location);
3259
3401
  }
3260
- const id = normalizeIdentifier2(tokens[1].value);
3261
- if (!id) {
3262
- throw new WorldOrbitError("Viewpoint id must not be empty", line, tokens[1].column);
3402
+ return value;
3403
+ }
3404
+ function parseAtlasBoolean(input, key, location) {
3405
+ const parsed = BOOLEAN_VALUES2.get(input.toLowerCase());
3406
+ if (parsed === void 0) {
3407
+ throw WorldOrbitError.fromLocation(`Invalid boolean value "${input}" for "${key}"`, location);
3408
+ }
3409
+ return parsed;
3410
+ }
3411
+ function parseAtlasAtReference(target, location) {
3412
+ if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
3413
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
3414
+ }
3415
+ const pairedMatch = target.match(/^([A-Za-z0-9._-]+)-([A-Za-z0-9._-]+):(L[1-5])$/);
3416
+ if (pairedMatch) {
3417
+ return {
3418
+ kind: "lagrange",
3419
+ primary: pairedMatch[1],
3420
+ secondary: pairedMatch[2],
3421
+ point: pairedMatch[3]
3422
+ };
3423
+ }
3424
+ const simpleMatch = target.match(/^([A-Za-z0-9._-]+):(L[1-5])$/);
3425
+ if (simpleMatch) {
3426
+ return {
3427
+ kind: "lagrange",
3428
+ primary: simpleMatch[1],
3429
+ secondary: null,
3430
+ point: simpleMatch[2]
3431
+ };
3432
+ }
3433
+ if (/^[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
3434
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
3435
+ }
3436
+ const anchorMatch = target.match(/^([A-Za-z0-9._-]+):([A-Za-z0-9._-]+)$/);
3437
+ if (anchorMatch) {
3438
+ return {
3439
+ kind: "anchor",
3440
+ objectId: anchorMatch[1],
3441
+ anchor: anchorMatch[2]
3442
+ };
3443
+ }
3444
+ return {
3445
+ kind: "named",
3446
+ name: target
3447
+ };
3448
+ }
3449
+ function validateAtlasImageSource(value, location) {
3450
+ if (!value) {
3451
+ throw WorldOrbitError.fromLocation('Field "image" must not be empty', location);
3452
+ }
3453
+ if (value.startsWith("//")) {
3454
+ throw WorldOrbitError.fromLocation('Field "image" must use a relative path, root-relative path, or an http/https URL', location);
3455
+ }
3456
+ const schemeMatch = value.match(URL_SCHEME_PATTERN2);
3457
+ if (!schemeMatch) {
3458
+ return;
3459
+ }
3460
+ const scheme = schemeMatch[0].slice(0, -1).toLowerCase();
3461
+ if (scheme !== "http" && scheme !== "https") {
3462
+ throw WorldOrbitError.fromLocation(`Field "image" does not support the "${scheme}" scheme`, location);
3463
+ }
3464
+ }
3465
+ function normalizeLegacyScalarValue(key, values, location) {
3466
+ const schema = getFieldSchema(key);
3467
+ if (!schema) {
3468
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
3469
+ }
3470
+ if (schema.arity === "single" && values.length !== 1) {
3471
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
3472
+ }
3473
+ switch (schema.kind) {
3474
+ case "list":
3475
+ return values;
3476
+ case "boolean":
3477
+ return parseAtlasBoolean(singleAtlasValue(values, key, location), key, location);
3478
+ case "number":
3479
+ return parseAtlasNumber(singleAtlasValue(values, key, location), key, location);
3480
+ case "unit":
3481
+ return parseAtlasUnitValue(singleAtlasValue(values, key, location), location, key);
3482
+ case "string": {
3483
+ const value = values.join(" ").trim();
3484
+ if (key === "image") {
3485
+ validateAtlasImageSource(value, location);
3486
+ }
3487
+ return value;
3488
+ }
3489
+ }
3490
+ }
3491
+ function ensureAtlasFieldSupported(key, objectType, location) {
3492
+ const schema = getFieldSchema(key);
3493
+ if (!schema) {
3494
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
3495
+ }
3496
+ if (!schema.objectTypes.includes(objectType)) {
3497
+ throw WorldOrbitError.fromLocation(`Field "${key}" is not valid on "${objectType}"`, location);
3498
+ }
3499
+ }
3500
+ function singleAtlasValue(values, key, location) {
3501
+ if (values.length !== 1) {
3502
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
3503
+ }
3504
+ return values[0];
3505
+ }
3506
+
3507
+ // packages/core/dist/atlas-validate.js
3508
+ var SURFACE_TARGET_TYPES2 = /* @__PURE__ */ new Set(["star", "planet", "moon", "asteroid", "comet"]);
3509
+ var EARTH_MASSES_PER_SOLAR = 332946.0487;
3510
+ var JUPITER_MASSES_PER_SOLAR = 1047.3486;
3511
+ var AU_IN_KM2 = 1495978707e-1;
3512
+ var EARTH_RADIUS_IN_KM2 = 6371;
3513
+ var SOLAR_RADIUS_IN_KM2 = 695700;
3514
+ var LY_IN_AU2 = 63241.077;
3515
+ var PC_IN_AU2 = 206264.806;
3516
+ var KPC_IN_AU2 = 206264806;
3517
+ function collectAtlasDiagnostics(document, sourceSchemaVersion) {
3518
+ const diagnostics = [];
3519
+ const objectMap = new Map(document.objects.map((object) => [object.id, object]));
3520
+ const groupIds = new Set(document.groups.map((group) => group.id));
3521
+ if (!document.system) {
3522
+ diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
3523
+ }
3524
+ const knownIds = /* @__PURE__ */ new Map();
3525
+ for (const [kind, ids] of [
3526
+ ["group", document.groups.map((group) => group.id)],
3527
+ ["viewpoint", document.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
3528
+ ["annotation", document.system?.annotations.map((annotation) => annotation.id) ?? []],
3529
+ ["relation", document.relations.map((relation) => relation.id)],
3530
+ ["object", document.objects.map((object) => object.id)]
3531
+ ]) {
3532
+ for (const id of ids) {
3533
+ const previous = knownIds.get(id);
3534
+ if (previous) {
3535
+ diagnostics.push(error("validate.id.duplicate", `Duplicate ${kind} id "${id}" already used by ${previous}.`));
3536
+ } else {
3537
+ knownIds.set(id, kind);
3538
+ }
3539
+ }
3540
+ }
3541
+ for (const relation of document.relations) {
3542
+ validateRelation(relation, objectMap, diagnostics);
3543
+ }
3544
+ for (const viewpoint of document.system?.viewpoints ?? []) {
3545
+ validateViewpointFilter(viewpoint.filter, groupIds, sourceSchemaVersion, diagnostics, viewpoint.id);
3546
+ }
3547
+ for (const object of document.objects) {
3548
+ validateObject(object, document.system, objectMap, groupIds, diagnostics);
3549
+ }
3550
+ return diagnostics;
3551
+ }
3552
+ function validateRelation(relation, objectMap, diagnostics) {
3553
+ if (!relation.from) {
3554
+ diagnostics.push(error("validate.relation.from.required", `Relation "${relation.id}" is missing a "from" target.`));
3555
+ } else if (!objectMap.has(relation.from)) {
3556
+ diagnostics.push(error("validate.relation.from.unknown", `Unknown relation source "${relation.from}" on "${relation.id}".`));
3557
+ }
3558
+ if (!relation.to) {
3559
+ diagnostics.push(error("validate.relation.to.required", `Relation "${relation.id}" is missing a "to" target.`));
3560
+ } else if (!objectMap.has(relation.to)) {
3561
+ diagnostics.push(error("validate.relation.to.unknown", `Unknown relation target "${relation.to}" on "${relation.id}".`));
3562
+ }
3563
+ if (!relation.kind) {
3564
+ diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
3565
+ }
3566
+ }
3567
+ function validateViewpointFilter(filter, groupIds, sourceSchemaVersion, diagnostics, viewpointId) {
3568
+ if (!filter || sourceSchemaVersion !== "2.1") {
3569
+ return;
3570
+ }
3571
+ for (const groupId of filter.groupIds) {
3572
+ if (!groupIds.has(groupId)) {
3573
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`));
3574
+ }
3575
+ }
3576
+ }
3577
+ function validateObject(object, system, objectMap, groupIds, diagnostics) {
3578
+ const placement = object.placement;
3579
+ const orbitPlacement = placement?.mode === "orbit" ? placement : null;
3580
+ const parentObject = placement?.mode === "orbit" ? objectMap.get(placement.target) ?? null : null;
3581
+ if (object.groups) {
3582
+ for (const groupId of object.groups) {
3583
+ if (!groupIds.has(groupId)) {
3584
+ diagnostics.push(warn("validate.group.unknown", `Unknown group "${groupId}" on "${object.id}".`, object.id, "groups"));
3585
+ }
3586
+ }
3587
+ }
3588
+ if (orbitPlacement) {
3589
+ if (!objectMap.has(orbitPlacement.target)) {
3590
+ diagnostics.push(error("validate.orbit.target.unknown", `Unknown placement target "${orbitPlacement.target}" on "${object.id}".`, object.id, "orbit"));
3591
+ }
3592
+ if (orbitPlacement.distance && orbitPlacement.semiMajor) {
3593
+ diagnostics.push(error("validate.orbit.distanceConflict", `Object "${object.id}" cannot declare both "distance" and "semiMajor".`, object.id, "distance"));
3594
+ }
3595
+ if (orbitPlacement.phase && !object.epoch && !system?.epoch) {
3596
+ diagnostics.push(warn("validate.phase.epochMissing", `Object "${object.id}" sets "phase" without an object or system epoch.`, object.id, "phase"));
3597
+ }
3598
+ if (orbitPlacement.inclination && !object.referencePlane && !system?.referencePlane) {
3599
+ diagnostics.push(warn("validate.inclination.referencePlaneMissing", `Object "${object.id}" sets "inclination" without an object or system reference plane.`, object.id, "inclination"));
3600
+ }
3601
+ if (orbitPlacement.period && !massInSolar(parentObject?.properties.mass)) {
3602
+ diagnostics.push(warn("validate.period.massMissing", `Object "${object.id}" sets "period" but its central mass cannot be derived.`, object.id, "period"));
3603
+ }
3604
+ }
3605
+ if (placement?.mode === "surface") {
3606
+ const target = objectMap.get(placement.target);
3607
+ if (!target) {
3608
+ diagnostics.push(error("validate.surface.target.unknown", `Unknown placement target "${placement.target}" on "${object.id}".`, object.id, "surface"));
3609
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
3610
+ diagnostics.push(error("validate.surface.target.invalid", `Surface target "${placement.target}" on "${object.id}" is not surface-capable.`, object.id, "surface"));
3611
+ }
3612
+ }
3613
+ if (placement?.mode === "at") {
3614
+ if (object.type !== "structure" && object.type !== "phenomenon") {
3615
+ diagnostics.push(error("validate.at.objectType", `Only structures and phenomena may use "at" placement; found "${object.type}" on "${object.id}".`, object.id, "at"));
3616
+ }
3617
+ if (!validateAtTarget(object, objectMap, diagnostics)) {
3618
+ diagnostics.push(error("validate.at.target.unknown", `Unknown at-reference target "${placement.target}" on "${object.id}".`, object.id, "at"));
3619
+ }
3620
+ }
3621
+ if (object.resonance) {
3622
+ const target = objectMap.get(object.resonance.targetObjectId);
3623
+ if (!target) {
3624
+ diagnostics.push(error("validate.resonance.target.unknown", `Unknown resonance target "${object.resonance.targetObjectId}" on "${object.id}".`, object.id, "resonance"));
3625
+ } else if (object.placement?.mode !== "orbit" || target.placement?.mode !== "orbit" || object.placement.target !== target.placement.target) {
3626
+ diagnostics.push(warn("validate.resonance.orbitMismatch", `Resonance target "${object.resonance.targetObjectId}" on "${object.id}" does not share a compatible orbital parent.`, object.id, "resonance"));
3627
+ }
3628
+ }
3629
+ for (const rule of object.deriveRules ?? []) {
3630
+ if (rule.field !== "period" || rule.strategy !== "kepler") {
3631
+ diagnostics.push(warn("validate.derive.unsupported", `Unsupported derive rule "${rule.field} ${rule.strategy}" on "${object.id}".`, object.id, "derive"));
3632
+ continue;
3633
+ }
3634
+ const derivedPeriodDays = keplerPeriodDays(object, parentObject);
3635
+ if (derivedPeriodDays === null) {
3636
+ diagnostics.push(warn("validate.derive.inputsMissing", `Object "${object.id}" requests "derive period kepler" but lacks enough input data.`, object.id, "derive"));
3637
+ continue;
3638
+ }
3639
+ if (!orbitPlacement?.period) {
3640
+ diagnostics.push(info("validate.derive.period.available", `Object "${object.id}" can derive a Kepler period of ${formatDays(derivedPeriodDays)}.`, object.id, "derive"));
3641
+ }
3642
+ }
3643
+ for (const rule of object.validationRules ?? []) {
3644
+ if (rule.rule !== "kepler") {
3645
+ diagnostics.push(warn("validate.rule.unsupported", `Unsupported validation rule "${rule.rule}" on "${object.id}".`, object.id, "validate"));
3646
+ continue;
3647
+ }
3648
+ const actualPeriodDays = durationInDays(orbitPlacement?.period);
3649
+ const derivedPeriodDays = keplerPeriodDays(object, parentObject);
3650
+ if (actualPeriodDays === null || derivedPeriodDays === null) {
3651
+ continue;
3652
+ }
3653
+ const toleranceDays = toleranceForField(object, "period");
3654
+ if (Math.abs(actualPeriodDays - derivedPeriodDays) > toleranceDays) {
3655
+ diagnostics.push(error("validate.kepler.mismatch", `Object "${object.id}" fails Kepler validation for "period".`, object.id, "validate"));
3656
+ }
3657
+ }
3658
+ }
3659
+ function validateAtTarget(object, objectMap, diagnostics) {
3660
+ const reference = object.placement?.mode === "at" ? object.placement.reference : null;
3661
+ if (!reference) {
3662
+ return true;
3663
+ }
3664
+ if (reference.kind === "named") {
3665
+ return objectMap.has(reference.name);
3666
+ }
3667
+ if (reference.kind === "anchor") {
3668
+ if (!objectMap.has(reference.objectId)) {
3669
+ diagnostics.push(error("validate.anchor.target.unknown", `Unknown anchor target "${reference.objectId}" on "${object.id}".`, object.id, "at"));
3670
+ return false;
3671
+ }
3672
+ return true;
3673
+ }
3674
+ if (!objectMap.has(reference.primary)) {
3675
+ diagnostics.push(error("validate.lagrange.primary.unknown", `Unknown Lagrange reference "${reference.primary}" on "${object.id}".`, object.id, "at"));
3676
+ return false;
3677
+ }
3678
+ if (reference.secondary && !objectMap.has(reference.secondary)) {
3679
+ diagnostics.push(error("validate.lagrange.secondary.unknown", `Unknown Lagrange reference "${reference.secondary}" on "${object.id}".`, object.id, "at"));
3680
+ return false;
3681
+ }
3682
+ return true;
3683
+ }
3684
+ function keplerPeriodDays(object, parentObject) {
3685
+ const placement = object.placement;
3686
+ if (!placement || placement.mode !== "orbit") {
3687
+ return null;
3688
+ }
3689
+ const semiMajorAu = distanceInAu(placement.semiMajor ?? placement.distance);
3690
+ const centralMassSolar = massInSolar(parentObject?.properties.mass);
3691
+ if (semiMajorAu === null || centralMassSolar === null || centralMassSolar <= 0) {
3692
+ return null;
3693
+ }
3694
+ const periodYears = Math.sqrt(semiMajorAu ** 3 / centralMassSolar);
3695
+ return periodYears * 365.25;
3696
+ }
3697
+ function distanceInAu(value) {
3698
+ if (!value)
3699
+ return null;
3700
+ switch (value.unit) {
3701
+ case null:
3702
+ case "au":
3703
+ return value.value;
3704
+ case "km":
3705
+ return value.value / AU_IN_KM2;
3706
+ case "m":
3707
+ return value.value / (AU_IN_KM2 * 1e3);
3708
+ case "ly":
3709
+ return value.value * LY_IN_AU2;
3710
+ case "pc":
3711
+ return value.value * PC_IN_AU2;
3712
+ case "kpc":
3713
+ return value.value * KPC_IN_AU2;
3714
+ case "re":
3715
+ return value.value * EARTH_RADIUS_IN_KM2 / AU_IN_KM2;
3716
+ case "sol":
3717
+ return value.value * SOLAR_RADIUS_IN_KM2 / AU_IN_KM2;
3718
+ default:
3719
+ return null;
3720
+ }
3721
+ }
3722
+ function massInSolar(value) {
3723
+ if (!value || typeof value !== "object" || !("value" in value)) {
3724
+ return null;
3725
+ }
3726
+ const unitValue = value;
3727
+ switch (unitValue.unit) {
3728
+ case null:
3729
+ case "sol":
3730
+ return unitValue.value;
3731
+ case "me":
3732
+ return unitValue.value / EARTH_MASSES_PER_SOLAR;
3733
+ case "mj":
3734
+ return unitValue.value / JUPITER_MASSES_PER_SOLAR;
3735
+ default:
3736
+ return null;
3737
+ }
3738
+ }
3739
+ function durationInDays(value) {
3740
+ if (!value)
3741
+ return null;
3742
+ switch (value.unit) {
3743
+ case null:
3744
+ case "d":
3745
+ return value.value;
3746
+ case "s":
3747
+ return value.value / 86400;
3748
+ case "min":
3749
+ return value.value / 1440;
3750
+ case "h":
3751
+ return value.value / 24;
3752
+ case "y":
3753
+ return value.value * 365.25;
3754
+ case "ky":
3755
+ return value.value * 365250;
3756
+ case "my":
3757
+ return value.value * 36525e4;
3758
+ case "gy":
3759
+ return value.value * 36525e7;
3760
+ default:
3761
+ return null;
3762
+ }
3763
+ }
3764
+ function toleranceForField(object, field) {
3765
+ const tolerance = object.tolerances?.find((entry) => entry.field === field)?.value;
3766
+ if (typeof tolerance === "number") {
3767
+ return tolerance;
3768
+ }
3769
+ if (tolerance && typeof tolerance === "object" && "value" in tolerance) {
3770
+ return durationInDays(tolerance) ?? 0;
3771
+ }
3772
+ return 0;
3773
+ }
3774
+ function formatDays(days) {
3775
+ return `${Math.round(days * 100) / 100}d`;
3776
+ }
3777
+ function error(code, message, objectId, field) {
3778
+ return { code, severity: "error", source: "validate", message, objectId, field };
3779
+ }
3780
+ function warn(code, message, objectId, field) {
3781
+ return { code, severity: "warning", source: "validate", message, objectId, field };
3782
+ }
3783
+ function info(code, message, objectId, field) {
3784
+ return { code, severity: "info", source: "validate", message, objectId, field };
3785
+ }
3786
+
3787
+ // packages/core/dist/draft-parse.js
3788
+ var STRUCTURED_TYPED_BLOCKS = /* @__PURE__ */ new Set([
3789
+ "climate",
3790
+ "habitability",
3791
+ "settlement"
3792
+ ]);
3793
+ var DRAFT_OBJECT_FIELD_SPECS = /* @__PURE__ */ new Map();
3794
+ for (const key of [
3795
+ "orbit",
3796
+ "distance",
3797
+ "semiMajor",
3798
+ "eccentricity",
3799
+ "period",
3800
+ "angle",
3801
+ "inclination",
3802
+ "phase",
3803
+ "at",
3804
+ "surface",
3805
+ "free",
3806
+ "kind",
3807
+ "class",
3808
+ "culture",
3809
+ "tags",
3810
+ "color",
3811
+ "image",
3812
+ "hidden",
3813
+ "radius",
3814
+ "mass",
3815
+ "density",
3816
+ "gravity",
3817
+ "temperature",
3818
+ "albedo",
3819
+ "atmosphere",
3820
+ "inner",
3821
+ "outer",
3822
+ "on",
3823
+ "source",
3824
+ "cycle"
3825
+ ]) {
3826
+ const schema = getFieldSchema(key);
3827
+ if (schema) {
3828
+ DRAFT_OBJECT_FIELD_SPECS.set(key, {
3829
+ key,
3830
+ version: "2.0",
3831
+ inlineMode: schema.arity === "multiple" ? "multiple" : "single",
3832
+ allowRepeat: false,
3833
+ legacySchema: schema
3834
+ });
3835
+ }
3836
+ }
3837
+ for (const spec of [
3838
+ { key: "groups", inlineMode: "multiple", allowRepeat: false },
3839
+ { key: "epoch", inlineMode: "single", allowRepeat: false },
3840
+ { key: "referencePlane", inlineMode: "single", allowRepeat: false },
3841
+ { key: "tidalLock", inlineMode: "single", allowRepeat: false },
3842
+ { key: "renderLabel", inlineMode: "single", allowRepeat: false },
3843
+ { key: "renderOrbit", inlineMode: "single", allowRepeat: false },
3844
+ { key: "renderPriority", inlineMode: "single", allowRepeat: false },
3845
+ { key: "resonance", inlineMode: "pair", allowRepeat: false },
3846
+ { key: "derive", inlineMode: "pair", allowRepeat: true },
3847
+ { key: "validate", inlineMode: "single", allowRepeat: true },
3848
+ { key: "locked", inlineMode: "multiple", allowRepeat: false },
3849
+ { key: "tolerance", inlineMode: "pair", allowRepeat: true }
3850
+ ]) {
3851
+ DRAFT_OBJECT_FIELD_SPECS.set(spec.key, {
3852
+ key: spec.key,
3853
+ version: "2.1",
3854
+ inlineMode: spec.inlineMode,
3855
+ allowRepeat: spec.allowRepeat
3856
+ });
3857
+ }
3858
+ var DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
3859
+ function parseWorldOrbitAtlas(source) {
3860
+ return parseAtlasSource(source);
3861
+ }
3862
+ function parseWorldOrbitDraft(source) {
3863
+ return parseAtlasSource(source, "2.0-draft");
3864
+ }
3865
+ function parseAtlasSource(source, forcedOutputVersion) {
3866
+ const prepared = preprocessAtlasSource(source);
3867
+ const lines = prepared.source.split(/\r?\n/);
3868
+ const diagnostics = [];
3869
+ let sawSchemaHeader = false;
3870
+ let sourceSchemaVersion = "2.0";
3871
+ let system = null;
3872
+ let section = null;
3873
+ const objectNodes = [];
3874
+ const groups = [];
3875
+ const relations = [];
3876
+ let sawDefaults = false;
3877
+ let sawAtlas = false;
3878
+ const viewpointIds = /* @__PURE__ */ new Set();
3879
+ const annotationIds = /* @__PURE__ */ new Set();
3880
+ const groupIds = /* @__PURE__ */ new Set();
3881
+ const relationIds = /* @__PURE__ */ new Set();
3882
+ for (let index = 0; index < lines.length; index++) {
3883
+ const rawLine = lines[index];
3884
+ const lineNumber = index + 1;
3885
+ if (!rawLine.trim()) {
3886
+ continue;
3887
+ }
3888
+ const indent = getIndent(rawLine);
3889
+ const tokens = tokenizeLineDetailed(rawLine.slice(indent), {
3890
+ line: lineNumber,
3891
+ columnOffset: indent
3892
+ });
3893
+ if (tokens.length === 0) {
3894
+ continue;
3895
+ }
3896
+ if (!sawSchemaHeader) {
3897
+ sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
3898
+ sawSchemaHeader = true;
3899
+ if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
3900
+ diagnostics.push({
3901
+ code: "parse.schema21.commentCompatibility",
3902
+ severity: "warning",
3903
+ source: "parse",
3904
+ message: `Comments require schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
3905
+ line: prepared.comments[0].line,
3906
+ column: prepared.comments[0].column
3907
+ });
3908
+ }
3909
+ continue;
3910
+ }
3911
+ if (indent === 0) {
3912
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, { sawDefaults, sawAtlas });
3913
+ if (section.kind === "system") {
3914
+ system = section.system;
3915
+ } else if (section.kind === "defaults") {
3916
+ sawDefaults = true;
3917
+ } else if (section.kind === "atlas") {
3918
+ sawAtlas = true;
3919
+ }
3920
+ continue;
3921
+ }
3922
+ if (!section) {
3923
+ throw new WorldOrbitError("Indented line without parent atlas section", lineNumber, indent + 1);
3924
+ }
3925
+ handleSectionLine(section, indent, tokens, lineNumber);
3926
+ }
3927
+ if (!sawSchemaHeader) {
3928
+ throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
3929
+ }
3930
+ const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
3931
+ const outputVersion = forcedOutputVersion ?? (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
3932
+ const baseDocument = {
3933
+ format: "worldorbit",
3934
+ sourceVersion: "1.0",
3935
+ system,
3936
+ groups,
3937
+ relations,
3938
+ objects,
3939
+ diagnostics
3940
+ };
3941
+ if (outputVersion === "2.0-draft") {
3942
+ const document2 = {
3943
+ ...baseDocument,
3944
+ version: "2.0-draft",
3945
+ schemaVersion: "2.0-draft"
3946
+ };
3947
+ document2.diagnostics.push(...collectAtlasDiagnostics(document2, sourceSchemaVersion));
3948
+ return document2;
3949
+ }
3950
+ const document = {
3951
+ ...baseDocument,
3952
+ version: outputVersion,
3953
+ schemaVersion: outputVersion
3954
+ };
3955
+ if (sourceSchemaVersion === "2.0-draft") {
3956
+ document.diagnostics.push({
3957
+ code: "load.schema.deprecatedDraft",
3958
+ severity: "warning",
3959
+ source: "upgrade",
3960
+ message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".'
3961
+ });
3962
+ }
3963
+ document.diagnostics.push(...collectAtlasDiagnostics(document, sourceSchemaVersion));
3964
+ return document;
3965
+ }
3966
+ function assertDraftSchemaHeader(tokens, line) {
3967
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
3968
+ throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
3969
+ }
3970
+ const version = tokens[1].value.toLowerCase();
3971
+ return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
3972
+ }
3973
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, flags) {
3974
+ const keyword = tokens[0]?.value.toLowerCase();
3975
+ switch (keyword) {
3976
+ case "system":
3977
+ if (system) {
3978
+ throw new WorldOrbitError('Atlas section "system" may only appear once', line, tokens[0].column);
3979
+ }
3980
+ return startSystemSection(tokens, line, sourceSchemaVersion, diagnostics);
3981
+ case "defaults":
3982
+ if (!system) {
3983
+ throw new WorldOrbitError('Atlas section "defaults" requires a preceding system declaration', line, tokens[0].column);
3984
+ }
3985
+ if (flags.sawDefaults) {
3986
+ throw new WorldOrbitError('Atlas section "defaults" may only appear once', line, tokens[0].column);
3987
+ }
3988
+ return {
3989
+ kind: "defaults",
3990
+ system,
3991
+ seenFields: /* @__PURE__ */ new Set()
3992
+ };
3993
+ case "atlas":
3994
+ if (!system) {
3995
+ throw new WorldOrbitError('Atlas section "atlas" requires a preceding system declaration', line, tokens[0].column);
3996
+ }
3997
+ if (flags.sawAtlas) {
3998
+ throw new WorldOrbitError('Atlas section "atlas" may only appear once', line, tokens[0].column);
3999
+ }
4000
+ return {
4001
+ kind: "atlas",
4002
+ system,
4003
+ inMetadata: false,
4004
+ metadataIndent: null
4005
+ };
4006
+ case "viewpoint":
4007
+ if (!system) {
4008
+ throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
4009
+ }
4010
+ return startViewpointSection(tokens, line, system, viewpointIds);
4011
+ case "annotation":
4012
+ if (!system) {
4013
+ throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
4014
+ }
4015
+ return startAnnotationSection(tokens, line, system, annotationIds);
4016
+ case "group":
4017
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "group", { line, column: tokens[0].column });
4018
+ return startGroupSection(tokens, line, groups, groupIds);
4019
+ case "relation":
4020
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
4021
+ return startRelationSection(tokens, line, relations, relationIds);
4022
+ case "object":
4023
+ return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
4024
+ default:
4025
+ throw new WorldOrbitError(`Unknown atlas section "${tokens[0]?.value ?? ""}"`, line, tokens[0]?.column ?? 1);
4026
+ }
4027
+ }
4028
+ function startSystemSection(tokens, line, sourceSchemaVersion, diagnostics) {
4029
+ if (tokens.length !== 2) {
4030
+ throw new WorldOrbitError("Invalid atlas system declaration", line, tokens[0]?.column ?? 1);
4031
+ }
4032
+ const system = {
4033
+ type: "system",
4034
+ id: tokens[1].value,
4035
+ title: null,
4036
+ description: null,
4037
+ epoch: null,
4038
+ referencePlane: null,
4039
+ defaults: {
4040
+ view: "topdown",
4041
+ scale: null,
4042
+ units: null,
4043
+ preset: null,
4044
+ theme: null
4045
+ },
4046
+ atlasMetadata: {},
4047
+ viewpoints: [],
4048
+ annotations: []
4049
+ };
4050
+ return {
4051
+ kind: "system",
4052
+ system,
4053
+ sourceSchemaVersion,
4054
+ diagnostics,
4055
+ seenFields: /* @__PURE__ */ new Set()
4056
+ };
4057
+ }
4058
+ function startViewpointSection(tokens, line, system, viewpointIds) {
4059
+ if (tokens.length !== 2) {
4060
+ throw new WorldOrbitError("Invalid viewpoint declaration", line, tokens[0]?.column ?? 1);
4061
+ }
4062
+ const id = normalizeIdentifier2(tokens[1].value);
4063
+ if (!id) {
4064
+ throw new WorldOrbitError("Viewpoint id must not be empty", line, tokens[1].column);
3263
4065
  }
3264
4066
  if (viewpointIds.has(id)) {
3265
4067
  throw new WorldOrbitError(`Duplicate viewpoint id "${id}"`, line, tokens[1].column);
@@ -3315,7 +4117,64 @@ var WorldOrbit = (() => {
3315
4117
  seenFields: /* @__PURE__ */ new Set()
3316
4118
  };
3317
4119
  }
3318
- function startObjectSection(tokens, line, objectNodes) {
4120
+ function startGroupSection(tokens, line, groups, groupIds) {
4121
+ if (tokens.length !== 2) {
4122
+ throw new WorldOrbitError("Invalid group declaration", line, tokens[0]?.column ?? 1);
4123
+ }
4124
+ const id = normalizeIdentifier2(tokens[1].value);
4125
+ if (!id) {
4126
+ throw new WorldOrbitError("Group id must not be empty", line, tokens[1].column);
4127
+ }
4128
+ if (groupIds.has(id)) {
4129
+ throw new WorldOrbitError(`Duplicate group id "${id}"`, line, tokens[1].column);
4130
+ }
4131
+ const group = {
4132
+ id,
4133
+ label: humanizeIdentifier3(id),
4134
+ summary: "",
4135
+ color: null,
4136
+ tags: [],
4137
+ hidden: false
4138
+ };
4139
+ groups.push(group);
4140
+ groupIds.add(id);
4141
+ return {
4142
+ kind: "group",
4143
+ group,
4144
+ seenFields: /* @__PURE__ */ new Set()
4145
+ };
4146
+ }
4147
+ function startRelationSection(tokens, line, relations, relationIds) {
4148
+ if (tokens.length !== 2) {
4149
+ throw new WorldOrbitError("Invalid relation declaration", line, tokens[0]?.column ?? 1);
4150
+ }
4151
+ const id = normalizeIdentifier2(tokens[1].value);
4152
+ if (!id) {
4153
+ throw new WorldOrbitError("Relation id must not be empty", line, tokens[1].column);
4154
+ }
4155
+ if (relationIds.has(id)) {
4156
+ throw new WorldOrbitError(`Duplicate relation id "${id}"`, line, tokens[1].column);
4157
+ }
4158
+ const relation = {
4159
+ id,
4160
+ from: "",
4161
+ to: "",
4162
+ kind: "",
4163
+ label: null,
4164
+ summary: null,
4165
+ tags: [],
4166
+ color: null,
4167
+ hidden: false
4168
+ };
4169
+ relations.push(relation);
4170
+ relationIds.add(id);
4171
+ return {
4172
+ kind: "relation",
4173
+ relation,
4174
+ seenFields: /* @__PURE__ */ new Set()
4175
+ };
4176
+ }
4177
+ function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
3319
4178
  if (tokens.length < 3) {
3320
4179
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
3321
4180
  }
@@ -3326,12 +4185,11 @@ var WorldOrbit = (() => {
3326
4185
  throw new WorldOrbitError(`Unknown object type "${objectTypeToken.value}"`, line, objectTypeToken.column);
3327
4186
  }
3328
4187
  const objectNode = {
3329
- type: "object",
3330
4188
  objectType,
3331
- name: idToken.value,
3332
- inlineFields: parseInlineFields2(tokens.slice(3), line),
3333
- blockFields: [],
4189
+ id: idToken.value,
4190
+ fields: parseInlineObjectFields(tokens.slice(3), line, objectType, sourceSchemaVersion, diagnostics),
3334
4191
  infoEntries: [],
4192
+ typedBlockEntries: {},
3335
4193
  location: {
3336
4194
  line,
3337
4195
  column: objectTypeToken.column
@@ -3341,8 +4199,12 @@ var WorldOrbit = (() => {
3341
4199
  return {
3342
4200
  kind: "object",
3343
4201
  objectNode,
3344
- inInfoBlock: false,
3345
- infoIndent: null
4202
+ sourceSchemaVersion,
4203
+ diagnostics,
4204
+ activeBlock: null,
4205
+ blockIndent: null,
4206
+ seenInfoKeys: /* @__PURE__ */ new Set(),
4207
+ seenTypedBlockKeys: {}
3346
4208
  };
3347
4209
  }
3348
4210
  function handleSectionLine(section, indent, tokens, line) {
@@ -3362,6 +4224,12 @@ var WorldOrbit = (() => {
3362
4224
  case "annotation":
3363
4225
  applyAnnotationField(section, tokens, line);
3364
4226
  return;
4227
+ case "group":
4228
+ applyGroupField(section, tokens, line);
4229
+ return;
4230
+ case "relation":
4231
+ applyRelationField(section, tokens, line);
4232
+ return;
3365
4233
  case "object":
3366
4234
  applyObjectField(section, indent, tokens, line);
3367
4235
  return;
@@ -3369,10 +4237,35 @@ var WorldOrbit = (() => {
3369
4237
  }
3370
4238
  function applySystemField(section, tokens, line) {
3371
4239
  const key = requireUniqueField(tokens, section.seenFields, line);
3372
- if (key !== "title") {
3373
- throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
4240
+ const value = joinFieldValue(tokens, line);
4241
+ switch (key) {
4242
+ case "title":
4243
+ section.system.title = value;
4244
+ return;
4245
+ case "description":
4246
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
4247
+ line,
4248
+ column: tokens[0].column
4249
+ });
4250
+ section.system.description = value;
4251
+ return;
4252
+ case "epoch":
4253
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
4254
+ line,
4255
+ column: tokens[0].column
4256
+ });
4257
+ section.system.epoch = value;
4258
+ return;
4259
+ case "referenceplane":
4260
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "referencePlane", {
4261
+ line,
4262
+ column: tokens[0].column
4263
+ });
4264
+ section.system.referencePlane = value;
4265
+ return;
4266
+ default:
4267
+ throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
3374
4268
  }
3375
- section.system.title = joinFieldValue(tokens, line);
3376
4269
  }
3377
4270
  function applyDefaultsField(section, tokens, line) {
3378
4271
  const key = requireUniqueField(tokens, section.seenFields, line);
@@ -3403,14 +4296,11 @@ var WorldOrbit = (() => {
3403
4296
  section.metadataIndent = null;
3404
4297
  }
3405
4298
  if (section.inMetadata) {
3406
- if (tokens.length < 2) {
3407
- throw new WorldOrbitError("Invalid atlas metadata entry", line, tokens[0]?.column ?? 1);
3408
- }
3409
- const key = tokens[0].value;
3410
- if (key in section.system.atlasMetadata) {
3411
- throw new WorldOrbitError(`Duplicate atlas metadata key "${key}"`, line, tokens[0].column);
4299
+ const entry = parseInfoLikeEntry(tokens, line, "Invalid atlas metadata entry");
4300
+ if (entry.key in section.system.atlasMetadata) {
4301
+ throw new WorldOrbitError(`Duplicate atlas metadata key "${entry.key}"`, line, tokens[0].column);
3412
4302
  }
3413
- section.system.atlasMetadata[key] = joinFieldValue(tokens, line);
4303
+ section.system.atlasMetadata[entry.key] = entry.value;
3414
4304
  return;
3415
4305
  }
3416
4306
  if (tokens.length === 1 && tokens[0].value.toLowerCase() === "metadata") {
@@ -3483,50 +4373,131 @@ var WorldOrbit = (() => {
3483
4373
  filter.objectTypes = parseObjectTypeTokens(tokens.slice(1), line);
3484
4374
  break;
3485
4375
  case "tags":
3486
- filter.tags = parseTokenList(tokens.slice(1), line, "tags");
3487
- break;
3488
- case "groups":
3489
- filter.groupIds = parseTokenList(tokens.slice(1), line, "groups");
3490
- break;
4376
+ filter.tags = parseTokenList(tokens.slice(1), line, "tags");
4377
+ break;
4378
+ case "groups":
4379
+ filter.groupIds = parseTokenList(tokens.slice(1), line, "groups");
4380
+ break;
4381
+ default:
4382
+ throw new WorldOrbitError(`Unknown viewpoint filter field "${tokens[0].value}"`, line, tokens[0].column);
4383
+ }
4384
+ section.viewpoint.filter = filter;
4385
+ }
4386
+ function applyAnnotationField(section, tokens, line) {
4387
+ const key = requireUniqueField(tokens, section.seenFields, line);
4388
+ switch (key) {
4389
+ case "label":
4390
+ section.annotation.label = joinFieldValue(tokens, line);
4391
+ return;
4392
+ case "target":
4393
+ section.annotation.targetObjectId = joinFieldValue(tokens, line);
4394
+ return;
4395
+ case "body":
4396
+ section.annotation.body = joinFieldValue(tokens, line);
4397
+ return;
4398
+ case "tags":
4399
+ section.annotation.tags = parseTokenList(tokens.slice(1), line, "tags");
4400
+ return;
4401
+ default:
4402
+ throw new WorldOrbitError(`Unknown annotation field "${tokens[0].value}"`, line, tokens[0].column);
4403
+ }
4404
+ }
4405
+ function applyGroupField(section, tokens, line) {
4406
+ const key = requireUniqueField(tokens, section.seenFields, line);
4407
+ switch (key) {
4408
+ case "label":
4409
+ section.group.label = joinFieldValue(tokens, line);
4410
+ return;
4411
+ case "summary":
4412
+ section.group.summary = joinFieldValue(tokens, line);
4413
+ return;
4414
+ case "color":
4415
+ section.group.color = joinFieldValue(tokens, line);
4416
+ return;
4417
+ case "tags":
4418
+ section.group.tags = parseTokenList(tokens.slice(1), line, "tags");
4419
+ return;
4420
+ case "hidden":
4421
+ section.group.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4422
+ line,
4423
+ column: tokens[0].column
4424
+ });
4425
+ return;
3491
4426
  default:
3492
- throw new WorldOrbitError(`Unknown viewpoint filter field "${tokens[0].value}"`, line, tokens[0].column);
4427
+ throw new WorldOrbitError(`Unknown group field "${tokens[0].value}"`, line, tokens[0].column);
3493
4428
  }
3494
- section.viewpoint.filter = filter;
3495
4429
  }
3496
- function applyAnnotationField(section, tokens, line) {
4430
+ function applyRelationField(section, tokens, line) {
3497
4431
  const key = requireUniqueField(tokens, section.seenFields, line);
3498
4432
  switch (key) {
3499
- case "label":
3500
- section.annotation.label = joinFieldValue(tokens, line);
4433
+ case "from":
4434
+ section.relation.from = joinFieldValue(tokens, line);
3501
4435
  return;
3502
- case "target":
3503
- section.annotation.targetObjectId = joinFieldValue(tokens, line);
4436
+ case "to":
4437
+ section.relation.to = joinFieldValue(tokens, line);
3504
4438
  return;
3505
- case "body":
3506
- section.annotation.body = joinFieldValue(tokens, line);
4439
+ case "kind":
4440
+ section.relation.kind = joinFieldValue(tokens, line);
4441
+ return;
4442
+ case "label":
4443
+ section.relation.label = joinFieldValue(tokens, line);
4444
+ return;
4445
+ case "summary":
4446
+ section.relation.summary = joinFieldValue(tokens, line);
3507
4447
  return;
3508
4448
  case "tags":
3509
- section.annotation.tags = parseTokenList(tokens.slice(1), line, "tags");
4449
+ section.relation.tags = parseTokenList(tokens.slice(1), line, "tags");
4450
+ return;
4451
+ case "color":
4452
+ section.relation.color = joinFieldValue(tokens, line);
4453
+ return;
4454
+ case "hidden":
4455
+ section.relation.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4456
+ line,
4457
+ column: tokens[0].column
4458
+ });
3510
4459
  return;
3511
4460
  default:
3512
- throw new WorldOrbitError(`Unknown annotation field "${tokens[0].value}"`, line, tokens[0].column);
4461
+ throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
3513
4462
  }
3514
4463
  }
3515
4464
  function applyObjectField(section, indent, tokens, line) {
3516
- if (tokens.length === 1 && tokens[0].value === "info") {
3517
- section.inInfoBlock = true;
3518
- section.infoIndent = indent;
3519
- return;
3520
- }
3521
- if (section.inInfoBlock && indent <= (section.infoIndent ?? 0)) {
3522
- section.inInfoBlock = false;
3523
- section.infoIndent = null;
4465
+ if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
4466
+ section.activeBlock = null;
4467
+ section.blockIndent = null;
4468
+ }
4469
+ if (tokens.length === 1) {
4470
+ const blockName = tokens[0].value.toLowerCase();
4471
+ if (blockName === "info" || STRUCTURED_TYPED_BLOCKS.has(blockName)) {
4472
+ if (blockName !== "info") {
4473
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, blockName, { line, column: tokens[0].column });
4474
+ }
4475
+ section.activeBlock = blockName;
4476
+ section.blockIndent = indent;
4477
+ return;
4478
+ }
3524
4479
  }
3525
- if (section.inInfoBlock) {
3526
- section.objectNode.infoEntries.push(parseInfoEntry2(tokens, line));
4480
+ if (section.activeBlock) {
4481
+ const entry = parseInfoLikeEntry(tokens, line, `Invalid ${section.activeBlock} entry`);
4482
+ if (section.activeBlock === "info") {
4483
+ if (section.seenInfoKeys.has(entry.key)) {
4484
+ throw new WorldOrbitError(`Duplicate info key "${entry.key}"`, line, tokens[0].column);
4485
+ }
4486
+ section.seenInfoKeys.add(entry.key);
4487
+ section.objectNode.infoEntries.push(entry);
4488
+ return;
4489
+ }
4490
+ const typedBlock = section.activeBlock;
4491
+ const seenKeys = section.seenTypedBlockKeys[typedBlock] ?? (section.seenTypedBlockKeys[typedBlock] = /* @__PURE__ */ new Set());
4492
+ if (seenKeys.has(entry.key)) {
4493
+ throw new WorldOrbitError(`Duplicate ${typedBlock} key "${entry.key}"`, line, tokens[0].column);
4494
+ }
4495
+ seenKeys.add(entry.key);
4496
+ const entries = section.objectNode.typedBlockEntries[typedBlock] ?? (section.objectNode.typedBlockEntries[typedBlock] = []);
4497
+ entries.push(entry);
3527
4498
  return;
3528
4499
  }
3529
- section.objectNode.blockFields.push(parseField2(tokens, line));
4500
+ section.objectNode.fields.push(parseObjectField(tokens, line, section.objectNode.objectType, section.sourceSchemaVersion, section.diagnostics));
3530
4501
  }
3531
4502
  function requireUniqueField(tokens, seenFields, line) {
3532
4503
  if (tokens.length < 2) {
@@ -3546,50 +4517,40 @@ var WorldOrbit = (() => {
3546
4517
  return tokens.slice(1).map((token) => token.value).join(" ").trim();
3547
4518
  }
3548
4519
  function parseObjectTypeTokens(tokens, line) {
3549
- if (tokens.length === 0) {
3550
- throw new WorldOrbitError("Missing value for atlas field", line);
3551
- }
3552
- return tokens.map((token) => {
3553
- const value = token.value;
3554
- if (value !== "star" && value !== "planet" && value !== "moon" && value !== "belt" && value !== "asteroid" && value !== "comet" && value !== "ring" && value !== "structure" && value !== "phenomenon") {
3555
- throw new WorldOrbitError(`Unknown viewpoint object type "${token.value}"`, line, token.column);
3556
- }
3557
- return value;
3558
- });
3559
- }
3560
- function parseTokenList(tokens, line, field) {
3561
- if (tokens.length === 0) {
3562
- throw new WorldOrbitError(`Missing value for field "${field}"`, line);
3563
- }
3564
- return tokens.map((token) => token.value);
4520
+ 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");
3565
4521
  }
3566
4522
  function parseLayerTokens(tokens, line) {
3567
- if (tokens.length === 0) {
3568
- throw new WorldOrbitError('Missing value for field "layers"', line);
3569
- }
3570
- const next = {};
3571
- for (const token of tokens) {
3572
- const enabled = !token.value.startsWith("-") && !token.value.startsWith("!");
3573
- const rawLayer = token.value.replace(/^[-!]+/, "").toLowerCase();
3574
- if (rawLayer === "orbits") {
3575
- next["orbits-back"] = enabled;
3576
- next["orbits-front"] = enabled;
4523
+ const layers = {};
4524
+ for (const token of parseTokenList(tokens, line, "layers")) {
4525
+ const enabled = !token.startsWith("-") && !token.startsWith("!");
4526
+ const raw = token.replace(/^[-!]+/, "").toLowerCase();
4527
+ if (raw === "orbits") {
4528
+ layers["orbits-back"] = enabled;
4529
+ layers["orbits-front"] = enabled;
3577
4530
  continue;
3578
4531
  }
3579
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
3580
- next[rawLayer] = enabled;
3581
- continue;
4532
+ if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "objects" || raw === "labels" || raw === "metadata") {
4533
+ layers[raw] = enabled;
3582
4534
  }
3583
- throw new WorldOrbitError(`Unknown layer token "${token.value}"`, line, token.column);
3584
4535
  }
3585
- return next;
4536
+ return layers;
4537
+ }
4538
+ function parseTokenList(tokens, line, fieldName) {
4539
+ if (tokens.length === 0) {
4540
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, 1);
4541
+ }
4542
+ const values = tokens.map((token) => token.value).filter(Boolean);
4543
+ if (values.length === 0) {
4544
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, tokens[0]?.column ?? 1);
4545
+ }
4546
+ return values;
3586
4547
  }
3587
4548
  function parseProjectionValue(value, line, column) {
3588
4549
  const normalized = value.toLowerCase();
3589
- if (normalized === "topdown" || normalized === "isometric") {
3590
- return normalized;
4550
+ if (normalized !== "topdown" && normalized !== "isometric") {
4551
+ throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
3591
4552
  }
3592
- throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
4553
+ return normalized;
3593
4554
  }
3594
4555
  function parsePresetValue(value, line, column) {
3595
4556
  const normalized = value.toLowerCase();
@@ -3599,16 +4560,16 @@ var WorldOrbit = (() => {
3599
4560
  throw new WorldOrbitError(`Unknown render preset "${value}"`, line, column);
3600
4561
  }
3601
4562
  function parsePositiveNumber2(value, line, column, field) {
3602
- const parsed = Number(value);
3603
- if (!Number.isFinite(parsed) || parsed <= 0) {
3604
- throw new WorldOrbitError(`Field "${field}" expects a positive number`, line, column);
4563
+ const parsed = parseFiniteNumber2(value, line, column, field);
4564
+ if (parsed <= 0) {
4565
+ throw new WorldOrbitError(`Field "${field}" must be greater than zero`, line, column);
3605
4566
  }
3606
4567
  return parsed;
3607
4568
  }
3608
4569
  function parseFiniteNumber2(value, line, column, field) {
3609
4570
  const parsed = Number(value);
3610
4571
  if (!Number.isFinite(parsed)) {
3611
- throw new WorldOrbitError(`Field "${field}" expects a finite number`, line, column);
4572
+ throw new WorldOrbitError(`Invalid numeric value "${value}" for "${field}"`, line, column);
3612
4573
  }
3613
4574
  return parsed;
3614
4575
  }
@@ -3620,28 +4581,43 @@ var WorldOrbit = (() => {
3620
4581
  groupIds: []
3621
4582
  };
3622
4583
  }
3623
- function parseInlineFields2(tokens, line) {
4584
+ function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
3624
4585
  const fields = [];
3625
4586
  let index = 0;
3626
4587
  while (index < tokens.length) {
3627
4588
  const keyToken = tokens[index];
3628
- const schema = getFieldSchema(keyToken.value);
3629
- if (!schema) {
4589
+ const spec = getDraftObjectFieldSpec(keyToken.value);
4590
+ if (!spec) {
3630
4591
  throw new WorldOrbitError(`Unknown field "${keyToken.value}"`, line, keyToken.column);
3631
4592
  }
4593
+ if (spec.version === "2.1") {
4594
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, keyToken.value, {
4595
+ line,
4596
+ column: keyToken.column
4597
+ });
4598
+ }
3632
4599
  index++;
3633
4600
  const valueTokens = [];
3634
- if (schema.arity === "multiple") {
3635
- while (index < tokens.length && !isKnownFieldKey(tokens[index].value)) {
3636
- valueTokens.push(tokens[index]);
3637
- index++;
3638
- }
3639
- } else {
4601
+ if (spec.inlineMode === "single") {
3640
4602
  const nextToken = tokens[index];
3641
4603
  if (nextToken) {
3642
4604
  valueTokens.push(nextToken);
3643
4605
  index++;
3644
4606
  }
4607
+ } else if (spec.inlineMode === "pair") {
4608
+ for (let count = 0; count < 2; count++) {
4609
+ const nextToken = tokens[index];
4610
+ if (!nextToken) {
4611
+ break;
4612
+ }
4613
+ valueTokens.push(nextToken);
4614
+ index++;
4615
+ }
4616
+ } else {
4617
+ while (index < tokens.length && !DRAFT_OBJECT_FIELD_KEYS.has(tokens[index].value)) {
4618
+ valueTokens.push(tokens[index]);
4619
+ index++;
4620
+ }
3645
4621
  }
3646
4622
  if (valueTokens.length === 0) {
3647
4623
  throw new WorldOrbitError(`Missing value for field "${keyToken.value}"`, line, keyToken.column);
@@ -3653,25 +4629,35 @@ var WorldOrbit = (() => {
3653
4629
  location: { line, column: keyToken.column }
3654
4630
  });
3655
4631
  }
4632
+ validateDraftObjectFieldCompatibility(fields, objectType);
3656
4633
  return fields;
3657
4634
  }
3658
- function parseField2(tokens, line) {
4635
+ function parseObjectField(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
3659
4636
  if (tokens.length < 2) {
3660
4637
  throw new WorldOrbitError("Invalid field line", line, tokens[0]?.column ?? 1);
3661
4638
  }
3662
- if (!getFieldSchema(tokens[0].value)) {
4639
+ const spec = getDraftObjectFieldSpec(tokens[0].value);
4640
+ if (!spec) {
3663
4641
  throw new WorldOrbitError(`Unknown field "${tokens[0].value}"`, line, tokens[0].column);
3664
4642
  }
3665
- return {
4643
+ if (spec.version === "2.1") {
4644
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, tokens[0].value, {
4645
+ line,
4646
+ column: tokens[0].column
4647
+ });
4648
+ }
4649
+ const field = {
3666
4650
  type: "field",
3667
4651
  key: tokens[0].value,
3668
4652
  values: tokens.slice(1).map((token) => token.value),
3669
4653
  location: { line, column: tokens[0].column }
3670
4654
  };
4655
+ validateDraftObjectFieldCompatibility([field], objectType);
4656
+ return field;
3671
4657
  }
3672
- function parseInfoEntry2(tokens, line) {
4658
+ function parseInfoLikeEntry(tokens, line, errorMessage) {
3673
4659
  if (tokens.length < 2) {
3674
- throw new WorldOrbitError("Invalid info entry", line, tokens[0]?.column ?? 1);
4660
+ throw new WorldOrbitError(errorMessage, line, tokens[0]?.column ?? 1);
3675
4661
  }
3676
4662
  return {
3677
4663
  type: "info-entry",
@@ -3680,23 +4666,356 @@ var WorldOrbit = (() => {
3680
4666
  location: { line, column: tokens[0].column }
3681
4667
  };
3682
4668
  }
3683
- function normalizeIdentifier2(value) {
3684
- return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
4669
+ function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
4670
+ const fieldMap = collectDraftFields(node.fields);
4671
+ const placement = extractDraftPlacement(node.objectType, fieldMap);
4672
+ const properties = normalizeDraftProperties(node.objectType, fieldMap);
4673
+ const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
4674
+ const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
4675
+ const referencePlane = parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0]);
4676
+ const tidalLock = fieldMap.has("tidalLock") ? parseAtlasBoolean(singleFieldValue2(fieldMap.get("tidalLock")[0]), "tidalLock", fieldMap.get("tidalLock")[0].location) : void 0;
4677
+ const resonance = fieldMap.has("resonance") ? parseResonanceField(fieldMap.get("resonance")[0]) : void 0;
4678
+ const renderHints = extractRenderHints(fieldMap);
4679
+ const deriveRules = fieldMap.get("derive")?.map((field) => parseDeriveField(field));
4680
+ const validationRules = fieldMap.get("validate")?.map((field) => ({
4681
+ rule: singleFieldValue2(field)
4682
+ }));
4683
+ const lockedFields = fieldMap.has("locked") ? [...new Set(fieldMap.get("locked").flatMap((field) => field.values))] : void 0;
4684
+ const tolerances = fieldMap.get("tolerance")?.map((field) => parseToleranceField(field));
4685
+ const typedBlocks = normalizeTypedBlocks(node.typedBlockEntries);
4686
+ const info2 = normalizeInfoEntries(node.infoEntries, "info");
4687
+ const object = {
4688
+ type: node.objectType,
4689
+ id: node.id,
4690
+ properties,
4691
+ placement,
4692
+ info: info2
4693
+ };
4694
+ if (groups.length > 0)
4695
+ object.groups = groups;
4696
+ if (epoch)
4697
+ object.epoch = epoch;
4698
+ if (referencePlane)
4699
+ object.referencePlane = referencePlane;
4700
+ if (tidalLock !== void 0)
4701
+ object.tidalLock = tidalLock;
4702
+ if (resonance)
4703
+ object.resonance = resonance;
4704
+ if (renderHints)
4705
+ object.renderHints = renderHints;
4706
+ if (deriveRules?.length)
4707
+ object.deriveRules = deriveRules;
4708
+ if (validationRules?.length)
4709
+ object.validationRules = validationRules;
4710
+ if (lockedFields?.length)
4711
+ object.lockedFields = lockedFields;
4712
+ if (tolerances?.length)
4713
+ object.tolerances = tolerances;
4714
+ if (typedBlocks && Object.keys(typedBlocks).length > 0)
4715
+ object.typedBlocks = typedBlocks;
4716
+ if (sourceSchemaVersion !== "2.1") {
4717
+ if (object.groups || object.epoch || object.referencePlane || object.tidalLock !== void 0 || object.resonance || object.renderHints || object.deriveRules?.length || object.validationRules?.length || object.lockedFields?.length || object.tolerances?.length || object.typedBlocks) {
4718
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
4719
+ }
4720
+ }
4721
+ return object;
3685
4722
  }
3686
- function humanizeIdentifier3(value) {
3687
- return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
4723
+ function collectDraftFields(fields) {
4724
+ const grouped = /* @__PURE__ */ new Map();
4725
+ for (const field of fields) {
4726
+ const spec = getDraftObjectFieldSpec(field.key);
4727
+ if (!spec) {
4728
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
4729
+ }
4730
+ if (!spec.allowRepeat && grouped.has(field.key)) {
4731
+ throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
4732
+ }
4733
+ const existing = grouped.get(field.key) ?? [];
4734
+ existing.push(field);
4735
+ grouped.set(field.key, existing);
4736
+ }
4737
+ return grouped;
4738
+ }
4739
+ function extractDraftPlacement(objectType, fieldMap) {
4740
+ const orbitField = fieldMap.get("orbit")?.[0];
4741
+ const atField = fieldMap.get("at")?.[0];
4742
+ const surfaceField = fieldMap.get("surface")?.[0];
4743
+ const freeField = fieldMap.get("free")?.[0];
4744
+ const count = [orbitField, atField, surfaceField, freeField].filter(Boolean).length;
4745
+ if (count > 1) {
4746
+ const conflictingField = orbitField ?? atField ?? surfaceField ?? freeField;
4747
+ throw WorldOrbitError.fromLocation("Object has multiple placement modes", conflictingField?.location);
4748
+ }
4749
+ if (orbitField) {
4750
+ return {
4751
+ mode: "orbit",
4752
+ target: singleFieldValue2(orbitField),
4753
+ distance: parseOptionalUnitField(fieldMap.get("distance")?.[0], "distance"),
4754
+ semiMajor: parseOptionalUnitField(fieldMap.get("semiMajor")?.[0], "semiMajor"),
4755
+ eccentricity: parseOptionalNumberField(fieldMap.get("eccentricity")?.[0], "eccentricity"),
4756
+ period: parseOptionalUnitField(fieldMap.get("period")?.[0], "period"),
4757
+ angle: parseOptionalUnitField(fieldMap.get("angle")?.[0], "angle"),
4758
+ inclination: parseOptionalUnitField(fieldMap.get("inclination")?.[0], "inclination"),
4759
+ phase: parseOptionalUnitField(fieldMap.get("phase")?.[0], "phase")
4760
+ };
4761
+ }
4762
+ if (atField) {
4763
+ const target = singleFieldValue2(atField);
4764
+ return {
4765
+ mode: "at",
4766
+ target,
4767
+ reference: parseAtlasAtReference(target, atField.location)
4768
+ };
4769
+ }
4770
+ if (surfaceField) {
4771
+ return {
4772
+ mode: "surface",
4773
+ target: singleFieldValue2(surfaceField)
4774
+ };
4775
+ }
4776
+ if (freeField) {
4777
+ const raw = singleFieldValue2(freeField);
4778
+ const distance = tryParseAtlasUnitValue(raw);
4779
+ return {
4780
+ mode: "free",
4781
+ distance: distance ?? void 0,
4782
+ descriptor: distance ? void 0 : raw
4783
+ };
4784
+ }
4785
+ return null;
4786
+ }
4787
+ function normalizeDraftProperties(objectType, fieldMap) {
4788
+ const properties = {};
4789
+ for (const [key, fields] of fieldMap.entries()) {
4790
+ const field = fields[0];
4791
+ const spec = getDraftObjectFieldSpec(key);
4792
+ if (!field || !spec?.legacySchema || spec.legacySchema.placement) {
4793
+ continue;
4794
+ }
4795
+ ensureAtlasFieldSupported(key, objectType, field.location);
4796
+ properties[key] = normalizeLegacyScalarValue(key, field.values, field.location);
4797
+ }
4798
+ return properties;
4799
+ }
4800
+ function normalizeInfoEntries(entries, label) {
4801
+ const normalized = {};
4802
+ for (const entry of entries) {
4803
+ if (entry.key in normalized) {
4804
+ throw WorldOrbitError.fromLocation(`Duplicate ${label} key "${entry.key}"`, entry.location);
4805
+ }
4806
+ normalized[entry.key] = entry.value;
4807
+ }
4808
+ return normalized;
4809
+ }
4810
+ function normalizeTypedBlocks(typedBlockEntries) {
4811
+ const typedBlocks = {};
4812
+ for (const blockName of Object.keys(typedBlockEntries)) {
4813
+ const entries = typedBlockEntries[blockName];
4814
+ if (entries?.length) {
4815
+ typedBlocks[blockName] = normalizeInfoEntries(entries, blockName);
4816
+ }
4817
+ }
4818
+ return typedBlocks;
4819
+ }
4820
+ function extractRenderHints(fieldMap) {
4821
+ const renderHints = {};
4822
+ const renderLabelField = fieldMap.get("renderLabel")?.[0];
4823
+ const renderOrbitField = fieldMap.get("renderOrbit")?.[0];
4824
+ const renderPriorityField = fieldMap.get("renderPriority")?.[0];
4825
+ if (renderLabelField) {
4826
+ renderHints.renderLabel = parseAtlasBoolean(singleFieldValue2(renderLabelField), "renderLabel", renderLabelField.location);
4827
+ }
4828
+ if (renderOrbitField) {
4829
+ renderHints.renderOrbit = parseAtlasBoolean(singleFieldValue2(renderOrbitField), "renderOrbit", renderOrbitField.location);
4830
+ }
4831
+ if (renderPriorityField) {
4832
+ renderHints.renderPriority = parseAtlasNumber(singleFieldValue2(renderPriorityField), "renderPriority", renderPriorityField.location);
4833
+ }
4834
+ return Object.keys(renderHints).length > 0 ? renderHints : void 0;
4835
+ }
4836
+ function parseResonanceField(field) {
4837
+ if (field.values.length !== 2) {
4838
+ throw WorldOrbitError.fromLocation('Field "resonance" expects "<targetObjectId> <ratio>"', field.location);
4839
+ }
4840
+ const ratio = field.values[1];
4841
+ if (!/^\d+:\d+$/.test(ratio)) {
4842
+ throw WorldOrbitError.fromLocation(`Invalid resonance ratio "${ratio}"`, field.location);
4843
+ }
4844
+ return {
4845
+ targetObjectId: field.values[0],
4846
+ ratio
4847
+ };
4848
+ }
4849
+ function parseDeriveField(field) {
4850
+ if (field.values.length !== 2) {
4851
+ throw WorldOrbitError.fromLocation('Field "derive" expects "<field> <strategy>"', field.location);
4852
+ }
4853
+ return {
4854
+ field: field.values[0],
4855
+ strategy: field.values[1]
4856
+ };
4857
+ }
4858
+ function parseToleranceField(field) {
4859
+ if (field.values.length !== 2) {
4860
+ throw WorldOrbitError.fromLocation('Field "tolerance" expects "<field> <value>"', field.location);
4861
+ }
4862
+ const rawValue = field.values[1];
4863
+ const unitValue = tryParseAtlasUnitValue(rawValue);
4864
+ const numericValue = Number(rawValue);
4865
+ return {
4866
+ field: field.values[0],
4867
+ value: unitValue ?? (Number.isFinite(numericValue) ? numericValue : rawValue)
4868
+ };
4869
+ }
4870
+ function parseOptionalTokenList(field) {
4871
+ return field ? [...new Set(field.values)] : [];
4872
+ }
4873
+ function parseOptionalJoinedValue(field) {
4874
+ if (!field) {
4875
+ return null;
4876
+ }
4877
+ return field.values.join(" ").trim() || null;
4878
+ }
4879
+ function parseOptionalUnitField(field, key) {
4880
+ return field ? parseAtlasUnitValue(singleFieldValue2(field), field.location, key) : void 0;
4881
+ }
4882
+ function parseOptionalNumberField(field, key) {
4883
+ return field ? parseAtlasNumber(singleFieldValue2(field), key, field.location) : void 0;
4884
+ }
4885
+ function singleFieldValue2(field) {
4886
+ return singleAtlasValue(field.values, field.key, field.location);
4887
+ }
4888
+ function getDraftObjectFieldSpec(key) {
4889
+ return DRAFT_OBJECT_FIELD_SPECS.get(key);
4890
+ }
4891
+ function validateDraftObjectFieldCompatibility(fields, objectType) {
4892
+ for (const field of fields) {
4893
+ const spec = getDraftObjectFieldSpec(field.key);
4894
+ if (!spec) {
4895
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
4896
+ }
4897
+ if (spec.legacySchema) {
4898
+ ensureAtlasFieldSupported(field.key, objectType, field.location);
4899
+ continue;
4900
+ }
4901
+ if ((field.key === "renderLabel" || field.key === "renderOrbit" || field.key === "tidalLock") && field.values.length !== 1) {
4902
+ throw WorldOrbitError.fromLocation(`Field "${field.key}" expects exactly one value`, field.location);
4903
+ }
4904
+ }
4905
+ }
4906
+ function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
4907
+ if (sourceSchemaVersion === "2.1") {
4908
+ return;
4909
+ }
4910
+ diagnostics.push({
4911
+ code: "parse.schema21.featureCompatibility",
4912
+ severity: "warning",
4913
+ source: "parse",
4914
+ message: `Feature "${featureName}" requires schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
4915
+ line: location.line,
4916
+ column: location.column
4917
+ });
4918
+ }
4919
+ function preprocessAtlasSource(source) {
4920
+ const chars = [...source];
4921
+ const comments = [];
4922
+ let inString = false;
4923
+ let inBlockComment = false;
4924
+ let blockCommentStart = null;
4925
+ let line = 1;
4926
+ let column = 1;
4927
+ for (let index = 0; index < chars.length; index++) {
4928
+ const ch = chars[index];
4929
+ const next = chars[index + 1];
4930
+ if (inBlockComment) {
4931
+ if (ch === "*" && next === "/") {
4932
+ chars[index] = " ";
4933
+ chars[index + 1] = " ";
4934
+ inBlockComment = false;
4935
+ blockCommentStart = null;
4936
+ index++;
4937
+ column += 2;
4938
+ continue;
4939
+ }
4940
+ if (ch !== "\n" && ch !== "\r") {
4941
+ chars[index] = " ";
4942
+ }
4943
+ if (ch === "\n") {
4944
+ line++;
4945
+ column = 1;
4946
+ } else {
4947
+ column++;
4948
+ }
4949
+ continue;
4950
+ }
4951
+ if (!inString && ch === "/" && next === "*") {
4952
+ comments.push({ kind: "block", line, column });
4953
+ chars[index] = " ";
4954
+ chars[index + 1] = " ";
4955
+ inBlockComment = true;
4956
+ blockCommentStart = { line, column };
4957
+ index++;
4958
+ column += 2;
4959
+ continue;
4960
+ }
4961
+ if (!inString && ch === "#" && !isHexColorLiteral(chars, index)) {
4962
+ comments.push({ kind: "line", line, column });
4963
+ chars[index] = " ";
4964
+ let inner = index + 1;
4965
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
4966
+ chars[inner] = " ";
4967
+ inner++;
4968
+ }
4969
+ column += inner - index;
4970
+ index = inner - 1;
4971
+ continue;
4972
+ }
4973
+ if (ch === '"' && chars[index - 1] !== "\\") {
4974
+ inString = !inString;
4975
+ }
4976
+ if (ch === "\n") {
4977
+ line++;
4978
+ column = 1;
4979
+ } else {
4980
+ column++;
4981
+ }
4982
+ }
4983
+ if (inBlockComment) {
4984
+ throw WorldOrbitError.fromLocation("Unclosed block comment", blockCommentStart ?? void 0);
4985
+ }
4986
+ return {
4987
+ source: chars.join(""),
4988
+ comments
4989
+ };
4990
+ }
4991
+ function isHexColorLiteral(chars, start) {
4992
+ let index = start + 1;
4993
+ let length = 0;
4994
+ while (index < chars.length && /[0-9a-f]/i.test(chars[index] ?? "")) {
4995
+ index++;
4996
+ length++;
4997
+ }
4998
+ if (![3, 4, 6, 8].includes(length)) {
4999
+ return false;
5000
+ }
5001
+ const next = chars[index];
5002
+ return next === void 0 || next === " " || next === " " || next === "\r" || next === "\n";
3688
5003
  }
3689
5004
 
3690
5005
  // packages/core/dist/atlas-edit.js
3691
- function createEmptyAtlasDocument(systemId = "WorldOrbit") {
5006
+ function createEmptyAtlasDocument(systemId = "WorldOrbit", version = "2.0") {
3692
5007
  return {
3693
5008
  format: "worldorbit",
3694
- version: "2.0",
5009
+ version,
5010
+ schemaVersion: version,
3695
5011
  sourceVersion: "1.0",
3696
5012
  system: {
3697
5013
  type: "system",
3698
5014
  id: systemId,
3699
5015
  title: systemId,
5016
+ description: null,
5017
+ epoch: null,
5018
+ referencePlane: null,
3700
5019
  defaults: {
3701
5020
  view: "topdown",
3702
5021
  scale: null,
@@ -3708,6 +5027,8 @@ var WorldOrbit = (() => {
3708
5027
  viewpoints: [],
3709
5028
  annotations: []
3710
5029
  },
5030
+ groups: [],
5031
+ relations: [],
3711
5032
  objects: [],
3712
5033
  diagnostics: []
3713
5034
  };
@@ -3721,14 +5042,20 @@ var WorldOrbit = (() => {
3721
5042
  for (const key of Object.keys(document.system.atlasMetadata).sort()) {
3722
5043
  paths.push({ kind: "metadata", key });
3723
5044
  }
3724
- for (const viewpoint of [...document.system.viewpoints].sort(compareIdLike)) {
5045
+ for (const viewpoint of [...document.system.viewpoints].sort(compareIdLike2)) {
3725
5046
  paths.push({ kind: "viewpoint", id: viewpoint.id });
3726
5047
  }
3727
- for (const annotation of [...document.system.annotations].sort(compareIdLike)) {
5048
+ for (const annotation of [...document.system.annotations].sort(compareIdLike2)) {
3728
5049
  paths.push({ kind: "annotation", id: annotation.id });
3729
5050
  }
3730
5051
  }
3731
- for (const object of [...document.objects].sort(compareIdLike)) {
5052
+ for (const group of [...document.groups].sort(compareIdLike2)) {
5053
+ paths.push({ kind: "group", id: group.id });
5054
+ }
5055
+ for (const relation of [...document.relations].sort(compareIdLike2)) {
5056
+ paths.push({ kind: "relation", id: relation.id });
5057
+ }
5058
+ for (const object of [...document.objects].sort(compareIdLike2)) {
3732
5059
  paths.push({ kind: "object", id: object.id });
3733
5060
  }
3734
5061
  return paths;
@@ -3741,12 +5068,16 @@ var WorldOrbit = (() => {
3741
5068
  return document.system?.defaults ?? null;
3742
5069
  case "metadata":
3743
5070
  return path.key ? document.system?.atlasMetadata[path.key] ?? null : null;
5071
+ case "group":
5072
+ return path.id ? findGroup(document, path.id) : null;
3744
5073
  case "object":
3745
5074
  return path.id ? findObject(document, path.id) : null;
3746
5075
  case "viewpoint":
3747
5076
  return path.id ? findViewpoint(document.system, path.id) : null;
3748
5077
  case "annotation":
3749
5078
  return path.id ? findAnnotation(document.system, path.id) : null;
5079
+ case "relation":
5080
+ return path.id ? findRelation(document, path.id) : null;
3750
5081
  }
3751
5082
  }
3752
5083
  function upsertAtlasDocumentNode(document, path, value) {
@@ -3772,6 +5103,12 @@ var WorldOrbit = (() => {
3772
5103
  system.atlasMetadata[path.key] = String(value);
3773
5104
  }
3774
5105
  return next;
5106
+ case "group":
5107
+ if (!path.id) {
5108
+ throw new Error('Group updates require an "id" value.');
5109
+ }
5110
+ upsertById(next.groups, value);
5111
+ return next;
3775
5112
  case "object":
3776
5113
  if (!path.id) {
3777
5114
  throw new Error('Object updates require an "id" value.');
@@ -3790,6 +5127,12 @@ var WorldOrbit = (() => {
3790
5127
  }
3791
5128
  upsertById(system.annotations, value);
3792
5129
  return next;
5130
+ case "relation":
5131
+ if (!path.id) {
5132
+ throw new Error('Relation updates require an "id" value.');
5133
+ }
5134
+ upsertById(next.relations, value);
5135
+ return next;
3793
5136
  }
3794
5137
  }
3795
5138
  function updateAtlasDocumentNode(document, path, updater) {
@@ -3809,6 +5152,11 @@ var WorldOrbit = (() => {
3809
5152
  next.objects = next.objects.filter((object) => object.id !== path.id);
3810
5153
  }
3811
5154
  return next;
5155
+ case "group":
5156
+ if (path.id) {
5157
+ next.groups = next.groups.filter((group) => group.id !== path.id);
5158
+ }
5159
+ return next;
3812
5160
  case "viewpoint":
3813
5161
  if (path.id) {
3814
5162
  system.viewpoints = system.viewpoints.filter((viewpoint) => viewpoint.id !== path.id);
@@ -3819,6 +5167,11 @@ var WorldOrbit = (() => {
3819
5167
  system.annotations = system.annotations.filter((annotation) => annotation.id !== path.id);
3820
5168
  }
3821
5169
  return next;
5170
+ case "relation":
5171
+ if (path.id) {
5172
+ next.relations = next.relations.filter((relation) => relation.id !== path.id);
5173
+ }
5174
+ return next;
3822
5175
  default:
3823
5176
  return next;
3824
5177
  }
@@ -3836,6 +5189,15 @@ var WorldOrbit = (() => {
3836
5189
  id: diagnostic.objectId
3837
5190
  };
3838
5191
  }
5192
+ if (diagnostic.field?.startsWith("group.")) {
5193
+ const parts = diagnostic.field.split(".");
5194
+ if (parts[1] && findGroup(document, parts[1])) {
5195
+ return {
5196
+ kind: "group",
5197
+ id: parts[1]
5198
+ };
5199
+ }
5200
+ }
3839
5201
  if (diagnostic.field?.startsWith("viewpoint.")) {
3840
5202
  const parts = diagnostic.field.split(".");
3841
5203
  if (parts[1] && findViewpoint(document.system, parts[1])) {
@@ -3854,6 +5216,15 @@ var WorldOrbit = (() => {
3854
5216
  };
3855
5217
  }
3856
5218
  }
5219
+ if (diagnostic.field?.startsWith("relation.")) {
5220
+ const parts = diagnostic.field.split(".");
5221
+ if (parts[1] && findRelation(document, parts[1])) {
5222
+ return {
5223
+ kind: "relation",
5224
+ id: parts[1]
5225
+ };
5226
+ }
5227
+ }
3857
5228
  if (diagnostic.field && diagnostic.field in ensureSystem(document).atlasMetadata) {
3858
5229
  return {
3859
5230
  kind: "metadata",
@@ -3863,9 +5234,11 @@ var WorldOrbit = (() => {
3863
5234
  return null;
3864
5235
  }
3865
5236
  function validateAtlasDocumentWithDiagnostics(document) {
3866
- const materialized = materializeAtlasDocument(document);
3867
- const result = validateDocumentWithDiagnostics(materialized);
3868
- return resolveAtlasDiagnostics(document, result.diagnostics);
5237
+ const diagnostics = [
5238
+ ...document.diagnostics,
5239
+ ...collectAtlasDiagnostics(document, document.version)
5240
+ ];
5241
+ return resolveAtlasDiagnostics(document, diagnostics);
3869
5242
  }
3870
5243
  function ensureSystem(document) {
3871
5244
  if (document.system) {
@@ -3877,6 +5250,12 @@ var WorldOrbit = (() => {
3877
5250
  function findObject(document, objectId) {
3878
5251
  return document.objects.find((object) => object.id === objectId) ?? null;
3879
5252
  }
5253
+ function findGroup(document, groupId) {
5254
+ return document.groups.find((group) => group.id === groupId) ?? null;
5255
+ }
5256
+ function findRelation(document, relationId) {
5257
+ return document.relations.find((relation) => relation.id === relationId) ?? null;
5258
+ }
3880
5259
  function findViewpoint(system, viewpointId) {
3881
5260
  return system?.viewpoints.find((viewpoint) => viewpoint.id === viewpointId) ?? null;
3882
5261
  }
@@ -3887,20 +5266,21 @@ var WorldOrbit = (() => {
3887
5266
  const index = items.findIndex((item) => item.id === value.id);
3888
5267
  if (index === -1) {
3889
5268
  items.push(value);
3890
- items.sort(compareIdLike);
5269
+ items.sort(compareIdLike2);
3891
5270
  return;
3892
5271
  }
3893
5272
  items[index] = value;
3894
5273
  }
3895
- function compareIdLike(left, right) {
5274
+ function compareIdLike2(left, right) {
3896
5275
  return left.id.localeCompare(right.id);
3897
5276
  }
3898
5277
 
3899
5278
  // packages/core/dist/load.js
3900
- var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0)?$/i;
5279
+ var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1)?$/i;
5280
+ var ATLAS_SCHEMA_21_PATTERN = /^schema\s+2\.1$/i;
3901
5281
  var LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
3902
5282
  function detectWorldOrbitSchemaVersion(source) {
3903
- for (const line of source.split(/\r?\n/)) {
5283
+ for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
3904
5284
  const trimmed = line.trim();
3905
5285
  if (!trimmed) {
3906
5286
  continue;
@@ -3908,6 +5288,9 @@ var WorldOrbit = (() => {
3908
5288
  if (LEGACY_DRAFT_SCHEMA_PATTERN.test(trimmed)) {
3909
5289
  return "2.0-draft";
3910
5290
  }
5291
+ if (ATLAS_SCHEMA_21_PATTERN.test(trimmed)) {
5292
+ return "2.1";
5293
+ }
3911
5294
  if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
3912
5295
  return "2.0";
3913
5296
  }
@@ -3915,6 +5298,49 @@ var WorldOrbit = (() => {
3915
5298
  }
3916
5299
  return "1.0";
3917
5300
  }
5301
+ function stripCommentsForSchemaDetection(source) {
5302
+ const chars = [...source];
5303
+ let inString = false;
5304
+ let inBlockComment = false;
5305
+ for (let index = 0; index < chars.length; index++) {
5306
+ const ch = chars[index];
5307
+ const next = chars[index + 1];
5308
+ if (inBlockComment) {
5309
+ if (ch === "*" && next === "/") {
5310
+ chars[index] = " ";
5311
+ chars[index + 1] = " ";
5312
+ inBlockComment = false;
5313
+ index++;
5314
+ continue;
5315
+ }
5316
+ if (ch !== "\n" && ch !== "\r") {
5317
+ chars[index] = " ";
5318
+ }
5319
+ continue;
5320
+ }
5321
+ if (!inString && ch === "/" && next === "*") {
5322
+ chars[index] = " ";
5323
+ chars[index + 1] = " ";
5324
+ inBlockComment = true;
5325
+ index++;
5326
+ continue;
5327
+ }
5328
+ if (!inString && ch === "#") {
5329
+ chars[index] = " ";
5330
+ let inner = index + 1;
5331
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
5332
+ chars[inner] = " ";
5333
+ inner++;
5334
+ }
5335
+ index = inner - 1;
5336
+ continue;
5337
+ }
5338
+ if (ch === '"' && chars[index - 1] !== "\\") {
5339
+ inString = !inString;
5340
+ }
5341
+ }
5342
+ return chars.join("");
5343
+ }
3918
5344
  function loadWorldOrbitSource(source) {
3919
5345
  const result = loadWorldOrbitSourceWithDiagnostics(source);
3920
5346
  if (!result.ok || !result.value) {
@@ -3925,36 +5351,36 @@ var WorldOrbit = (() => {
3925
5351
  }
3926
5352
  function loadWorldOrbitSourceWithDiagnostics(source) {
3927
5353
  const schemaVersion = detectWorldOrbitSchemaVersion(source);
3928
- if (schemaVersion === "2.0" || schemaVersion === "2.0-draft") {
5354
+ if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1") {
3929
5355
  return loadAtlasSourceWithDiagnostics(source, schemaVersion);
3930
5356
  }
3931
5357
  let ast;
3932
5358
  try {
3933
5359
  ast = parseWorldOrbit(source);
3934
- } catch (error) {
5360
+ } catch (error2) {
3935
5361
  return {
3936
5362
  ok: false,
3937
5363
  value: null,
3938
- diagnostics: [diagnosticFromError(error, "parse")]
5364
+ diagnostics: [diagnosticFromError(error2, "parse")]
3939
5365
  };
3940
5366
  }
3941
5367
  let document;
3942
5368
  try {
3943
5369
  document = normalizeDocument(ast);
3944
- } catch (error) {
5370
+ } catch (error2) {
3945
5371
  return {
3946
5372
  ok: false,
3947
5373
  value: null,
3948
- diagnostics: [diagnosticFromError(error, "normalize")]
5374
+ diagnostics: [diagnosticFromError(error2, "normalize")]
3949
5375
  };
3950
5376
  }
3951
5377
  try {
3952
5378
  validateDocument(document);
3953
- } catch (error) {
5379
+ } catch (error2) {
3954
5380
  return {
3955
5381
  ok: false,
3956
5382
  value: null,
3957
- diagnostics: [diagnosticFromError(error, "validate")]
5383
+ diagnostics: [diagnosticFromError(error2, "validate")]
3958
5384
  };
3959
5385
  }
3960
5386
  return {
@@ -3974,30 +5400,29 @@ var WorldOrbit = (() => {
3974
5400
  let atlasDocument;
3975
5401
  try {
3976
5402
  atlasDocument = parseWorldOrbitAtlas(source);
3977
- } catch (error) {
5403
+ } catch (error2) {
3978
5404
  return {
3979
5405
  ok: false,
3980
5406
  value: null,
3981
- diagnostics: [diagnosticFromError(error, "parse", "load.atlas.failed")]
5407
+ diagnostics: [diagnosticFromError(error2, "parse", "load.atlas.failed")]
3982
5408
  };
3983
5409
  }
3984
- let document;
3985
- try {
3986
- document = materializeAtlasDocument(atlasDocument);
3987
- } catch (error) {
5410
+ const atlasDiagnostics = [...atlasDocument.diagnostics];
5411
+ if (atlasDiagnostics.some((diagnostic) => diagnostic.severity === "error")) {
3988
5412
  return {
3989
5413
  ok: false,
3990
5414
  value: null,
3991
- diagnostics: [diagnosticFromError(error, "normalize", "load.atlas.materialize.failed")]
5415
+ diagnostics: atlasDiagnostics
3992
5416
  };
3993
5417
  }
5418
+ let document;
3994
5419
  try {
3995
- validateDocument(document);
3996
- } catch (error) {
5420
+ document = materializeAtlasDocument(atlasDocument);
5421
+ } catch (error2) {
3997
5422
  return {
3998
5423
  ok: false,
3999
5424
  value: null,
4000
- diagnostics: [diagnosticFromError(error, "validate", "load.atlas.validate.failed")]
5425
+ diagnostics: [diagnosticFromError(error2, "normalize", "load.atlas.materialize.failed")]
4001
5426
  };
4002
5427
  }
4003
5428
  const loaded = {
@@ -4006,12 +5431,12 @@ var WorldOrbit = (() => {
4006
5431
  document,
4007
5432
  atlasDocument,
4008
5433
  draftDocument: atlasDocument,
4009
- diagnostics: [...atlasDocument.diagnostics]
5434
+ diagnostics: atlasDiagnostics
4010
5435
  };
4011
5436
  return {
4012
5437
  ok: true,
4013
5438
  value: loaded,
4014
- diagnostics: [...atlasDocument.diagnostics]
5439
+ diagnostics: atlasDiagnostics
4015
5440
  };
4016
5441
  }
4017
5442
 
@@ -4077,5 +5502,5 @@ var WorldOrbit = (() => {
4077
5502
  function stringify(document, options = {}) {
4078
5503
  return formatDocument(document, options);
4079
5504
  }
4080
- return __toCommonJS(dist_exports);
5505
+ return __toCommonJS(index_exports);
4081
5506
  })();