worldorbit 2.5.13 → 2.5.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +37 -11
  2. package/dist/unpkg/worldorbit-core.min.js +12 -5
  3. package/dist/unpkg/worldorbit-markdown.min.js +32 -23
  4. package/dist/unpkg/worldorbit-viewer.min.js +55 -41
  5. package/dist/unpkg/worldorbit.js +1713 -231
  6. package/dist/unpkg/worldorbit.min.js +58 -44
  7. package/package.json +2 -2
  8. package/packages/core/README.md +5 -1
  9. package/packages/core/dist/atlas-edit.d.ts +2 -2
  10. package/packages/core/dist/atlas-edit.js +70 -7
  11. package/packages/core/dist/atlas-utils.d.ts +22 -0
  12. package/packages/core/dist/atlas-utils.js +189 -0
  13. package/packages/core/dist/atlas-validate.d.ts +2 -0
  14. package/packages/core/dist/atlas-validate.js +285 -0
  15. package/packages/core/dist/draft-parse.js +786 -153
  16. package/packages/core/dist/draft.d.ts +3 -0
  17. package/packages/core/dist/draft.js +47 -3
  18. package/packages/core/dist/format.js +165 -9
  19. package/packages/core/dist/load.js +58 -13
  20. package/packages/core/dist/normalize.js +7 -0
  21. package/packages/core/dist/scene.js +66 -13
  22. package/packages/core/dist/types.d.ts +97 -3
  23. package/packages/markdown/README.md +1 -1
  24. package/packages/viewer/README.md +2 -1
  25. package/packages/viewer/dist/atlas-state.js +7 -1
  26. package/packages/viewer/dist/atlas-viewer.js +35 -1
  27. package/packages/viewer/dist/render.js +16 -7
  28. package/packages/viewer/dist/theme.js +4 -0
  29. package/packages/viewer/dist/tooltip.js +35 -0
  30. package/packages/viewer/dist/types.d.ts +7 -0
  31. package/packages/viewer/dist/viewer.js +4 -0
  32. package/packages/editor/dist/editor.d.ts +0 -2
  33. package/packages/editor/dist/editor.js +0 -2998
  34. package/packages/editor/dist/index.d.ts +0 -2
  35. package/packages/editor/dist/index.js +0 -1
  36. package/packages/editor/dist/types.d.ts +0 -53
  37. package/packages/editor/dist/types.js +0 -1
  38. package/packages/markdown/dist/html.d.ts +0 -3
  39. package/packages/markdown/dist/html.js +0 -57
  40. package/packages/markdown/dist/index.d.ts +0 -4
  41. package/packages/markdown/dist/index.js +0 -3
  42. package/packages/markdown/dist/rehype.d.ts +0 -10
  43. package/packages/markdown/dist/rehype.js +0 -49
  44. package/packages/markdown/dist/remark.d.ts +0 -9
  45. package/packages/markdown/dist/remark.js +0 -28
  46. package/packages/markdown/dist/types.d.ts +0 -11
  47. package/packages/markdown/dist/types.js +0 -1
@@ -643,7 +643,10 @@ var WorldOrbit = (() => {
643
643
  return {
644
644
  format: "worldorbit",
645
645
  version: "1.0",
646
+ schemaVersion: "1.0",
646
647
  system,
648
+ groups: [],
649
+ relations: [],
647
650
  objects
648
651
  };
649
652
  }
@@ -653,13 +656,17 @@ var WorldOrbit = (() => {
653
656
  const fieldMap = collectFields(mergedFields);
654
657
  const placement = extractPlacement(node.objectType, fieldMap);
655
658
  const properties = normalizeProperties(fieldMap);
656
- const info = normalizeInfo(node.infoEntries);
659
+ const info2 = normalizeInfo(node.infoEntries);
657
660
  if (node.objectType === "system") {
658
661
  return {
659
662
  type: "system",
660
663
  id: node.name,
664
+ title: typeof properties.title === "string" ? properties.title : null,
665
+ description: null,
666
+ epoch: null,
667
+ referencePlane: null,
661
668
  properties,
662
- info
669
+ info: info2
663
670
  };
664
671
  }
665
672
  return {
@@ -667,7 +674,7 @@ var WorldOrbit = (() => {
667
674
  id: node.name,
668
675
  properties,
669
676
  placement,
670
- info
677
+ info: info2
671
678
  };
672
679
  }
673
680
  function validateFieldCompatibility(objectType, fields) {
@@ -797,14 +804,14 @@ var WorldOrbit = (() => {
797
804
  }
798
805
  }
799
806
  function normalizeInfo(entries) {
800
- const info = {};
807
+ const info2 = {};
801
808
  for (const entry of entries) {
802
- if (entry.key in info) {
809
+ if (entry.key in info2) {
803
810
  throw WorldOrbitError.fromLocation(`Duplicate info key "${entry.key}"`, entry.location);
804
811
  }
805
- info[entry.key] = entry.value;
812
+ info2[entry.key] = entry.value;
806
813
  }
807
- return info;
814
+ return info2;
808
815
  }
809
816
  function parseAtReference(target, location) {
810
817
  if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
@@ -978,38 +985,38 @@ var WorldOrbit = (() => {
978
985
  function createDiagnostic(diagnostic) {
979
986
  return { ...diagnostic };
980
987
  }
981
- function diagnosticFromError(error, source, code = `${source}.failed`) {
982
- if (error instanceof WorldOrbitError) {
988
+ function diagnosticFromError(error2, source, code = `${source}.failed`) {
989
+ if (error2 instanceof WorldOrbitError) {
983
990
  return {
984
991
  code,
985
992
  severity: "error",
986
993
  source,
987
- message: error.message,
988
- line: error.line,
989
- column: error.column
994
+ message: error2.message,
995
+ line: error2.line,
996
+ column: error2.column
990
997
  };
991
998
  }
992
- if (error instanceof Error) {
999
+ if (error2 instanceof Error) {
993
1000
  return {
994
1001
  code,
995
1002
  severity: "error",
996
1003
  source,
997
- message: error.message
1004
+ message: error2.message
998
1005
  };
999
1006
  }
1000
1007
  return {
1001
1008
  code,
1002
1009
  severity: "error",
1003
1010
  source,
1004
- message: String(error)
1011
+ message: String(error2)
1005
1012
  };
1006
1013
  }
1007
1014
  function parseWithDiagnostics(source) {
1008
1015
  let ast;
1009
1016
  try {
1010
1017
  ast = parseWorldOrbit(source);
1011
- } catch (error) {
1012
- const diagnostic = diagnosticFromError(error, "parse");
1018
+ } catch (error2) {
1019
+ const diagnostic = diagnosticFromError(error2, "parse");
1013
1020
  return {
1014
1021
  ok: false,
1015
1022
  value: null,
@@ -1019,20 +1026,20 @@ var WorldOrbit = (() => {
1019
1026
  let document2;
1020
1027
  try {
1021
1028
  document2 = normalizeDocument(ast);
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
  try {
1030
1037
  validateDocument(document2);
1031
- } catch (error) {
1038
+ } catch (error2) {
1032
1039
  return {
1033
1040
  ok: false,
1034
1041
  value: null,
1035
- diagnostics: [diagnosticFromError(error, "validate")]
1042
+ diagnostics: [diagnosticFromError(error2, "validate")]
1036
1043
  };
1037
1044
  }
1038
1045
  return {
@@ -1051,11 +1058,11 @@ var WorldOrbit = (() => {
1051
1058
  value: normalizeDocument(ast),
1052
1059
  diagnostics: []
1053
1060
  };
1054
- } catch (error) {
1061
+ } catch (error2) {
1055
1062
  return {
1056
1063
  ok: false,
1057
1064
  value: null,
1058
- diagnostics: [diagnosticFromError(error, "normalize")]
1065
+ diagnostics: [diagnosticFromError(error2, "normalize")]
1059
1066
  };
1060
1067
  }
1061
1068
  }
@@ -1067,11 +1074,11 @@ var WorldOrbit = (() => {
1067
1074
  value: document2,
1068
1075
  diagnostics: []
1069
1076
  };
1070
- } catch (error) {
1077
+ } catch (error2) {
1071
1078
  return {
1072
1079
  ok: false,
1073
1080
  value: null,
1074
- diagnostics: [diagnosticFromError(error, "validate")]
1081
+ diagnostics: [diagnosticFromError(error2, "validate")]
1075
1082
  };
1076
1083
  }
1077
1084
  }
@@ -1203,8 +1210,10 @@ var WorldOrbit = (() => {
1203
1210
  const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
1204
1211
  const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
1205
1212
  const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
1206
- const layers = createSceneLayers(orbitVisuals, leaders, objects, labels);
1213
+ const relations = createSceneRelations(document2, objects);
1214
+ const layers = createSceneLayers(orbitVisuals, relations, leaders, objects, labels);
1207
1215
  const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
1216
+ const semanticGroups = createSceneSemanticGroups(document2, objects);
1208
1217
  const viewpoints = createSceneViewpoints(document2, projection, frame.preset, relationships, objectMap);
1209
1218
  const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
1210
1219
  return {
@@ -1214,7 +1223,7 @@ var WorldOrbit = (() => {
1214
1223
  renderPreset: frame.preset,
1215
1224
  projection,
1216
1225
  scaleModel,
1217
- title: String(document2.system?.properties.title ?? document2.system?.id ?? "WorldOrbit") || "WorldOrbit",
1226
+ title: String(document2.system?.title ?? document2.system?.properties.title ?? document2.system?.id ?? "WorldOrbit") || "WorldOrbit",
1218
1227
  subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
1219
1228
  systemId,
1220
1229
  viewMode: projection,
@@ -1230,9 +1239,11 @@ var WorldOrbit = (() => {
1230
1239
  contentBounds,
1231
1240
  layers,
1232
1241
  groups,
1242
+ semanticGroups,
1233
1243
  viewpoints,
1234
1244
  objects,
1235
1245
  orbitVisuals,
1246
+ relations,
1236
1247
  leaders,
1237
1248
  labels
1238
1249
  };
@@ -1342,6 +1353,7 @@ var WorldOrbit = (() => {
1342
1353
  }
1343
1354
  function createSceneObject(position, scaleModel, relationships) {
1344
1355
  const { object, x, y, radius, sortKey, anchorX, anchorY } = position;
1356
+ const renderPriority = object.renderHints?.renderPriority ?? 0;
1345
1357
  return {
1346
1358
  renderId: createRenderId(object.id),
1347
1359
  objectId: object.id,
@@ -1350,11 +1362,12 @@ var WorldOrbit = (() => {
1350
1362
  ancestorIds: relationships.ancestorIds.get(object.id) ?? [],
1351
1363
  childIds: relationships.childIds.get(object.id) ?? [],
1352
1364
  groupId: relationships.groupIds.get(object.id) ?? null,
1365
+ semanticGroupIds: [...object.groups ?? []],
1353
1366
  x,
1354
1367
  y,
1355
1368
  radius,
1356
1369
  visualRadius: visualExtentForObject(object, radius, scaleModel),
1357
- sortKey,
1370
+ sortKey: sortKey + renderPriority * 1e-3,
1358
1371
  anchorX,
1359
1372
  anchorY,
1360
1373
  label: object.id,
@@ -1371,6 +1384,7 @@ var WorldOrbit = (() => {
1371
1384
  object: draft.object,
1372
1385
  parentId: draft.parentId,
1373
1386
  groupId,
1387
+ semanticGroupIds: [...draft.object.groups ?? []],
1374
1388
  kind: draft.kind,
1375
1389
  cx: draft.cx,
1376
1390
  cy: draft.cy,
@@ -1382,7 +1396,7 @@ var WorldOrbit = (() => {
1382
1396
  bandThickness: draft.bandThickness,
1383
1397
  frontArcPath: draft.frontArcPath,
1384
1398
  backArcPath: draft.backArcPath,
1385
- hidden: draft.object.properties.hidden === true
1399
+ hidden: draft.object.properties.hidden === true || draft.object.renderHints?.renderOrbit === false
1386
1400
  };
1387
1401
  }
1388
1402
  function createLeaderLine(draft) {
@@ -1391,6 +1405,7 @@ var WorldOrbit = (() => {
1391
1405
  objectId: draft.object.id,
1392
1406
  object: draft.object,
1393
1407
  groupId: draft.groupId,
1408
+ semanticGroupIds: [...draft.object.groups ?? []],
1394
1409
  x1: draft.x1,
1395
1410
  y1: draft.y1,
1396
1411
  x2: draft.x2,
@@ -1402,7 +1417,7 @@ var WorldOrbit = (() => {
1402
1417
  function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1403
1418
  const labels = [];
1404
1419
  const occupied = [];
1405
- const visibleObjects = [...objects].filter((object) => !object.hidden).sort((left, right) => left.sortKey - right.sortKey);
1420
+ const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort((left, right) => left.sortKey - right.sortKey);
1406
1421
  for (const object of visibleObjects) {
1407
1422
  const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1408
1423
  const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
@@ -1422,6 +1437,7 @@ var WorldOrbit = (() => {
1422
1437
  objectId: object.objectId,
1423
1438
  object: object.object,
1424
1439
  groupId: object.groupId,
1440
+ semanticGroupIds: [...object.semanticGroupIds],
1425
1441
  label: object.label,
1426
1442
  secondaryLabel: object.secondaryLabel,
1427
1443
  x: object.x,
@@ -1434,7 +1450,7 @@ var WorldOrbit = (() => {
1434
1450
  }
1435
1451
  return labels;
1436
1452
  }
1437
- function createSceneLayers(orbitVisuals, leaders, objects, labels) {
1453
+ function createSceneLayers(orbitVisuals, relations, leaders, objects, labels) {
1438
1454
  const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1439
1455
  const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1440
1456
  return [
@@ -1445,6 +1461,10 @@ var WorldOrbit = (() => {
1445
1461
  },
1446
1462
  { id: "orbits-back", renderIds: backOrbitIds },
1447
1463
  { id: "orbits-front", renderIds: frontOrbitIds },
1464
+ {
1465
+ id: "relations",
1466
+ renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId)
1467
+ },
1448
1468
  {
1449
1469
  id: "objects",
1450
1470
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
@@ -1509,6 +1529,36 @@ var WorldOrbit = (() => {
1509
1529
  }
1510
1530
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1511
1531
  }
1532
+ function createSceneSemanticGroups(document2, objects) {
1533
+ return [...document2.groups].map((group) => ({
1534
+ id: group.id,
1535
+ label: group.label,
1536
+ summary: group.summary,
1537
+ color: group.color,
1538
+ tags: [...group.tags],
1539
+ hidden: group.hidden,
1540
+ objectIds: objects.filter((object) => !object.hidden && object.semanticGroupIds.includes(group.id)).map((object) => object.objectId)
1541
+ })).sort((left, right) => left.label.localeCompare(right.label));
1542
+ }
1543
+ function createSceneRelations(document2, objects) {
1544
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1545
+ return document2.relations.map((relation) => {
1546
+ const from = objectMap.get(relation.from);
1547
+ const to = objectMap.get(relation.to);
1548
+ return {
1549
+ renderId: `${createRenderId(relation.id)}-relation`,
1550
+ relationId: relation.id,
1551
+ relation,
1552
+ fromObjectId: relation.from,
1553
+ toObjectId: relation.to,
1554
+ x1: from?.x ?? 0,
1555
+ y1: from?.y ?? 0,
1556
+ x2: to?.x ?? 0,
1557
+ y2: to?.y ?? 0,
1558
+ hidden: relation.hidden || !from || !to || from.hidden || to.hidden
1559
+ };
1560
+ }).sort((left, right) => left.relation.id.localeCompare(right.relation.id));
1561
+ }
1512
1562
  function createSceneViewpoints(document2, projection, preset, relationships, objectMap) {
1513
1563
  const generatedOverview = createGeneratedOverviewViewpoint(document2, projection, preset);
1514
1564
  const drafts = /* @__PURE__ */ new Map();
@@ -1526,7 +1576,7 @@ var WorldOrbit = (() => {
1526
1576
  }
1527
1577
  const field = fieldParts.join(".").toLowerCase();
1528
1578
  const draft = drafts.get(id) ?? { id };
1529
- applyViewpointField(draft, field, value, projection, preset, relationships, objectMap);
1579
+ applyViewpointField(draft, field, value, document2, projection, preset, relationships, objectMap);
1530
1580
  drafts.set(id, draft);
1531
1581
  }
1532
1582
  const viewpoints = [...drafts.values()].map((draft) => finalizeViewpointDraft(draft, projection, preset, objectMap)).filter(Boolean);
@@ -1554,7 +1604,8 @@ var WorldOrbit = (() => {
1554
1604
  });
1555
1605
  }
1556
1606
  function createGeneratedOverviewViewpoint(document2, projection, preset) {
1557
- const label = document2.system?.properties.title ? `${String(document2.system.properties.title)} Overview` : "Overview";
1607
+ const title = document2.system?.title ?? document2.system?.properties.title;
1608
+ const label = title ? `${String(title)} Overview` : "Overview";
1558
1609
  return {
1559
1610
  id: "overview",
1560
1611
  label,
@@ -1570,7 +1621,7 @@ var WorldOrbit = (() => {
1570
1621
  generated: true
1571
1622
  };
1572
1623
  }
1573
- function applyViewpointField(draft, field, value, projection, preset, relationships, objectMap) {
1624
+ function applyViewpointField(draft, field, value, document2, projection, preset, relationships, objectMap) {
1574
1625
  const normalizedValue = value.trim();
1575
1626
  switch (field) {
1576
1627
  case "label":
@@ -1637,7 +1688,7 @@ var WorldOrbit = (() => {
1637
1688
  case "groups":
1638
1689
  draft.filter = {
1639
1690
  ...draft.filter ?? createEmptyViewpointFilter(),
1640
- groupIds: parseViewpointGroups(normalizedValue, relationships, objectMap)
1691
+ groupIds: parseViewpointGroups(normalizedValue, document2, relationships, objectMap)
1641
1692
  };
1642
1693
  return;
1643
1694
  }
@@ -1710,7 +1761,7 @@ var WorldOrbit = (() => {
1710
1761
  next["orbits-front"] = enabled;
1711
1762
  continue;
1712
1763
  }
1713
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1764
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1714
1765
  next[rawLayer] = enabled;
1715
1766
  }
1716
1767
  }
@@ -1719,8 +1770,11 @@ var WorldOrbit = (() => {
1719
1770
  function parseViewpointObjectTypes(value) {
1720
1771
  return splitListValue(value).filter((entry) => entry === "star" || entry === "planet" || entry === "moon" || entry === "belt" || entry === "asteroid" || entry === "comet" || entry === "ring" || entry === "structure" || entry === "phenomenon");
1721
1772
  }
1722
- function parseViewpointGroups(value, relationships, objectMap) {
1773
+ function parseViewpointGroups(value, document2, relationships, objectMap) {
1723
1774
  return splitListValue(value).map((entry) => {
1775
+ if (document2.schemaVersion === "2.1" || document2.groups.some((group) => group.id === entry)) {
1776
+ return entry;
1777
+ }
1724
1778
  if (entry.startsWith("wo-") && entry.endsWith("-group")) {
1725
1779
  return entry;
1726
1780
  }
@@ -2504,8 +2558,11 @@ var WorldOrbit = (() => {
2504
2558
  return {
2505
2559
  format: "worldorbit",
2506
2560
  version: "2.0",
2561
+ schemaVersion: "2.0",
2507
2562
  sourceVersion: document2.version,
2508
2563
  system,
2564
+ groups: structuredClone(document2.groups ?? []),
2565
+ relations: structuredClone(document2.relations ?? []),
2509
2566
  objects: document2.objects.map(cloneWorldOrbitObject),
2510
2567
  diagnostics
2511
2568
  };
@@ -2517,13 +2574,20 @@ var WorldOrbit = (() => {
2517
2574
  const system = document2.system ? {
2518
2575
  type: "system",
2519
2576
  id: document2.system.id,
2577
+ title: document2.system.title,
2578
+ description: document2.system.description,
2579
+ epoch: document2.system.epoch,
2580
+ referencePlane: document2.system.referencePlane,
2520
2581
  properties: materializeDraftSystemProperties(document2.system),
2521
2582
  info: materializeDraftSystemInfo(document2.system)
2522
2583
  } : null;
2523
2584
  return {
2524
2585
  format: "worldorbit",
2525
2586
  version: "1.0",
2587
+ schemaVersion: document2.version,
2526
2588
  system,
2589
+ groups: structuredClone(document2.groups ?? []),
2590
+ relations: structuredClone(document2.relations ?? []),
2527
2591
  objects: document2.objects.map(cloneWorldOrbitObject)
2528
2592
  };
2529
2593
  }
@@ -2538,7 +2602,10 @@ var WorldOrbit = (() => {
2538
2602
  return {
2539
2603
  type: "system",
2540
2604
  id: document2.system?.id ?? "WorldOrbit",
2541
- title: typeof document2.system?.properties.title === "string" ? document2.system.properties.title : null,
2605
+ title: document2.system?.title ?? (typeof document2.system?.properties.title === "string" ? document2.system.properties.title : null),
2606
+ description: document2.system?.description ?? null,
2607
+ epoch: document2.system?.epoch ?? null,
2608
+ referencePlane: document2.system?.referencePlane ?? null,
2542
2609
  defaults,
2543
2610
  atlasMetadata,
2544
2611
  viewpoints: scene.viewpoints.map(mapSceneViewpointToDraftViewpoint),
@@ -2665,6 +2732,17 @@ var WorldOrbit = (() => {
2665
2732
  function cloneWorldOrbitObject(object) {
2666
2733
  return {
2667
2734
  ...object,
2735
+ groups: object.groups ? [...object.groups] : void 0,
2736
+ resonance: object.resonance ? { ...object.resonance } : object.resonance,
2737
+ renderHints: object.renderHints ? { ...object.renderHints } : object.renderHints,
2738
+ deriveRules: object.deriveRules ? object.deriveRules.map((rule) => ({ ...rule })) : void 0,
2739
+ validationRules: object.validationRules ? object.validationRules.map((rule) => ({ ...rule })) : void 0,
2740
+ lockedFields: object.lockedFields ? [...object.lockedFields] : void 0,
2741
+ tolerances: object.tolerances ? object.tolerances.map((entry) => ({
2742
+ field: entry.field,
2743
+ 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
2744
+ })) : void 0,
2745
+ typedBlocks: object.typedBlocks ? Object.fromEntries(Object.entries(object.typedBlocks).map(([key, block]) => [key, { ...block ?? {} }])) : void 0,
2668
2746
  properties: cloneProperties(object.properties),
2669
2747
  placement: object.placement ? structuredClone(object.placement) : null,
2670
2748
  info: { ...object.info }
@@ -2709,71 +2787,80 @@ var WorldOrbit = (() => {
2709
2787
  if (system.defaults.units) {
2710
2788
  properties.units = system.defaults.units;
2711
2789
  }
2790
+ if (system.description) {
2791
+ properties.description = system.description;
2792
+ }
2793
+ if (system.epoch) {
2794
+ properties.epoch = system.epoch;
2795
+ }
2796
+ if (system.referencePlane) {
2797
+ properties.referencePlane = system.referencePlane;
2798
+ }
2712
2799
  return properties;
2713
2800
  }
2714
2801
  function materializeDraftSystemInfo(system) {
2715
- const info = {
2802
+ const info2 = {
2716
2803
  ...system.atlasMetadata
2717
2804
  };
2718
2805
  if (system.defaults.theme) {
2719
- info["atlas.theme"] = system.defaults.theme;
2806
+ info2["atlas.theme"] = system.defaults.theme;
2720
2807
  }
2721
2808
  for (const viewpoint of system.viewpoints) {
2722
2809
  const prefix = `viewpoint.${viewpoint.id}`;
2723
- info[`${prefix}.label`] = viewpoint.label;
2810
+ info2[`${prefix}.label`] = viewpoint.label;
2724
2811
  if (viewpoint.summary) {
2725
- info[`${prefix}.summary`] = viewpoint.summary;
2812
+ info2[`${prefix}.summary`] = viewpoint.summary;
2726
2813
  }
2727
2814
  if (viewpoint.focusObjectId) {
2728
- info[`${prefix}.focus`] = viewpoint.focusObjectId;
2815
+ info2[`${prefix}.focus`] = viewpoint.focusObjectId;
2729
2816
  }
2730
2817
  if (viewpoint.selectedObjectId) {
2731
- info[`${prefix}.select`] = viewpoint.selectedObjectId;
2818
+ info2[`${prefix}.select`] = viewpoint.selectedObjectId;
2732
2819
  }
2733
2820
  if (viewpoint.projection) {
2734
- info[`${prefix}.projection`] = viewpoint.projection;
2821
+ info2[`${prefix}.projection`] = viewpoint.projection;
2735
2822
  }
2736
2823
  if (viewpoint.preset) {
2737
- info[`${prefix}.preset`] = viewpoint.preset;
2824
+ info2[`${prefix}.preset`] = viewpoint.preset;
2738
2825
  }
2739
2826
  if (viewpoint.zoom !== null) {
2740
- info[`${prefix}.zoom`] = String(viewpoint.zoom);
2827
+ info2[`${prefix}.zoom`] = String(viewpoint.zoom);
2741
2828
  }
2742
2829
  if (viewpoint.rotationDeg !== 0) {
2743
- info[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2830
+ info2[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2744
2831
  }
2745
2832
  const serializedLayers = serializeViewpointLayers(viewpoint.layers);
2746
2833
  if (serializedLayers) {
2747
- info[`${prefix}.layers`] = serializedLayers;
2834
+ info2[`${prefix}.layers`] = serializedLayers;
2748
2835
  }
2749
2836
  if (viewpoint.filter?.query) {
2750
- info[`${prefix}.query`] = viewpoint.filter.query;
2837
+ info2[`${prefix}.query`] = viewpoint.filter.query;
2751
2838
  }
2752
2839
  if ((viewpoint.filter?.objectTypes.length ?? 0) > 0) {
2753
- info[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
2840
+ info2[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
2754
2841
  }
2755
2842
  if ((viewpoint.filter?.tags.length ?? 0) > 0) {
2756
- info[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
2843
+ info2[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
2757
2844
  }
2758
2845
  if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2759
- info[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2846
+ info2[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2760
2847
  }
2761
2848
  }
2762
2849
  for (const annotation of system.annotations) {
2763
2850
  const prefix = `annotation.${annotation.id}`;
2764
- info[`${prefix}.label`] = annotation.label;
2851
+ info2[`${prefix}.label`] = annotation.label;
2765
2852
  if (annotation.targetObjectId) {
2766
- info[`${prefix}.target`] = annotation.targetObjectId;
2853
+ info2[`${prefix}.target`] = annotation.targetObjectId;
2767
2854
  }
2768
- info[`${prefix}.body`] = annotation.body;
2855
+ info2[`${prefix}.body`] = annotation.body;
2769
2856
  if (annotation.tags.length > 0) {
2770
- info[`${prefix}.tags`] = annotation.tags.join(" ");
2857
+ info2[`${prefix}.tags`] = annotation.tags.join(" ");
2771
2858
  }
2772
2859
  if (annotation.sourceObjectId) {
2773
- info[`${prefix}.source`] = annotation.sourceObjectId;
2860
+ info2[`${prefix}.source`] = annotation.sourceObjectId;
2774
2861
  }
2775
2862
  }
2776
- return info;
2863
+ return info2;
2777
2864
  }
2778
2865
  function serializeViewpointLayers(layers) {
2779
2866
  const tokens = [];
@@ -2782,7 +2869,7 @@ var WorldOrbit = (() => {
2782
2869
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2783
2870
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2784
2871
  }
2785
- for (const key of ["background", "guides", "objects", "labels", "metadata"]) {
2872
+ for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
2786
2873
  if (layers[key] !== void 0) {
2787
2874
  tokens.push(layers[key] ? key : `-${key}`);
2788
2875
  }
@@ -2792,7 +2879,8 @@ var WorldOrbit = (() => {
2792
2879
  function convertAtlasDocumentToLegacyDraft(document2) {
2793
2880
  return {
2794
2881
  ...document2,
2795
- version: "2.0-draft"
2882
+ version: "2.0-draft",
2883
+ schemaVersion: "2.0-draft"
2796
2884
  };
2797
2885
  }
2798
2886
 
@@ -2834,19 +2922,28 @@ var WorldOrbit = (() => {
2834
2922
  ];
2835
2923
  function formatDocument(document2, options = {}) {
2836
2924
  const schema = options.schema ?? "auto";
2837
- const useDraft = schema === "2.0" || schema === "2.0-draft" || document2.version === "2.0" || document2.version === "2.0-draft";
2925
+ const useDraft = schema === "2.0" || schema === "2.1" || schema === "2.0-draft" || document2.version === "2.0" || document2.version === "2.1" || document2.version === "2.0-draft";
2838
2926
  if (useDraft) {
2839
2927
  if (schema === "2.0-draft") {
2840
- const legacyDraftDocument = document2.version === "2.0-draft" ? document2 : document2.version === "2.0" ? {
2928
+ const legacyDraftDocument = document2.version === "2.0-draft" ? document2 : document2.version === "2.0" || document2.version === "2.1" ? {
2841
2929
  ...document2,
2842
- version: "2.0-draft"
2930
+ version: "2.0-draft",
2931
+ schemaVersion: "2.0-draft"
2843
2932
  } : upgradeDocumentToDraftV2(document2);
2844
2933
  return formatDraftDocument(legacyDraftDocument);
2845
2934
  }
2846
- const atlasDocument = document2.version === "2.0" ? document2 : document2.version === "2.0-draft" ? {
2935
+ const atlasDocument = document2.version === "2.0" || document2.version === "2.1" ? document2 : document2.version === "2.0-draft" ? {
2847
2936
  ...document2,
2848
- version: "2.0"
2937
+ version: "2.0",
2938
+ schemaVersion: "2.0"
2849
2939
  } : upgradeDocumentToV2(document2);
2940
+ if (schema === "2.1" && atlasDocument.version !== "2.1") {
2941
+ return formatAtlasDocument({
2942
+ ...atlasDocument,
2943
+ version: "2.1",
2944
+ schemaVersion: "2.1"
2945
+ });
2946
+ }
2850
2947
  return formatAtlasDocument(atlasDocument);
2851
2948
  }
2852
2949
  const lines = [];
@@ -2864,10 +2961,18 @@ var WorldOrbit = (() => {
2864
2961
  return lines.join("\n");
2865
2962
  }
2866
2963
  function formatAtlasDocument(document2) {
2867
- const lines = ["schema 2.0", ""];
2964
+ const lines = [`schema ${document2.version}`, ""];
2868
2965
  if (document2.system) {
2869
2966
  lines.push(...formatAtlasSystem(document2.system));
2870
2967
  }
2968
+ for (const group of [...document2.groups].sort(compareIdLike)) {
2969
+ lines.push("");
2970
+ lines.push(...formatAtlasGroup(group));
2971
+ }
2972
+ for (const relation of [...document2.relations].sort(compareIdLike)) {
2973
+ lines.push("");
2974
+ lines.push(...formatAtlasRelation(relation));
2975
+ }
2871
2976
  const sortedObjects = [...document2.objects].sort(compareObjects);
2872
2977
  if (sortedObjects.length > 0 && lines.at(-1) !== "") {
2873
2978
  lines.push("");
@@ -2883,12 +2988,21 @@ var WorldOrbit = (() => {
2883
2988
  function formatDraftDocument(document2) {
2884
2989
  const legacy = document2.version === "2.0-draft" ? document2 : {
2885
2990
  ...document2,
2886
- version: "2.0-draft"
2991
+ version: "2.0-draft",
2992
+ schemaVersion: "2.0-draft"
2887
2993
  };
2888
2994
  const lines = ["schema 2.0-draft", ""];
2889
2995
  if (legacy.system) {
2890
2996
  lines.push(...formatAtlasSystem(legacy.system));
2891
2997
  }
2998
+ for (const group of [...legacy.groups].sort(compareIdLike)) {
2999
+ lines.push("");
3000
+ lines.push(...formatAtlasGroup(group));
3001
+ }
3002
+ for (const relation of [...legacy.relations].sort(compareIdLike)) {
3003
+ lines.push("");
3004
+ lines.push(...formatAtlasRelation(relation));
3005
+ }
2892
3006
  const sortedObjects = [...legacy.objects].sort(compareObjects);
2893
3007
  if (sortedObjects.length > 0 && lines.at(-1) !== "") {
2894
3008
  lines.push("");
@@ -2904,11 +3018,38 @@ var WorldOrbit = (() => {
2904
3018
  function formatSystem(system) {
2905
3019
  return formatLines("system", system.id, system.properties, null, system.info);
2906
3020
  }
3021
+ function formatLines(objectType, id, properties, placement, info2) {
3022
+ const lines = [`${objectType} ${id}`];
3023
+ const fieldLines = [...formatPlacement(placement), ...formatProperties(properties)];
3024
+ for (const fieldLine of fieldLines) {
3025
+ lines.push(` ${fieldLine}`);
3026
+ }
3027
+ const infoEntries = Object.entries(info2).sort(([left], [right]) => left.localeCompare(right));
3028
+ if (infoEntries.length > 0) {
3029
+ if (fieldLines.length > 0) {
3030
+ lines.push("");
3031
+ }
3032
+ lines.push(" info");
3033
+ for (const [key, value] of infoEntries) {
3034
+ lines.push(` ${key} ${quoteIfNeeded(value)}`);
3035
+ }
3036
+ }
3037
+ return lines;
3038
+ }
2907
3039
  function formatAtlasSystem(system) {
2908
3040
  const lines = [`system ${system.id}`];
2909
3041
  if (system.title) {
2910
3042
  lines.push(` title ${quoteIfNeeded(system.title)}`);
2911
3043
  }
3044
+ if (system.description) {
3045
+ lines.push(` description ${quoteIfNeeded(system.description)}`);
3046
+ }
3047
+ if (system.epoch) {
3048
+ lines.push(` epoch ${quoteIfNeeded(system.epoch)}`);
3049
+ }
3050
+ if (system.referencePlane) {
3051
+ lines.push(` referencePlane ${quoteIfNeeded(system.referencePlane)}`);
3052
+ }
2912
3053
  lines.push("");
2913
3054
  lines.push("defaults");
2914
3055
  lines.push(` view ${system.defaults.view}`);
@@ -2943,18 +3084,22 @@ var WorldOrbit = (() => {
2943
3084
  return lines;
2944
3085
  }
2945
3086
  function formatObject(object) {
2946
- return formatLines(object.type, object.id, object.properties, object.placement, object.info);
3087
+ return formatWorldOrbitObject(object.type, object.id, object);
2947
3088
  }
2948
3089
  function formatAtlasObject(object) {
2949
- return formatLines(`object ${object.type}`, object.id, object.properties, object.placement, object.info);
3090
+ return formatWorldOrbitObject(`object ${object.type}`, object.id, object);
2950
3091
  }
2951
- function formatLines(objectType, id, properties, placement, info) {
3092
+ function formatWorldOrbitObject(objectType, id, object) {
2952
3093
  const lines = [`${objectType} ${id}`];
2953
- const fieldLines = [...formatPlacement(placement), ...formatProperties(properties)];
3094
+ const fieldLines = [
3095
+ ...formatPlacement(object.placement),
3096
+ ...formatProperties(object.properties),
3097
+ ...formatObjectMetadata(object)
3098
+ ];
2954
3099
  for (const fieldLine of fieldLines) {
2955
3100
  lines.push(` ${fieldLine}`);
2956
3101
  }
2957
- const infoEntries = Object.entries(info).sort(([left], [right]) => left.localeCompare(right));
3102
+ const infoEntries = Object.entries(object.info).sort(([left], [right]) => left.localeCompare(right));
2958
3103
  if (infoEntries.length > 0) {
2959
3104
  if (fieldLines.length > 0) {
2960
3105
  lines.push("");
@@ -2964,6 +3109,16 @@ var WorldOrbit = (() => {
2964
3109
  lines.push(` ${key} ${quoteIfNeeded(value)}`);
2965
3110
  }
2966
3111
  }
3112
+ for (const blockName of ["climate", "habitability", "settlement"]) {
3113
+ const blockEntries = Object.entries(object.typedBlocks?.[blockName] ?? {}).sort(([left], [right]) => left.localeCompare(right));
3114
+ if (blockEntries.length > 0) {
3115
+ lines.push("");
3116
+ lines.push(` ${blockName}`);
3117
+ for (const [key, value] of blockEntries) {
3118
+ lines.push(` ${key} ${quoteIfNeeded(value)}`);
3119
+ }
3120
+ }
3121
+ }
2967
3122
  return lines;
2968
3123
  }
2969
3124
  function formatPlacement(placement) {
@@ -2992,6 +3147,46 @@ var WorldOrbit = (() => {
2992
3147
  function formatProperties(properties) {
2993
3148
  return Object.keys(properties).sort(compareFieldKeys).map((key) => `${key} ${formatValue(properties[key])}`);
2994
3149
  }
3150
+ function formatObjectMetadata(object) {
3151
+ const lines = [];
3152
+ if (object.groups?.length) {
3153
+ lines.push(`groups ${object.groups.join(" ")}`);
3154
+ }
3155
+ if (object.epoch) {
3156
+ lines.push(`epoch ${quoteIfNeeded(object.epoch)}`);
3157
+ }
3158
+ if (object.referencePlane) {
3159
+ lines.push(`referencePlane ${quoteIfNeeded(object.referencePlane)}`);
3160
+ }
3161
+ if (object.tidalLock !== void 0) {
3162
+ lines.push(`tidalLock ${object.tidalLock ? "true" : "false"}`);
3163
+ }
3164
+ if (object.renderHints?.renderLabel !== void 0) {
3165
+ lines.push(`renderLabel ${object.renderHints.renderLabel ? "true" : "false"}`);
3166
+ }
3167
+ if (object.renderHints?.renderOrbit !== void 0) {
3168
+ lines.push(`renderOrbit ${object.renderHints.renderOrbit ? "true" : "false"}`);
3169
+ }
3170
+ if (object.renderHints?.renderPriority !== void 0) {
3171
+ lines.push(`renderPriority ${object.renderHints.renderPriority}`);
3172
+ }
3173
+ if (object.resonance) {
3174
+ lines.push(`resonance ${object.resonance.targetObjectId} ${object.resonance.ratio}`);
3175
+ }
3176
+ for (const rule of object.deriveRules ?? []) {
3177
+ lines.push(`derive ${rule.field} ${rule.strategy}`);
3178
+ }
3179
+ for (const rule of object.validationRules ?? []) {
3180
+ lines.push(`validate ${rule.rule}`);
3181
+ }
3182
+ if (object.lockedFields?.length) {
3183
+ lines.push(`locked ${object.lockedFields.join(" ")}`);
3184
+ }
3185
+ for (const tolerance of object.tolerances ?? []) {
3186
+ lines.push(`tolerance ${tolerance.field} ${formatValue(tolerance.value)}`);
3187
+ }
3188
+ return lines;
3189
+ }
2995
3190
  function formatAtlasViewpoint(viewpoint) {
2996
3191
  const lines = [`viewpoint ${viewpoint.id}`, ` label ${quoteIfNeeded(viewpoint.label)}`];
2997
3192
  if (viewpoint.focusObjectId) {
@@ -3047,6 +3242,50 @@ var WorldOrbit = (() => {
3047
3242
  }
3048
3243
  return lines;
3049
3244
  }
3245
+ function formatAtlasGroup(group) {
3246
+ const lines = [`group ${group.id}`, ` label ${quoteIfNeeded(group.label)}`];
3247
+ if (group.summary) {
3248
+ lines.push(` summary ${quoteIfNeeded(group.summary)}`);
3249
+ }
3250
+ if (group.color) {
3251
+ lines.push(` color ${quoteIfNeeded(group.color)}`);
3252
+ }
3253
+ if (group.tags.length > 0) {
3254
+ lines.push(` tags ${group.tags.map(quoteIfNeeded).join(" ")}`);
3255
+ }
3256
+ if (group.hidden) {
3257
+ lines.push(" hidden true");
3258
+ }
3259
+ return lines;
3260
+ }
3261
+ function formatAtlasRelation(relation) {
3262
+ const lines = [`relation ${relation.id}`];
3263
+ if (relation.from) {
3264
+ lines.push(` from ${quoteIfNeeded(relation.from)}`);
3265
+ }
3266
+ if (relation.to) {
3267
+ lines.push(` to ${quoteIfNeeded(relation.to)}`);
3268
+ }
3269
+ if (relation.kind) {
3270
+ lines.push(` kind ${quoteIfNeeded(relation.kind)}`);
3271
+ }
3272
+ if (relation.label) {
3273
+ lines.push(` label ${quoteIfNeeded(relation.label)}`);
3274
+ }
3275
+ if (relation.summary) {
3276
+ lines.push(` summary ${quoteIfNeeded(relation.summary)}`);
3277
+ }
3278
+ if (relation.tags.length > 0) {
3279
+ lines.push(` tags ${relation.tags.map(quoteIfNeeded).join(" ")}`);
3280
+ }
3281
+ if (relation.color) {
3282
+ lines.push(` color ${quoteIfNeeded(relation.color)}`);
3283
+ }
3284
+ if (relation.hidden) {
3285
+ lines.push(" hidden true");
3286
+ }
3287
+ return lines;
3288
+ }
3050
3289
  function formatValue(value) {
3051
3290
  if (Array.isArray(value)) {
3052
3291
  return value.map((item) => quoteIfNeeded(item)).join(" ");
@@ -3088,7 +3327,7 @@ var WorldOrbit = (() => {
3088
3327
  if (orbitFront !== void 0 || orbitBack !== void 0) {
3089
3328
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
3090
3329
  }
3091
- for (const key of ["background", "guides", "objects", "labels", "metadata"]) {
3330
+ for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
3092
3331
  if (layers[key] !== void 0) {
3093
3332
  tokens.push(layers[key] ? key : `-${key}`);
3094
3333
  }
@@ -3113,6 +3352,9 @@ var WorldOrbit = (() => {
3113
3352
  return leftIndex - rightIndex;
3114
3353
  return left.id.localeCompare(right.id);
3115
3354
  }
3355
+ function compareIdLike(left, right) {
3356
+ return left.id.localeCompare(right.id);
3357
+ }
3116
3358
  function objectTypeIndex(objectType) {
3117
3359
  switch (objectType) {
3118
3360
  case "star":
@@ -3142,24 +3384,533 @@ var WorldOrbit = (() => {
3142
3384
  return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
3143
3385
  }
3144
3386
 
3387
+ // packages/core/dist/atlas-utils.js
3388
+ 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)?$/;
3389
+ var BOOLEAN_VALUES2 = /* @__PURE__ */ new Map([
3390
+ ["true", true],
3391
+ ["false", false],
3392
+ ["yes", true],
3393
+ ["no", false]
3394
+ ]);
3395
+ var URL_SCHEME_PATTERN2 = /^[A-Za-z][A-Za-z0-9+.-]*:/;
3396
+ function normalizeIdentifier2(value) {
3397
+ return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
3398
+ }
3399
+ function humanizeIdentifier3(value) {
3400
+ return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
3401
+ }
3402
+ function parseAtlasUnitValue(input, location, fieldKey) {
3403
+ const match = input.match(UNIT_PATTERN2);
3404
+ if (!match) {
3405
+ throw WorldOrbitError.fromLocation(`Invalid unit value "${input}"`, location);
3406
+ }
3407
+ const unitValue = {
3408
+ value: Number(match[1]),
3409
+ unit: match[2] ?? null
3410
+ };
3411
+ if (fieldKey) {
3412
+ const schema = getFieldSchema(fieldKey);
3413
+ if (schema?.unitFamily && !unitFamilyAllowsUnit(schema.unitFamily, unitValue.unit)) {
3414
+ throw WorldOrbitError.fromLocation(`Unit "${unitValue.unit ?? "none"}" is not valid for "${fieldKey}"`, location);
3415
+ }
3416
+ }
3417
+ return unitValue;
3418
+ }
3419
+ function tryParseAtlasUnitValue(input) {
3420
+ const match = input.match(UNIT_PATTERN2);
3421
+ if (!match) {
3422
+ return null;
3423
+ }
3424
+ return {
3425
+ value: Number(match[1]),
3426
+ unit: match[2] ?? null
3427
+ };
3428
+ }
3429
+ function parseAtlasNumber(input, key, location) {
3430
+ const value = Number(input);
3431
+ if (!Number.isFinite(value)) {
3432
+ throw WorldOrbitError.fromLocation(`Invalid numeric value "${input}" for "${key}"`, location);
3433
+ }
3434
+ return value;
3435
+ }
3436
+ function parseAtlasBoolean(input, key, location) {
3437
+ const parsed = BOOLEAN_VALUES2.get(input.toLowerCase());
3438
+ if (parsed === void 0) {
3439
+ throw WorldOrbitError.fromLocation(`Invalid boolean value "${input}" for "${key}"`, location);
3440
+ }
3441
+ return parsed;
3442
+ }
3443
+ function parseAtlasAtReference(target, location) {
3444
+ if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
3445
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
3446
+ }
3447
+ const pairedMatch = target.match(/^([A-Za-z0-9._-]+)-([A-Za-z0-9._-]+):(L[1-5])$/);
3448
+ if (pairedMatch) {
3449
+ return {
3450
+ kind: "lagrange",
3451
+ primary: pairedMatch[1],
3452
+ secondary: pairedMatch[2],
3453
+ point: pairedMatch[3]
3454
+ };
3455
+ }
3456
+ const simpleMatch = target.match(/^([A-Za-z0-9._-]+):(L[1-5])$/);
3457
+ if (simpleMatch) {
3458
+ return {
3459
+ kind: "lagrange",
3460
+ primary: simpleMatch[1],
3461
+ secondary: null,
3462
+ point: simpleMatch[2]
3463
+ };
3464
+ }
3465
+ if (/^[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
3466
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
3467
+ }
3468
+ const anchorMatch = target.match(/^([A-Za-z0-9._-]+):([A-Za-z0-9._-]+)$/);
3469
+ if (anchorMatch) {
3470
+ return {
3471
+ kind: "anchor",
3472
+ objectId: anchorMatch[1],
3473
+ anchor: anchorMatch[2]
3474
+ };
3475
+ }
3476
+ return {
3477
+ kind: "named",
3478
+ name: target
3479
+ };
3480
+ }
3481
+ function validateAtlasImageSource(value, location) {
3482
+ if (!value) {
3483
+ throw WorldOrbitError.fromLocation('Field "image" must not be empty', location);
3484
+ }
3485
+ if (value.startsWith("//")) {
3486
+ throw WorldOrbitError.fromLocation('Field "image" must use a relative path, root-relative path, or an http/https URL', location);
3487
+ }
3488
+ const schemeMatch = value.match(URL_SCHEME_PATTERN2);
3489
+ if (!schemeMatch) {
3490
+ return;
3491
+ }
3492
+ const scheme = schemeMatch[0].slice(0, -1).toLowerCase();
3493
+ if (scheme !== "http" && scheme !== "https") {
3494
+ throw WorldOrbitError.fromLocation(`Field "image" does not support the "${scheme}" scheme`, location);
3495
+ }
3496
+ }
3497
+ function normalizeLegacyScalarValue(key, values, location) {
3498
+ const schema = getFieldSchema(key);
3499
+ if (!schema) {
3500
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
3501
+ }
3502
+ if (schema.arity === "single" && values.length !== 1) {
3503
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
3504
+ }
3505
+ switch (schema.kind) {
3506
+ case "list":
3507
+ return values;
3508
+ case "boolean":
3509
+ return parseAtlasBoolean(singleAtlasValue(values, key, location), key, location);
3510
+ case "number":
3511
+ return parseAtlasNumber(singleAtlasValue(values, key, location), key, location);
3512
+ case "unit":
3513
+ return parseAtlasUnitValue(singleAtlasValue(values, key, location), location, key);
3514
+ case "string": {
3515
+ const value = values.join(" ").trim();
3516
+ if (key === "image") {
3517
+ validateAtlasImageSource(value, location);
3518
+ }
3519
+ return value;
3520
+ }
3521
+ }
3522
+ }
3523
+ function ensureAtlasFieldSupported(key, objectType, location) {
3524
+ const schema = getFieldSchema(key);
3525
+ if (!schema) {
3526
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
3527
+ }
3528
+ if (!schema.objectTypes.includes(objectType)) {
3529
+ throw WorldOrbitError.fromLocation(`Field "${key}" is not valid on "${objectType}"`, location);
3530
+ }
3531
+ }
3532
+ function singleAtlasValue(values, key, location) {
3533
+ if (values.length !== 1) {
3534
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
3535
+ }
3536
+ return values[0];
3537
+ }
3538
+
3539
+ // packages/core/dist/atlas-validate.js
3540
+ var SURFACE_TARGET_TYPES2 = /* @__PURE__ */ new Set(["star", "planet", "moon", "asteroid", "comet"]);
3541
+ var EARTH_MASSES_PER_SOLAR = 332946.0487;
3542
+ var JUPITER_MASSES_PER_SOLAR = 1047.3486;
3543
+ var AU_IN_KM2 = 1495978707e-1;
3544
+ var EARTH_RADIUS_IN_KM2 = 6371;
3545
+ var SOLAR_RADIUS_IN_KM2 = 695700;
3546
+ var LY_IN_AU2 = 63241.077;
3547
+ var PC_IN_AU2 = 206264.806;
3548
+ var KPC_IN_AU2 = 206264806;
3549
+ function collectAtlasDiagnostics(document2, sourceSchemaVersion) {
3550
+ const diagnostics = [];
3551
+ const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
3552
+ const groupIds = new Set(document2.groups.map((group) => group.id));
3553
+ if (!document2.system) {
3554
+ diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
3555
+ }
3556
+ const knownIds = /* @__PURE__ */ new Map();
3557
+ for (const [kind, ids] of [
3558
+ ["group", document2.groups.map((group) => group.id)],
3559
+ ["viewpoint", document2.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
3560
+ ["annotation", document2.system?.annotations.map((annotation) => annotation.id) ?? []],
3561
+ ["relation", document2.relations.map((relation) => relation.id)],
3562
+ ["object", document2.objects.map((object) => object.id)]
3563
+ ]) {
3564
+ for (const id of ids) {
3565
+ const previous = knownIds.get(id);
3566
+ if (previous) {
3567
+ diagnostics.push(error("validate.id.duplicate", `Duplicate ${kind} id "${id}" already used by ${previous}.`));
3568
+ } else {
3569
+ knownIds.set(id, kind);
3570
+ }
3571
+ }
3572
+ }
3573
+ for (const relation of document2.relations) {
3574
+ validateRelation(relation, objectMap, diagnostics);
3575
+ }
3576
+ for (const viewpoint of document2.system?.viewpoints ?? []) {
3577
+ validateViewpointFilter(viewpoint.filter, groupIds, sourceSchemaVersion, diagnostics, viewpoint.id);
3578
+ }
3579
+ for (const object of document2.objects) {
3580
+ validateObject(object, document2.system, objectMap, groupIds, diagnostics);
3581
+ }
3582
+ return diagnostics;
3583
+ }
3584
+ function validateRelation(relation, objectMap, diagnostics) {
3585
+ if (!relation.from) {
3586
+ diagnostics.push(error("validate.relation.from.required", `Relation "${relation.id}" is missing a "from" target.`));
3587
+ } else if (!objectMap.has(relation.from)) {
3588
+ diagnostics.push(error("validate.relation.from.unknown", `Unknown relation source "${relation.from}" on "${relation.id}".`));
3589
+ }
3590
+ if (!relation.to) {
3591
+ diagnostics.push(error("validate.relation.to.required", `Relation "${relation.id}" is missing a "to" target.`));
3592
+ } else if (!objectMap.has(relation.to)) {
3593
+ diagnostics.push(error("validate.relation.to.unknown", `Unknown relation target "${relation.to}" on "${relation.id}".`));
3594
+ }
3595
+ if (!relation.kind) {
3596
+ diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
3597
+ }
3598
+ }
3599
+ function validateViewpointFilter(filter, groupIds, sourceSchemaVersion, diagnostics, viewpointId) {
3600
+ if (!filter || sourceSchemaVersion !== "2.1") {
3601
+ return;
3602
+ }
3603
+ for (const groupId of filter.groupIds) {
3604
+ if (!groupIds.has(groupId)) {
3605
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`));
3606
+ }
3607
+ }
3608
+ }
3609
+ function validateObject(object, system, objectMap, groupIds, diagnostics) {
3610
+ const placement = object.placement;
3611
+ const orbitPlacement = placement?.mode === "orbit" ? placement : null;
3612
+ const parentObject = placement?.mode === "orbit" ? objectMap.get(placement.target) ?? null : null;
3613
+ if (object.groups) {
3614
+ for (const groupId of object.groups) {
3615
+ if (!groupIds.has(groupId)) {
3616
+ diagnostics.push(warn("validate.group.unknown", `Unknown group "${groupId}" on "${object.id}".`, object.id, "groups"));
3617
+ }
3618
+ }
3619
+ }
3620
+ if (orbitPlacement) {
3621
+ if (!objectMap.has(orbitPlacement.target)) {
3622
+ diagnostics.push(error("validate.orbit.target.unknown", `Unknown placement target "${orbitPlacement.target}" on "${object.id}".`, object.id, "orbit"));
3623
+ }
3624
+ if (orbitPlacement.distance && orbitPlacement.semiMajor) {
3625
+ diagnostics.push(error("validate.orbit.distanceConflict", `Object "${object.id}" cannot declare both "distance" and "semiMajor".`, object.id, "distance"));
3626
+ }
3627
+ if (orbitPlacement.phase && !object.epoch && !system?.epoch) {
3628
+ diagnostics.push(warn("validate.phase.epochMissing", `Object "${object.id}" sets "phase" without an object or system epoch.`, object.id, "phase"));
3629
+ }
3630
+ if (orbitPlacement.inclination && !object.referencePlane && !system?.referencePlane) {
3631
+ diagnostics.push(warn("validate.inclination.referencePlaneMissing", `Object "${object.id}" sets "inclination" without an object or system reference plane.`, object.id, "inclination"));
3632
+ }
3633
+ if (orbitPlacement.period && !massInSolar(parentObject?.properties.mass)) {
3634
+ diagnostics.push(warn("validate.period.massMissing", `Object "${object.id}" sets "period" but its central mass cannot be derived.`, object.id, "period"));
3635
+ }
3636
+ }
3637
+ if (placement?.mode === "surface") {
3638
+ const target = objectMap.get(placement.target);
3639
+ if (!target) {
3640
+ diagnostics.push(error("validate.surface.target.unknown", `Unknown placement target "${placement.target}" on "${object.id}".`, object.id, "surface"));
3641
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
3642
+ diagnostics.push(error("validate.surface.target.invalid", `Surface target "${placement.target}" on "${object.id}" is not surface-capable.`, object.id, "surface"));
3643
+ }
3644
+ }
3645
+ if (placement?.mode === "at") {
3646
+ if (object.type !== "structure" && object.type !== "phenomenon") {
3647
+ diagnostics.push(error("validate.at.objectType", `Only structures and phenomena may use "at" placement; found "${object.type}" on "${object.id}".`, object.id, "at"));
3648
+ }
3649
+ if (!validateAtTarget(object, objectMap, diagnostics)) {
3650
+ diagnostics.push(error("validate.at.target.unknown", `Unknown at-reference target "${placement.target}" on "${object.id}".`, object.id, "at"));
3651
+ }
3652
+ }
3653
+ if (object.resonance) {
3654
+ const target = objectMap.get(object.resonance.targetObjectId);
3655
+ if (!target) {
3656
+ diagnostics.push(error("validate.resonance.target.unknown", `Unknown resonance target "${object.resonance.targetObjectId}" on "${object.id}".`, object.id, "resonance"));
3657
+ } else if (object.placement?.mode !== "orbit" || target.placement?.mode !== "orbit" || object.placement.target !== target.placement.target) {
3658
+ diagnostics.push(warn("validate.resonance.orbitMismatch", `Resonance target "${object.resonance.targetObjectId}" on "${object.id}" does not share a compatible orbital parent.`, object.id, "resonance"));
3659
+ }
3660
+ }
3661
+ for (const rule of object.deriveRules ?? []) {
3662
+ if (rule.field !== "period" || rule.strategy !== "kepler") {
3663
+ diagnostics.push(warn("validate.derive.unsupported", `Unsupported derive rule "${rule.field} ${rule.strategy}" on "${object.id}".`, object.id, "derive"));
3664
+ continue;
3665
+ }
3666
+ const derivedPeriodDays = keplerPeriodDays(object, parentObject);
3667
+ if (derivedPeriodDays === null) {
3668
+ diagnostics.push(warn("validate.derive.inputsMissing", `Object "${object.id}" requests "derive period kepler" but lacks enough input data.`, object.id, "derive"));
3669
+ continue;
3670
+ }
3671
+ if (!orbitPlacement?.period) {
3672
+ diagnostics.push(info("validate.derive.period.available", `Object "${object.id}" can derive a Kepler period of ${formatDays(derivedPeriodDays)}.`, object.id, "derive"));
3673
+ }
3674
+ }
3675
+ for (const rule of object.validationRules ?? []) {
3676
+ if (rule.rule !== "kepler") {
3677
+ diagnostics.push(warn("validate.rule.unsupported", `Unsupported validation rule "${rule.rule}" on "${object.id}".`, object.id, "validate"));
3678
+ continue;
3679
+ }
3680
+ const actualPeriodDays = durationInDays(orbitPlacement?.period);
3681
+ const derivedPeriodDays = keplerPeriodDays(object, parentObject);
3682
+ if (actualPeriodDays === null || derivedPeriodDays === null) {
3683
+ continue;
3684
+ }
3685
+ const toleranceDays = toleranceForField(object, "period");
3686
+ if (Math.abs(actualPeriodDays - derivedPeriodDays) > toleranceDays) {
3687
+ diagnostics.push(error("validate.kepler.mismatch", `Object "${object.id}" fails Kepler validation for "period".`, object.id, "validate"));
3688
+ }
3689
+ }
3690
+ }
3691
+ function validateAtTarget(object, objectMap, diagnostics) {
3692
+ const reference = object.placement?.mode === "at" ? object.placement.reference : null;
3693
+ if (!reference) {
3694
+ return true;
3695
+ }
3696
+ if (reference.kind === "named") {
3697
+ return objectMap.has(reference.name);
3698
+ }
3699
+ if (reference.kind === "anchor") {
3700
+ if (!objectMap.has(reference.objectId)) {
3701
+ diagnostics.push(error("validate.anchor.target.unknown", `Unknown anchor target "${reference.objectId}" on "${object.id}".`, object.id, "at"));
3702
+ return false;
3703
+ }
3704
+ return true;
3705
+ }
3706
+ if (!objectMap.has(reference.primary)) {
3707
+ diagnostics.push(error("validate.lagrange.primary.unknown", `Unknown Lagrange reference "${reference.primary}" on "${object.id}".`, object.id, "at"));
3708
+ return false;
3709
+ }
3710
+ if (reference.secondary && !objectMap.has(reference.secondary)) {
3711
+ diagnostics.push(error("validate.lagrange.secondary.unknown", `Unknown Lagrange reference "${reference.secondary}" on "${object.id}".`, object.id, "at"));
3712
+ return false;
3713
+ }
3714
+ return true;
3715
+ }
3716
+ function keplerPeriodDays(object, parentObject) {
3717
+ const placement = object.placement;
3718
+ if (!placement || placement.mode !== "orbit") {
3719
+ return null;
3720
+ }
3721
+ const semiMajorAu = distanceInAu(placement.semiMajor ?? placement.distance);
3722
+ const centralMassSolar = massInSolar(parentObject?.properties.mass);
3723
+ if (semiMajorAu === null || centralMassSolar === null || centralMassSolar <= 0) {
3724
+ return null;
3725
+ }
3726
+ const periodYears = Math.sqrt(semiMajorAu ** 3 / centralMassSolar);
3727
+ return periodYears * 365.25;
3728
+ }
3729
+ function distanceInAu(value) {
3730
+ if (!value)
3731
+ return null;
3732
+ switch (value.unit) {
3733
+ case null:
3734
+ case "au":
3735
+ return value.value;
3736
+ case "km":
3737
+ return value.value / AU_IN_KM2;
3738
+ case "m":
3739
+ return value.value / (AU_IN_KM2 * 1e3);
3740
+ case "ly":
3741
+ return value.value * LY_IN_AU2;
3742
+ case "pc":
3743
+ return value.value * PC_IN_AU2;
3744
+ case "kpc":
3745
+ return value.value * KPC_IN_AU2;
3746
+ case "re":
3747
+ return value.value * EARTH_RADIUS_IN_KM2 / AU_IN_KM2;
3748
+ case "sol":
3749
+ return value.value * SOLAR_RADIUS_IN_KM2 / AU_IN_KM2;
3750
+ default:
3751
+ return null;
3752
+ }
3753
+ }
3754
+ function massInSolar(value) {
3755
+ if (!value || typeof value !== "object" || !("value" in value)) {
3756
+ return null;
3757
+ }
3758
+ const unitValue = value;
3759
+ switch (unitValue.unit) {
3760
+ case null:
3761
+ case "sol":
3762
+ return unitValue.value;
3763
+ case "me":
3764
+ return unitValue.value / EARTH_MASSES_PER_SOLAR;
3765
+ case "mj":
3766
+ return unitValue.value / JUPITER_MASSES_PER_SOLAR;
3767
+ default:
3768
+ return null;
3769
+ }
3770
+ }
3771
+ function durationInDays(value) {
3772
+ if (!value)
3773
+ return null;
3774
+ switch (value.unit) {
3775
+ case null:
3776
+ case "d":
3777
+ return value.value;
3778
+ case "s":
3779
+ return value.value / 86400;
3780
+ case "min":
3781
+ return value.value / 1440;
3782
+ case "h":
3783
+ return value.value / 24;
3784
+ case "y":
3785
+ return value.value * 365.25;
3786
+ case "ky":
3787
+ return value.value * 365250;
3788
+ case "my":
3789
+ return value.value * 36525e4;
3790
+ case "gy":
3791
+ return value.value * 36525e7;
3792
+ default:
3793
+ return null;
3794
+ }
3795
+ }
3796
+ function toleranceForField(object, field) {
3797
+ const tolerance = object.tolerances?.find((entry) => entry.field === field)?.value;
3798
+ if (typeof tolerance === "number") {
3799
+ return tolerance;
3800
+ }
3801
+ if (tolerance && typeof tolerance === "object" && "value" in tolerance) {
3802
+ return durationInDays(tolerance) ?? 0;
3803
+ }
3804
+ return 0;
3805
+ }
3806
+ function formatDays(days) {
3807
+ return `${Math.round(days * 100) / 100}d`;
3808
+ }
3809
+ function error(code, message, objectId, field) {
3810
+ return { code, severity: "error", source: "validate", message, objectId, field };
3811
+ }
3812
+ function warn(code, message, objectId, field) {
3813
+ return { code, severity: "warning", source: "validate", message, objectId, field };
3814
+ }
3815
+ function info(code, message, objectId, field) {
3816
+ return { code, severity: "info", source: "validate", message, objectId, field };
3817
+ }
3818
+
3145
3819
  // packages/core/dist/draft-parse.js
3820
+ var STRUCTURED_TYPED_BLOCKS = /* @__PURE__ */ new Set([
3821
+ "climate",
3822
+ "habitability",
3823
+ "settlement"
3824
+ ]);
3825
+ var DRAFT_OBJECT_FIELD_SPECS = /* @__PURE__ */ new Map();
3826
+ for (const key of [
3827
+ "orbit",
3828
+ "distance",
3829
+ "semiMajor",
3830
+ "eccentricity",
3831
+ "period",
3832
+ "angle",
3833
+ "inclination",
3834
+ "phase",
3835
+ "at",
3836
+ "surface",
3837
+ "free",
3838
+ "kind",
3839
+ "class",
3840
+ "culture",
3841
+ "tags",
3842
+ "color",
3843
+ "image",
3844
+ "hidden",
3845
+ "radius",
3846
+ "mass",
3847
+ "density",
3848
+ "gravity",
3849
+ "temperature",
3850
+ "albedo",
3851
+ "atmosphere",
3852
+ "inner",
3853
+ "outer",
3854
+ "on",
3855
+ "source",
3856
+ "cycle"
3857
+ ]) {
3858
+ const schema = getFieldSchema(key);
3859
+ if (schema) {
3860
+ DRAFT_OBJECT_FIELD_SPECS.set(key, {
3861
+ key,
3862
+ version: "2.0",
3863
+ inlineMode: schema.arity === "multiple" ? "multiple" : "single",
3864
+ allowRepeat: false,
3865
+ legacySchema: schema
3866
+ });
3867
+ }
3868
+ }
3869
+ for (const spec of [
3870
+ { key: "groups", inlineMode: "multiple", allowRepeat: false },
3871
+ { key: "epoch", inlineMode: "single", allowRepeat: false },
3872
+ { key: "referencePlane", inlineMode: "single", allowRepeat: false },
3873
+ { key: "tidalLock", inlineMode: "single", allowRepeat: false },
3874
+ { key: "renderLabel", inlineMode: "single", allowRepeat: false },
3875
+ { key: "renderOrbit", inlineMode: "single", allowRepeat: false },
3876
+ { key: "renderPriority", inlineMode: "single", allowRepeat: false },
3877
+ { key: "resonance", inlineMode: "pair", allowRepeat: false },
3878
+ { key: "derive", inlineMode: "pair", allowRepeat: true },
3879
+ { key: "validate", inlineMode: "single", allowRepeat: true },
3880
+ { key: "locked", inlineMode: "multiple", allowRepeat: false },
3881
+ { key: "tolerance", inlineMode: "pair", allowRepeat: true }
3882
+ ]) {
3883
+ DRAFT_OBJECT_FIELD_SPECS.set(spec.key, {
3884
+ key: spec.key,
3885
+ version: "2.1",
3886
+ inlineMode: spec.inlineMode,
3887
+ allowRepeat: spec.allowRepeat
3888
+ });
3889
+ }
3890
+ var DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
3146
3891
  function parseWorldOrbitAtlas(source) {
3147
- return parseAtlasSource(source, "2.0");
3892
+ return parseAtlasSource(source);
3148
3893
  }
3149
3894
  function parseWorldOrbitDraft(source) {
3150
3895
  return parseAtlasSource(source, "2.0-draft");
3151
3896
  }
3152
- function parseAtlasSource(source, outputVersion) {
3153
- const lines = source.split(/\r?\n/);
3897
+ function parseAtlasSource(source, forcedOutputVersion) {
3898
+ const prepared = preprocessAtlasSource(source);
3899
+ const lines = prepared.source.split(/\r?\n/);
3900
+ const diagnostics = [];
3154
3901
  let sawSchemaHeader = false;
3155
- let schemaVersion = "2.0";
3902
+ let sourceSchemaVersion = "2.0";
3156
3903
  let system = null;
3157
3904
  let section = null;
3158
3905
  const objectNodes = [];
3906
+ const groups = [];
3907
+ const relations = [];
3159
3908
  let sawDefaults = false;
3160
3909
  let sawAtlas = false;
3161
3910
  const viewpointIds = /* @__PURE__ */ new Set();
3162
3911
  const annotationIds = /* @__PURE__ */ new Set();
3912
+ const groupIds = /* @__PURE__ */ new Set();
3913
+ const relationIds = /* @__PURE__ */ new Set();
3163
3914
  for (let index = 0; index < lines.length; index++) {
3164
3915
  const rawLine = lines[index];
3165
3916
  const lineNumber = index + 1;
@@ -3175,15 +3926,22 @@ var WorldOrbit = (() => {
3175
3926
  continue;
3176
3927
  }
3177
3928
  if (!sawSchemaHeader) {
3178
- schemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
3929
+ sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
3179
3930
  sawSchemaHeader = true;
3931
+ if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
3932
+ diagnostics.push({
3933
+ code: "parse.schema21.commentCompatibility",
3934
+ severity: "warning",
3935
+ source: "parse",
3936
+ message: `Comments require schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
3937
+ line: prepared.comments[0].line,
3938
+ column: prepared.comments[0].column
3939
+ });
3940
+ }
3180
3941
  continue;
3181
3942
  }
3182
3943
  if (indent === 0) {
3183
- section = startTopLevelSection(tokens, lineNumber, system, objectNodes, viewpointIds, annotationIds, {
3184
- sawDefaults,
3185
- sawAtlas
3186
- });
3944
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, { sawDefaults, sawAtlas });
3187
3945
  if (section.kind === "system") {
3188
3946
  system = section.system;
3189
3947
  } else if (section.kind === "defaults") {
@@ -3201,48 +3959,57 @@ var WorldOrbit = (() => {
3201
3959
  if (!sawSchemaHeader) {
3202
3960
  throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
3203
3961
  }
3204
- const ast = {
3205
- type: "document",
3206
- objects: objectNodes
3207
- };
3208
- const normalizedObjects = normalizeDocument(ast).objects;
3209
- validateDocument({
3210
- format: "worldorbit",
3211
- version: "1.0",
3212
- system: null,
3213
- objects: normalizedObjects
3214
- });
3215
- const diagnostics = schemaVersion === "2.0-draft" && outputVersion === "2.0" ? [
3216
- {
3217
- code: "load.schema.deprecatedDraft",
3218
- severity: "warning",
3219
- source: "upgrade",
3220
- message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".'
3221
- }
3222
- ] : [];
3223
- return {
3962
+ const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
3963
+ const outputVersion = forcedOutputVersion ?? (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
3964
+ const baseDocument = {
3224
3965
  format: "worldorbit",
3225
- version: outputVersion,
3226
3966
  sourceVersion: "1.0",
3227
3967
  system,
3228
- objects: normalizedObjects,
3968
+ groups,
3969
+ relations,
3970
+ objects,
3229
3971
  diagnostics
3230
3972
  };
3973
+ if (outputVersion === "2.0-draft") {
3974
+ const document3 = {
3975
+ ...baseDocument,
3976
+ version: "2.0-draft",
3977
+ schemaVersion: "2.0-draft"
3978
+ };
3979
+ document3.diagnostics.push(...collectAtlasDiagnostics(document3, sourceSchemaVersion));
3980
+ return document3;
3981
+ }
3982
+ const document2 = {
3983
+ ...baseDocument,
3984
+ version: outputVersion,
3985
+ schemaVersion: outputVersion
3986
+ };
3987
+ if (sourceSchemaVersion === "2.0-draft") {
3988
+ document2.diagnostics.push({
3989
+ code: "load.schema.deprecatedDraft",
3990
+ severity: "warning",
3991
+ source: "upgrade",
3992
+ message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".'
3993
+ });
3994
+ }
3995
+ document2.diagnostics.push(...collectAtlasDiagnostics(document2, sourceSchemaVersion));
3996
+ return document2;
3231
3997
  }
3232
3998
  function assertDraftSchemaHeader(tokens, line) {
3233
- if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || tokens[1].value.toLowerCase() !== "2.0-draft" && tokens[1].value.toLowerCase() !== "2.0") {
3234
- throw new WorldOrbitError('Expected atlas header "schema 2.0" or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
3999
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
4000
+ throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
3235
4001
  }
3236
- return tokens[1].value.toLowerCase() === "2.0-draft" ? "2.0-draft" : "2.0";
4002
+ const version = tokens[1].value.toLowerCase();
4003
+ return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
3237
4004
  }
3238
- function startTopLevelSection(tokens, line, system, objectNodes, viewpointIds, annotationIds, flags) {
4005
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, flags) {
3239
4006
  const keyword = tokens[0]?.value.toLowerCase();
3240
4007
  switch (keyword) {
3241
4008
  case "system":
3242
4009
  if (system) {
3243
4010
  throw new WorldOrbitError('Atlas section "system" may only appear once', line, tokens[0].column);
3244
4011
  }
3245
- return startSystemSection(tokens, line);
4012
+ return startSystemSection(tokens, line, sourceSchemaVersion, diagnostics);
3246
4013
  case "defaults":
3247
4014
  if (!system) {
3248
4015
  throw new WorldOrbitError('Atlas section "defaults" requires a preceding system declaration', line, tokens[0].column);
@@ -3278,13 +4045,19 @@ var WorldOrbit = (() => {
3278
4045
  throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
3279
4046
  }
3280
4047
  return startAnnotationSection(tokens, line, system, annotationIds);
4048
+ case "group":
4049
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "group", { line, column: tokens[0].column });
4050
+ return startGroupSection(tokens, line, groups, groupIds);
4051
+ case "relation":
4052
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
4053
+ return startRelationSection(tokens, line, relations, relationIds);
3281
4054
  case "object":
3282
- return startObjectSection(tokens, line, objectNodes);
4055
+ return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
3283
4056
  default:
3284
4057
  throw new WorldOrbitError(`Unknown atlas section "${tokens[0]?.value ?? ""}"`, line, tokens[0]?.column ?? 1);
3285
4058
  }
3286
4059
  }
3287
- function startSystemSection(tokens, line) {
4060
+ function startSystemSection(tokens, line, sourceSchemaVersion, diagnostics) {
3288
4061
  if (tokens.length !== 2) {
3289
4062
  throw new WorldOrbitError("Invalid atlas system declaration", line, tokens[0]?.column ?? 1);
3290
4063
  }
@@ -3292,6 +4065,9 @@ var WorldOrbit = (() => {
3292
4065
  type: "system",
3293
4066
  id: tokens[1].value,
3294
4067
  title: null,
4068
+ description: null,
4069
+ epoch: null,
4070
+ referencePlane: null,
3295
4071
  defaults: {
3296
4072
  view: "topdown",
3297
4073
  scale: null,
@@ -3306,6 +4082,8 @@ var WorldOrbit = (() => {
3306
4082
  return {
3307
4083
  kind: "system",
3308
4084
  system,
4085
+ sourceSchemaVersion,
4086
+ diagnostics,
3309
4087
  seenFields: /* @__PURE__ */ new Set()
3310
4088
  };
3311
4089
  }
@@ -3371,7 +4149,64 @@ var WorldOrbit = (() => {
3371
4149
  seenFields: /* @__PURE__ */ new Set()
3372
4150
  };
3373
4151
  }
3374
- function startObjectSection(tokens, line, objectNodes) {
4152
+ function startGroupSection(tokens, line, groups, groupIds) {
4153
+ if (tokens.length !== 2) {
4154
+ throw new WorldOrbitError("Invalid group declaration", line, tokens[0]?.column ?? 1);
4155
+ }
4156
+ const id = normalizeIdentifier2(tokens[1].value);
4157
+ if (!id) {
4158
+ throw new WorldOrbitError("Group id must not be empty", line, tokens[1].column);
4159
+ }
4160
+ if (groupIds.has(id)) {
4161
+ throw new WorldOrbitError(`Duplicate group id "${id}"`, line, tokens[1].column);
4162
+ }
4163
+ const group = {
4164
+ id,
4165
+ label: humanizeIdentifier3(id),
4166
+ summary: "",
4167
+ color: null,
4168
+ tags: [],
4169
+ hidden: false
4170
+ };
4171
+ groups.push(group);
4172
+ groupIds.add(id);
4173
+ return {
4174
+ kind: "group",
4175
+ group,
4176
+ seenFields: /* @__PURE__ */ new Set()
4177
+ };
4178
+ }
4179
+ function startRelationSection(tokens, line, relations, relationIds) {
4180
+ if (tokens.length !== 2) {
4181
+ throw new WorldOrbitError("Invalid relation declaration", line, tokens[0]?.column ?? 1);
4182
+ }
4183
+ const id = normalizeIdentifier2(tokens[1].value);
4184
+ if (!id) {
4185
+ throw new WorldOrbitError("Relation id must not be empty", line, tokens[1].column);
4186
+ }
4187
+ if (relationIds.has(id)) {
4188
+ throw new WorldOrbitError(`Duplicate relation id "${id}"`, line, tokens[1].column);
4189
+ }
4190
+ const relation = {
4191
+ id,
4192
+ from: "",
4193
+ to: "",
4194
+ kind: "",
4195
+ label: null,
4196
+ summary: null,
4197
+ tags: [],
4198
+ color: null,
4199
+ hidden: false
4200
+ };
4201
+ relations.push(relation);
4202
+ relationIds.add(id);
4203
+ return {
4204
+ kind: "relation",
4205
+ relation,
4206
+ seenFields: /* @__PURE__ */ new Set()
4207
+ };
4208
+ }
4209
+ function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
3375
4210
  if (tokens.length < 3) {
3376
4211
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
3377
4212
  }
@@ -3382,12 +4217,11 @@ var WorldOrbit = (() => {
3382
4217
  throw new WorldOrbitError(`Unknown object type "${objectTypeToken.value}"`, line, objectTypeToken.column);
3383
4218
  }
3384
4219
  const objectNode = {
3385
- type: "object",
3386
4220
  objectType,
3387
- name: idToken.value,
3388
- inlineFields: parseInlineFields2(tokens.slice(3), line),
3389
- blockFields: [],
4221
+ id: idToken.value,
4222
+ fields: parseInlineObjectFields(tokens.slice(3), line, objectType, sourceSchemaVersion, diagnostics),
3390
4223
  infoEntries: [],
4224
+ typedBlockEntries: {},
3391
4225
  location: {
3392
4226
  line,
3393
4227
  column: objectTypeToken.column
@@ -3397,8 +4231,12 @@ var WorldOrbit = (() => {
3397
4231
  return {
3398
4232
  kind: "object",
3399
4233
  objectNode,
3400
- inInfoBlock: false,
3401
- infoIndent: null
4234
+ sourceSchemaVersion,
4235
+ diagnostics,
4236
+ activeBlock: null,
4237
+ blockIndent: null,
4238
+ seenInfoKeys: /* @__PURE__ */ new Set(),
4239
+ seenTypedBlockKeys: {}
3402
4240
  };
3403
4241
  }
3404
4242
  function handleSectionLine(section, indent, tokens, line) {
@@ -3418,6 +4256,12 @@ var WorldOrbit = (() => {
3418
4256
  case "annotation":
3419
4257
  applyAnnotationField(section, tokens, line);
3420
4258
  return;
4259
+ case "group":
4260
+ applyGroupField(section, tokens, line);
4261
+ return;
4262
+ case "relation":
4263
+ applyRelationField(section, tokens, line);
4264
+ return;
3421
4265
  case "object":
3422
4266
  applyObjectField(section, indent, tokens, line);
3423
4267
  return;
@@ -3425,10 +4269,35 @@ var WorldOrbit = (() => {
3425
4269
  }
3426
4270
  function applySystemField(section, tokens, line) {
3427
4271
  const key = requireUniqueField(tokens, section.seenFields, line);
3428
- if (key !== "title") {
3429
- throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
4272
+ const value = joinFieldValue(tokens, line);
4273
+ switch (key) {
4274
+ case "title":
4275
+ section.system.title = value;
4276
+ return;
4277
+ case "description":
4278
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
4279
+ line,
4280
+ column: tokens[0].column
4281
+ });
4282
+ section.system.description = value;
4283
+ return;
4284
+ case "epoch":
4285
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
4286
+ line,
4287
+ column: tokens[0].column
4288
+ });
4289
+ section.system.epoch = value;
4290
+ return;
4291
+ case "referenceplane":
4292
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "referencePlane", {
4293
+ line,
4294
+ column: tokens[0].column
4295
+ });
4296
+ section.system.referencePlane = value;
4297
+ return;
4298
+ default:
4299
+ throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
3430
4300
  }
3431
- section.system.title = joinFieldValue(tokens, line);
3432
4301
  }
3433
4302
  function applyDefaultsField(section, tokens, line) {
3434
4303
  const key = requireUniqueField(tokens, section.seenFields, line);
@@ -3459,14 +4328,11 @@ var WorldOrbit = (() => {
3459
4328
  section.metadataIndent = null;
3460
4329
  }
3461
4330
  if (section.inMetadata) {
3462
- if (tokens.length < 2) {
3463
- throw new WorldOrbitError("Invalid atlas metadata entry", line, tokens[0]?.column ?? 1);
3464
- }
3465
- const key = tokens[0].value;
3466
- if (key in section.system.atlasMetadata) {
3467
- throw new WorldOrbitError(`Duplicate atlas metadata key "${key}"`, line, tokens[0].column);
4331
+ const entry = parseInfoLikeEntry(tokens, line, "Invalid atlas metadata entry");
4332
+ if (entry.key in section.system.atlasMetadata) {
4333
+ throw new WorldOrbitError(`Duplicate atlas metadata key "${entry.key}"`, line, tokens[0].column);
3468
4334
  }
3469
- section.system.atlasMetadata[key] = joinFieldValue(tokens, line);
4335
+ section.system.atlasMetadata[entry.key] = entry.value;
3470
4336
  return;
3471
4337
  }
3472
4338
  if (tokens.length === 1 && tokens[0].value.toLowerCase() === "metadata") {
@@ -3562,27 +4428,108 @@ var WorldOrbit = (() => {
3562
4428
  section.annotation.body = joinFieldValue(tokens, line);
3563
4429
  return;
3564
4430
  case "tags":
3565
- section.annotation.tags = parseTokenList(tokens.slice(1), line, "tags");
4431
+ section.annotation.tags = parseTokenList(tokens.slice(1), line, "tags");
4432
+ return;
4433
+ default:
4434
+ throw new WorldOrbitError(`Unknown annotation field "${tokens[0].value}"`, line, tokens[0].column);
4435
+ }
4436
+ }
4437
+ function applyGroupField(section, tokens, line) {
4438
+ const key = requireUniqueField(tokens, section.seenFields, line);
4439
+ switch (key) {
4440
+ case "label":
4441
+ section.group.label = joinFieldValue(tokens, line);
4442
+ return;
4443
+ case "summary":
4444
+ section.group.summary = joinFieldValue(tokens, line);
4445
+ return;
4446
+ case "color":
4447
+ section.group.color = joinFieldValue(tokens, line);
4448
+ return;
4449
+ case "tags":
4450
+ section.group.tags = parseTokenList(tokens.slice(1), line, "tags");
4451
+ return;
4452
+ case "hidden":
4453
+ section.group.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4454
+ line,
4455
+ column: tokens[0].column
4456
+ });
4457
+ return;
4458
+ default:
4459
+ throw new WorldOrbitError(`Unknown group field "${tokens[0].value}"`, line, tokens[0].column);
4460
+ }
4461
+ }
4462
+ function applyRelationField(section, tokens, line) {
4463
+ const key = requireUniqueField(tokens, section.seenFields, line);
4464
+ switch (key) {
4465
+ case "from":
4466
+ section.relation.from = joinFieldValue(tokens, line);
4467
+ return;
4468
+ case "to":
4469
+ section.relation.to = joinFieldValue(tokens, line);
4470
+ return;
4471
+ case "kind":
4472
+ section.relation.kind = joinFieldValue(tokens, line);
4473
+ return;
4474
+ case "label":
4475
+ section.relation.label = joinFieldValue(tokens, line);
4476
+ return;
4477
+ case "summary":
4478
+ section.relation.summary = joinFieldValue(tokens, line);
4479
+ return;
4480
+ case "tags":
4481
+ section.relation.tags = parseTokenList(tokens.slice(1), line, "tags");
4482
+ return;
4483
+ case "color":
4484
+ section.relation.color = joinFieldValue(tokens, line);
4485
+ return;
4486
+ case "hidden":
4487
+ section.relation.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4488
+ line,
4489
+ column: tokens[0].column
4490
+ });
3566
4491
  return;
3567
4492
  default:
3568
- throw new WorldOrbitError(`Unknown annotation field "${tokens[0].value}"`, line, tokens[0].column);
4493
+ throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
3569
4494
  }
3570
4495
  }
3571
4496
  function applyObjectField(section, indent, tokens, line) {
3572
- if (tokens.length === 1 && tokens[0].value === "info") {
3573
- section.inInfoBlock = true;
3574
- section.infoIndent = indent;
3575
- return;
3576
- }
3577
- if (section.inInfoBlock && indent <= (section.infoIndent ?? 0)) {
3578
- section.inInfoBlock = false;
3579
- section.infoIndent = null;
4497
+ if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
4498
+ section.activeBlock = null;
4499
+ section.blockIndent = null;
4500
+ }
4501
+ if (tokens.length === 1) {
4502
+ const blockName = tokens[0].value.toLowerCase();
4503
+ if (blockName === "info" || STRUCTURED_TYPED_BLOCKS.has(blockName)) {
4504
+ if (blockName !== "info") {
4505
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, blockName, { line, column: tokens[0].column });
4506
+ }
4507
+ section.activeBlock = blockName;
4508
+ section.blockIndent = indent;
4509
+ return;
4510
+ }
3580
4511
  }
3581
- if (section.inInfoBlock) {
3582
- section.objectNode.infoEntries.push(parseInfoEntry2(tokens, line));
4512
+ if (section.activeBlock) {
4513
+ const entry = parseInfoLikeEntry(tokens, line, `Invalid ${section.activeBlock} entry`);
4514
+ if (section.activeBlock === "info") {
4515
+ if (section.seenInfoKeys.has(entry.key)) {
4516
+ throw new WorldOrbitError(`Duplicate info key "${entry.key}"`, line, tokens[0].column);
4517
+ }
4518
+ section.seenInfoKeys.add(entry.key);
4519
+ section.objectNode.infoEntries.push(entry);
4520
+ return;
4521
+ }
4522
+ const typedBlock = section.activeBlock;
4523
+ const seenKeys = section.seenTypedBlockKeys[typedBlock] ?? (section.seenTypedBlockKeys[typedBlock] = /* @__PURE__ */ new Set());
4524
+ if (seenKeys.has(entry.key)) {
4525
+ throw new WorldOrbitError(`Duplicate ${typedBlock} key "${entry.key}"`, line, tokens[0].column);
4526
+ }
4527
+ seenKeys.add(entry.key);
4528
+ const entries = section.objectNode.typedBlockEntries[typedBlock] ?? (section.objectNode.typedBlockEntries[typedBlock] = []);
4529
+ entries.push(entry);
3583
4530
  return;
3584
4531
  }
3585
- section.objectNode.blockFields.push(parseField2(tokens, line));
4532
+ section.objectNode.fields.push(parseObjectField(tokens, line, section.objectNode.objectType, section.sourceSchemaVersion, section.diagnostics));
3586
4533
  }
3587
4534
  function requireUniqueField(tokens, seenFields, line) {
3588
4535
  if (tokens.length < 2) {
@@ -3602,50 +4549,40 @@ var WorldOrbit = (() => {
3602
4549
  return tokens.slice(1).map((token) => token.value).join(" ").trim();
3603
4550
  }
3604
4551
  function parseObjectTypeTokens(tokens, line) {
3605
- if (tokens.length === 0) {
3606
- throw new WorldOrbitError("Missing value for atlas field", line);
3607
- }
3608
- return tokens.map((token) => {
3609
- const value = token.value;
3610
- if (value !== "star" && value !== "planet" && value !== "moon" && value !== "belt" && value !== "asteroid" && value !== "comet" && value !== "ring" && value !== "structure" && value !== "phenomenon") {
3611
- throw new WorldOrbitError(`Unknown viewpoint object type "${token.value}"`, line, token.column);
3612
- }
3613
- return value;
3614
- });
3615
- }
3616
- function parseTokenList(tokens, line, field) {
3617
- if (tokens.length === 0) {
3618
- throw new WorldOrbitError(`Missing value for field "${field}"`, line);
3619
- }
3620
- return tokens.map((token) => token.value);
4552
+ 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");
3621
4553
  }
3622
4554
  function parseLayerTokens(tokens, line) {
3623
- if (tokens.length === 0) {
3624
- throw new WorldOrbitError('Missing value for field "layers"', line);
3625
- }
3626
- const next = {};
3627
- for (const token of tokens) {
3628
- const enabled = !token.value.startsWith("-") && !token.value.startsWith("!");
3629
- const rawLayer = token.value.replace(/^[-!]+/, "").toLowerCase();
3630
- if (rawLayer === "orbits") {
3631
- next["orbits-back"] = enabled;
3632
- next["orbits-front"] = enabled;
4555
+ const layers = {};
4556
+ for (const token of parseTokenList(tokens, line, "layers")) {
4557
+ const enabled = !token.startsWith("-") && !token.startsWith("!");
4558
+ const raw = token.replace(/^[-!]+/, "").toLowerCase();
4559
+ if (raw === "orbits") {
4560
+ layers["orbits-back"] = enabled;
4561
+ layers["orbits-front"] = enabled;
3633
4562
  continue;
3634
4563
  }
3635
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
3636
- next[rawLayer] = enabled;
3637
- continue;
4564
+ if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "objects" || raw === "labels" || raw === "metadata") {
4565
+ layers[raw] = enabled;
3638
4566
  }
3639
- throw new WorldOrbitError(`Unknown layer token "${token.value}"`, line, token.column);
3640
4567
  }
3641
- return next;
4568
+ return layers;
4569
+ }
4570
+ function parseTokenList(tokens, line, fieldName) {
4571
+ if (tokens.length === 0) {
4572
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, 1);
4573
+ }
4574
+ const values = tokens.map((token) => token.value).filter(Boolean);
4575
+ if (values.length === 0) {
4576
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, tokens[0]?.column ?? 1);
4577
+ }
4578
+ return values;
3642
4579
  }
3643
4580
  function parseProjectionValue(value, line, column) {
3644
4581
  const normalized = value.toLowerCase();
3645
- if (normalized === "topdown" || normalized === "isometric") {
3646
- return normalized;
4582
+ if (normalized !== "topdown" && normalized !== "isometric") {
4583
+ throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
3647
4584
  }
3648
- throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
4585
+ return normalized;
3649
4586
  }
3650
4587
  function parsePresetValue(value, line, column) {
3651
4588
  const normalized = value.toLowerCase();
@@ -3655,16 +4592,16 @@ var WorldOrbit = (() => {
3655
4592
  throw new WorldOrbitError(`Unknown render preset "${value}"`, line, column);
3656
4593
  }
3657
4594
  function parsePositiveNumber2(value, line, column, field) {
3658
- const parsed = Number(value);
3659
- if (!Number.isFinite(parsed) || parsed <= 0) {
3660
- throw new WorldOrbitError(`Field "${field}" expects a positive number`, line, column);
4595
+ const parsed = parseFiniteNumber2(value, line, column, field);
4596
+ if (parsed <= 0) {
4597
+ throw new WorldOrbitError(`Field "${field}" must be greater than zero`, line, column);
3661
4598
  }
3662
4599
  return parsed;
3663
4600
  }
3664
4601
  function parseFiniteNumber2(value, line, column, field) {
3665
4602
  const parsed = Number(value);
3666
4603
  if (!Number.isFinite(parsed)) {
3667
- throw new WorldOrbitError(`Field "${field}" expects a finite number`, line, column);
4604
+ throw new WorldOrbitError(`Invalid numeric value "${value}" for "${field}"`, line, column);
3668
4605
  }
3669
4606
  return parsed;
3670
4607
  }
@@ -3676,28 +4613,43 @@ var WorldOrbit = (() => {
3676
4613
  groupIds: []
3677
4614
  };
3678
4615
  }
3679
- function parseInlineFields2(tokens, line) {
4616
+ function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
3680
4617
  const fields = [];
3681
4618
  let index = 0;
3682
4619
  while (index < tokens.length) {
3683
4620
  const keyToken = tokens[index];
3684
- const schema = getFieldSchema(keyToken.value);
3685
- if (!schema) {
4621
+ const spec = getDraftObjectFieldSpec(keyToken.value);
4622
+ if (!spec) {
3686
4623
  throw new WorldOrbitError(`Unknown field "${keyToken.value}"`, line, keyToken.column);
3687
4624
  }
4625
+ if (spec.version === "2.1") {
4626
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, keyToken.value, {
4627
+ line,
4628
+ column: keyToken.column
4629
+ });
4630
+ }
3688
4631
  index++;
3689
4632
  const valueTokens = [];
3690
- if (schema.arity === "multiple") {
3691
- while (index < tokens.length && !isKnownFieldKey(tokens[index].value)) {
3692
- valueTokens.push(tokens[index]);
3693
- index++;
3694
- }
3695
- } else {
4633
+ if (spec.inlineMode === "single") {
3696
4634
  const nextToken = tokens[index];
3697
4635
  if (nextToken) {
3698
4636
  valueTokens.push(nextToken);
3699
4637
  index++;
3700
4638
  }
4639
+ } else if (spec.inlineMode === "pair") {
4640
+ for (let count = 0; count < 2; count++) {
4641
+ const nextToken = tokens[index];
4642
+ if (!nextToken) {
4643
+ break;
4644
+ }
4645
+ valueTokens.push(nextToken);
4646
+ index++;
4647
+ }
4648
+ } else {
4649
+ while (index < tokens.length && !DRAFT_OBJECT_FIELD_KEYS.has(tokens[index].value)) {
4650
+ valueTokens.push(tokens[index]);
4651
+ index++;
4652
+ }
3701
4653
  }
3702
4654
  if (valueTokens.length === 0) {
3703
4655
  throw new WorldOrbitError(`Missing value for field "${keyToken.value}"`, line, keyToken.column);
@@ -3709,25 +4661,35 @@ var WorldOrbit = (() => {
3709
4661
  location: { line, column: keyToken.column }
3710
4662
  });
3711
4663
  }
4664
+ validateDraftObjectFieldCompatibility(fields, objectType);
3712
4665
  return fields;
3713
4666
  }
3714
- function parseField2(tokens, line) {
4667
+ function parseObjectField(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
3715
4668
  if (tokens.length < 2) {
3716
4669
  throw new WorldOrbitError("Invalid field line", line, tokens[0]?.column ?? 1);
3717
4670
  }
3718
- if (!getFieldSchema(tokens[0].value)) {
4671
+ const spec = getDraftObjectFieldSpec(tokens[0].value);
4672
+ if (!spec) {
3719
4673
  throw new WorldOrbitError(`Unknown field "${tokens[0].value}"`, line, tokens[0].column);
3720
4674
  }
3721
- return {
4675
+ if (spec.version === "2.1") {
4676
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, tokens[0].value, {
4677
+ line,
4678
+ column: tokens[0].column
4679
+ });
4680
+ }
4681
+ const field = {
3722
4682
  type: "field",
3723
4683
  key: tokens[0].value,
3724
4684
  values: tokens.slice(1).map((token) => token.value),
3725
4685
  location: { line, column: tokens[0].column }
3726
4686
  };
4687
+ validateDraftObjectFieldCompatibility([field], objectType);
4688
+ return field;
3727
4689
  }
3728
- function parseInfoEntry2(tokens, line) {
4690
+ function parseInfoLikeEntry(tokens, line, errorMessage) {
3729
4691
  if (tokens.length < 2) {
3730
- throw new WorldOrbitError("Invalid info entry", line, tokens[0]?.column ?? 1);
4692
+ throw new WorldOrbitError(errorMessage, line, tokens[0]?.column ?? 1);
3731
4693
  }
3732
4694
  return {
3733
4695
  type: "info-entry",
@@ -3736,23 +4698,356 @@ var WorldOrbit = (() => {
3736
4698
  location: { line, column: tokens[0].column }
3737
4699
  };
3738
4700
  }
3739
- function normalizeIdentifier2(value) {
3740
- return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
4701
+ function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
4702
+ const fieldMap = collectDraftFields(node.fields);
4703
+ const placement = extractDraftPlacement(node.objectType, fieldMap);
4704
+ const properties = normalizeDraftProperties(node.objectType, fieldMap);
4705
+ const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
4706
+ const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
4707
+ const referencePlane = parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0]);
4708
+ const tidalLock = fieldMap.has("tidalLock") ? parseAtlasBoolean(singleFieldValue2(fieldMap.get("tidalLock")[0]), "tidalLock", fieldMap.get("tidalLock")[0].location) : void 0;
4709
+ const resonance = fieldMap.has("resonance") ? parseResonanceField(fieldMap.get("resonance")[0]) : void 0;
4710
+ const renderHints = extractRenderHints(fieldMap);
4711
+ const deriveRules = fieldMap.get("derive")?.map((field) => parseDeriveField(field));
4712
+ const validationRules = fieldMap.get("validate")?.map((field) => ({
4713
+ rule: singleFieldValue2(field)
4714
+ }));
4715
+ const lockedFields = fieldMap.has("locked") ? [...new Set(fieldMap.get("locked").flatMap((field) => field.values))] : void 0;
4716
+ const tolerances = fieldMap.get("tolerance")?.map((field) => parseToleranceField(field));
4717
+ const typedBlocks = normalizeTypedBlocks(node.typedBlockEntries);
4718
+ const info2 = normalizeInfoEntries(node.infoEntries, "info");
4719
+ const object = {
4720
+ type: node.objectType,
4721
+ id: node.id,
4722
+ properties,
4723
+ placement,
4724
+ info: info2
4725
+ };
4726
+ if (groups.length > 0)
4727
+ object.groups = groups;
4728
+ if (epoch)
4729
+ object.epoch = epoch;
4730
+ if (referencePlane)
4731
+ object.referencePlane = referencePlane;
4732
+ if (tidalLock !== void 0)
4733
+ object.tidalLock = tidalLock;
4734
+ if (resonance)
4735
+ object.resonance = resonance;
4736
+ if (renderHints)
4737
+ object.renderHints = renderHints;
4738
+ if (deriveRules?.length)
4739
+ object.deriveRules = deriveRules;
4740
+ if (validationRules?.length)
4741
+ object.validationRules = validationRules;
4742
+ if (lockedFields?.length)
4743
+ object.lockedFields = lockedFields;
4744
+ if (tolerances?.length)
4745
+ object.tolerances = tolerances;
4746
+ if (typedBlocks && Object.keys(typedBlocks).length > 0)
4747
+ object.typedBlocks = typedBlocks;
4748
+ if (sourceSchemaVersion !== "2.1") {
4749
+ 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) {
4750
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
4751
+ }
4752
+ }
4753
+ return object;
4754
+ }
4755
+ function collectDraftFields(fields) {
4756
+ const grouped = /* @__PURE__ */ new Map();
4757
+ for (const field of fields) {
4758
+ const spec = getDraftObjectFieldSpec(field.key);
4759
+ if (!spec) {
4760
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
4761
+ }
4762
+ if (!spec.allowRepeat && grouped.has(field.key)) {
4763
+ throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
4764
+ }
4765
+ const existing = grouped.get(field.key) ?? [];
4766
+ existing.push(field);
4767
+ grouped.set(field.key, existing);
4768
+ }
4769
+ return grouped;
3741
4770
  }
3742
- function humanizeIdentifier3(value) {
3743
- return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
4771
+ function extractDraftPlacement(objectType, fieldMap) {
4772
+ const orbitField = fieldMap.get("orbit")?.[0];
4773
+ const atField = fieldMap.get("at")?.[0];
4774
+ const surfaceField = fieldMap.get("surface")?.[0];
4775
+ const freeField = fieldMap.get("free")?.[0];
4776
+ const count = [orbitField, atField, surfaceField, freeField].filter(Boolean).length;
4777
+ if (count > 1) {
4778
+ const conflictingField = orbitField ?? atField ?? surfaceField ?? freeField;
4779
+ throw WorldOrbitError.fromLocation("Object has multiple placement modes", conflictingField?.location);
4780
+ }
4781
+ if (orbitField) {
4782
+ return {
4783
+ mode: "orbit",
4784
+ target: singleFieldValue2(orbitField),
4785
+ distance: parseOptionalUnitField(fieldMap.get("distance")?.[0], "distance"),
4786
+ semiMajor: parseOptionalUnitField(fieldMap.get("semiMajor")?.[0], "semiMajor"),
4787
+ eccentricity: parseOptionalNumberField(fieldMap.get("eccentricity")?.[0], "eccentricity"),
4788
+ period: parseOptionalUnitField(fieldMap.get("period")?.[0], "period"),
4789
+ angle: parseOptionalUnitField(fieldMap.get("angle")?.[0], "angle"),
4790
+ inclination: parseOptionalUnitField(fieldMap.get("inclination")?.[0], "inclination"),
4791
+ phase: parseOptionalUnitField(fieldMap.get("phase")?.[0], "phase")
4792
+ };
4793
+ }
4794
+ if (atField) {
4795
+ const target = singleFieldValue2(atField);
4796
+ return {
4797
+ mode: "at",
4798
+ target,
4799
+ reference: parseAtlasAtReference(target, atField.location)
4800
+ };
4801
+ }
4802
+ if (surfaceField) {
4803
+ return {
4804
+ mode: "surface",
4805
+ target: singleFieldValue2(surfaceField)
4806
+ };
4807
+ }
4808
+ if (freeField) {
4809
+ const raw = singleFieldValue2(freeField);
4810
+ const distance = tryParseAtlasUnitValue(raw);
4811
+ return {
4812
+ mode: "free",
4813
+ distance: distance ?? void 0,
4814
+ descriptor: distance ? void 0 : raw
4815
+ };
4816
+ }
4817
+ return null;
4818
+ }
4819
+ function normalizeDraftProperties(objectType, fieldMap) {
4820
+ const properties = {};
4821
+ for (const [key, fields] of fieldMap.entries()) {
4822
+ const field = fields[0];
4823
+ const spec = getDraftObjectFieldSpec(key);
4824
+ if (!field || !spec?.legacySchema || spec.legacySchema.placement) {
4825
+ continue;
4826
+ }
4827
+ ensureAtlasFieldSupported(key, objectType, field.location);
4828
+ properties[key] = normalizeLegacyScalarValue(key, field.values, field.location);
4829
+ }
4830
+ return properties;
4831
+ }
4832
+ function normalizeInfoEntries(entries, label) {
4833
+ const normalized = {};
4834
+ for (const entry of entries) {
4835
+ if (entry.key in normalized) {
4836
+ throw WorldOrbitError.fromLocation(`Duplicate ${label} key "${entry.key}"`, entry.location);
4837
+ }
4838
+ normalized[entry.key] = entry.value;
4839
+ }
4840
+ return normalized;
4841
+ }
4842
+ function normalizeTypedBlocks(typedBlockEntries) {
4843
+ const typedBlocks = {};
4844
+ for (const blockName of Object.keys(typedBlockEntries)) {
4845
+ const entries = typedBlockEntries[blockName];
4846
+ if (entries?.length) {
4847
+ typedBlocks[blockName] = normalizeInfoEntries(entries, blockName);
4848
+ }
4849
+ }
4850
+ return typedBlocks;
4851
+ }
4852
+ function extractRenderHints(fieldMap) {
4853
+ const renderHints = {};
4854
+ const renderLabelField = fieldMap.get("renderLabel")?.[0];
4855
+ const renderOrbitField = fieldMap.get("renderOrbit")?.[0];
4856
+ const renderPriorityField = fieldMap.get("renderPriority")?.[0];
4857
+ if (renderLabelField) {
4858
+ renderHints.renderLabel = parseAtlasBoolean(singleFieldValue2(renderLabelField), "renderLabel", renderLabelField.location);
4859
+ }
4860
+ if (renderOrbitField) {
4861
+ renderHints.renderOrbit = parseAtlasBoolean(singleFieldValue2(renderOrbitField), "renderOrbit", renderOrbitField.location);
4862
+ }
4863
+ if (renderPriorityField) {
4864
+ renderHints.renderPriority = parseAtlasNumber(singleFieldValue2(renderPriorityField), "renderPriority", renderPriorityField.location);
4865
+ }
4866
+ return Object.keys(renderHints).length > 0 ? renderHints : void 0;
4867
+ }
4868
+ function parseResonanceField(field) {
4869
+ if (field.values.length !== 2) {
4870
+ throw WorldOrbitError.fromLocation('Field "resonance" expects "<targetObjectId> <ratio>"', field.location);
4871
+ }
4872
+ const ratio = field.values[1];
4873
+ if (!/^\d+:\d+$/.test(ratio)) {
4874
+ throw WorldOrbitError.fromLocation(`Invalid resonance ratio "${ratio}"`, field.location);
4875
+ }
4876
+ return {
4877
+ targetObjectId: field.values[0],
4878
+ ratio
4879
+ };
4880
+ }
4881
+ function parseDeriveField(field) {
4882
+ if (field.values.length !== 2) {
4883
+ throw WorldOrbitError.fromLocation('Field "derive" expects "<field> <strategy>"', field.location);
4884
+ }
4885
+ return {
4886
+ field: field.values[0],
4887
+ strategy: field.values[1]
4888
+ };
4889
+ }
4890
+ function parseToleranceField(field) {
4891
+ if (field.values.length !== 2) {
4892
+ throw WorldOrbitError.fromLocation('Field "tolerance" expects "<field> <value>"', field.location);
4893
+ }
4894
+ const rawValue = field.values[1];
4895
+ const unitValue = tryParseAtlasUnitValue(rawValue);
4896
+ const numericValue2 = Number(rawValue);
4897
+ return {
4898
+ field: field.values[0],
4899
+ value: unitValue ?? (Number.isFinite(numericValue2) ? numericValue2 : rawValue)
4900
+ };
4901
+ }
4902
+ function parseOptionalTokenList(field) {
4903
+ return field ? [...new Set(field.values)] : [];
4904
+ }
4905
+ function parseOptionalJoinedValue(field) {
4906
+ if (!field) {
4907
+ return null;
4908
+ }
4909
+ return field.values.join(" ").trim() || null;
4910
+ }
4911
+ function parseOptionalUnitField(field, key) {
4912
+ return field ? parseAtlasUnitValue(singleFieldValue2(field), field.location, key) : void 0;
4913
+ }
4914
+ function parseOptionalNumberField(field, key) {
4915
+ return field ? parseAtlasNumber(singleFieldValue2(field), key, field.location) : void 0;
4916
+ }
4917
+ function singleFieldValue2(field) {
4918
+ return singleAtlasValue(field.values, field.key, field.location);
4919
+ }
4920
+ function getDraftObjectFieldSpec(key) {
4921
+ return DRAFT_OBJECT_FIELD_SPECS.get(key);
4922
+ }
4923
+ function validateDraftObjectFieldCompatibility(fields, objectType) {
4924
+ for (const field of fields) {
4925
+ const spec = getDraftObjectFieldSpec(field.key);
4926
+ if (!spec) {
4927
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
4928
+ }
4929
+ if (spec.legacySchema) {
4930
+ ensureAtlasFieldSupported(field.key, objectType, field.location);
4931
+ continue;
4932
+ }
4933
+ if ((field.key === "renderLabel" || field.key === "renderOrbit" || field.key === "tidalLock") && field.values.length !== 1) {
4934
+ throw WorldOrbitError.fromLocation(`Field "${field.key}" expects exactly one value`, field.location);
4935
+ }
4936
+ }
4937
+ }
4938
+ function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
4939
+ if (sourceSchemaVersion === "2.1") {
4940
+ return;
4941
+ }
4942
+ diagnostics.push({
4943
+ code: "parse.schema21.featureCompatibility",
4944
+ severity: "warning",
4945
+ source: "parse",
4946
+ message: `Feature "${featureName}" requires schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
4947
+ line: location.line,
4948
+ column: location.column
4949
+ });
4950
+ }
4951
+ function preprocessAtlasSource(source) {
4952
+ const chars = [...source];
4953
+ const comments = [];
4954
+ let inString = false;
4955
+ let inBlockComment = false;
4956
+ let blockCommentStart = null;
4957
+ let line = 1;
4958
+ let column = 1;
4959
+ for (let index = 0; index < chars.length; index++) {
4960
+ const ch = chars[index];
4961
+ const next = chars[index + 1];
4962
+ if (inBlockComment) {
4963
+ if (ch === "*" && next === "/") {
4964
+ chars[index] = " ";
4965
+ chars[index + 1] = " ";
4966
+ inBlockComment = false;
4967
+ blockCommentStart = null;
4968
+ index++;
4969
+ column += 2;
4970
+ continue;
4971
+ }
4972
+ if (ch !== "\n" && ch !== "\r") {
4973
+ chars[index] = " ";
4974
+ }
4975
+ if (ch === "\n") {
4976
+ line++;
4977
+ column = 1;
4978
+ } else {
4979
+ column++;
4980
+ }
4981
+ continue;
4982
+ }
4983
+ if (!inString && ch === "/" && next === "*") {
4984
+ comments.push({ kind: "block", line, column });
4985
+ chars[index] = " ";
4986
+ chars[index + 1] = " ";
4987
+ inBlockComment = true;
4988
+ blockCommentStart = { line, column };
4989
+ index++;
4990
+ column += 2;
4991
+ continue;
4992
+ }
4993
+ if (!inString && ch === "#" && !isHexColorLiteral(chars, index)) {
4994
+ comments.push({ kind: "line", line, column });
4995
+ chars[index] = " ";
4996
+ let inner = index + 1;
4997
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
4998
+ chars[inner] = " ";
4999
+ inner++;
5000
+ }
5001
+ column += inner - index;
5002
+ index = inner - 1;
5003
+ continue;
5004
+ }
5005
+ if (ch === '"' && chars[index - 1] !== "\\") {
5006
+ inString = !inString;
5007
+ }
5008
+ if (ch === "\n") {
5009
+ line++;
5010
+ column = 1;
5011
+ } else {
5012
+ column++;
5013
+ }
5014
+ }
5015
+ if (inBlockComment) {
5016
+ throw WorldOrbitError.fromLocation("Unclosed block comment", blockCommentStart ?? void 0);
5017
+ }
5018
+ return {
5019
+ source: chars.join(""),
5020
+ comments
5021
+ };
5022
+ }
5023
+ function isHexColorLiteral(chars, start) {
5024
+ let index = start + 1;
5025
+ let length = 0;
5026
+ while (index < chars.length && /[0-9a-f]/i.test(chars[index] ?? "")) {
5027
+ index++;
5028
+ length++;
5029
+ }
5030
+ if (![3, 4, 6, 8].includes(length)) {
5031
+ return false;
5032
+ }
5033
+ const next = chars[index];
5034
+ return next === void 0 || next === " " || next === " " || next === "\r" || next === "\n";
3744
5035
  }
3745
5036
 
3746
5037
  // packages/core/dist/atlas-edit.js
3747
- function createEmptyAtlasDocument(systemId = "WorldOrbit") {
5038
+ function createEmptyAtlasDocument(systemId = "WorldOrbit", version = "2.0") {
3748
5039
  return {
3749
5040
  format: "worldorbit",
3750
- version: "2.0",
5041
+ version,
5042
+ schemaVersion: version,
3751
5043
  sourceVersion: "1.0",
3752
5044
  system: {
3753
5045
  type: "system",
3754
5046
  id: systemId,
3755
5047
  title: systemId,
5048
+ description: null,
5049
+ epoch: null,
5050
+ referencePlane: null,
3756
5051
  defaults: {
3757
5052
  view: "topdown",
3758
5053
  scale: null,
@@ -3764,6 +5059,8 @@ var WorldOrbit = (() => {
3764
5059
  viewpoints: [],
3765
5060
  annotations: []
3766
5061
  },
5062
+ groups: [],
5063
+ relations: [],
3767
5064
  objects: [],
3768
5065
  diagnostics: []
3769
5066
  };
@@ -3777,14 +5074,20 @@ var WorldOrbit = (() => {
3777
5074
  for (const key of Object.keys(document2.system.atlasMetadata).sort()) {
3778
5075
  paths.push({ kind: "metadata", key });
3779
5076
  }
3780
- for (const viewpoint of [...document2.system.viewpoints].sort(compareIdLike)) {
5077
+ for (const viewpoint of [...document2.system.viewpoints].sort(compareIdLike2)) {
3781
5078
  paths.push({ kind: "viewpoint", id: viewpoint.id });
3782
5079
  }
3783
- for (const annotation of [...document2.system.annotations].sort(compareIdLike)) {
5080
+ for (const annotation of [...document2.system.annotations].sort(compareIdLike2)) {
3784
5081
  paths.push({ kind: "annotation", id: annotation.id });
3785
5082
  }
3786
5083
  }
3787
- for (const object of [...document2.objects].sort(compareIdLike)) {
5084
+ for (const group of [...document2.groups].sort(compareIdLike2)) {
5085
+ paths.push({ kind: "group", id: group.id });
5086
+ }
5087
+ for (const relation of [...document2.relations].sort(compareIdLike2)) {
5088
+ paths.push({ kind: "relation", id: relation.id });
5089
+ }
5090
+ for (const object of [...document2.objects].sort(compareIdLike2)) {
3788
5091
  paths.push({ kind: "object", id: object.id });
3789
5092
  }
3790
5093
  return paths;
@@ -3797,12 +5100,16 @@ var WorldOrbit = (() => {
3797
5100
  return document2.system?.defaults ?? null;
3798
5101
  case "metadata":
3799
5102
  return path.key ? document2.system?.atlasMetadata[path.key] ?? null : null;
5103
+ case "group":
5104
+ return path.id ? findGroup(document2, path.id) : null;
3800
5105
  case "object":
3801
5106
  return path.id ? findObject(document2, path.id) : null;
3802
5107
  case "viewpoint":
3803
5108
  return path.id ? findViewpoint(document2.system, path.id) : null;
3804
5109
  case "annotation":
3805
5110
  return path.id ? findAnnotation(document2.system, path.id) : null;
5111
+ case "relation":
5112
+ return path.id ? findRelation(document2, path.id) : null;
3806
5113
  }
3807
5114
  }
3808
5115
  function upsertAtlasDocumentNode(document2, path, value) {
@@ -3828,6 +5135,12 @@ var WorldOrbit = (() => {
3828
5135
  system.atlasMetadata[path.key] = String(value);
3829
5136
  }
3830
5137
  return next;
5138
+ case "group":
5139
+ if (!path.id) {
5140
+ throw new Error('Group updates require an "id" value.');
5141
+ }
5142
+ upsertById(next.groups, value);
5143
+ return next;
3831
5144
  case "object":
3832
5145
  if (!path.id) {
3833
5146
  throw new Error('Object updates require an "id" value.');
@@ -3846,6 +5159,12 @@ var WorldOrbit = (() => {
3846
5159
  }
3847
5160
  upsertById(system.annotations, value);
3848
5161
  return next;
5162
+ case "relation":
5163
+ if (!path.id) {
5164
+ throw new Error('Relation updates require an "id" value.');
5165
+ }
5166
+ upsertById(next.relations, value);
5167
+ return next;
3849
5168
  }
3850
5169
  }
3851
5170
  function updateAtlasDocumentNode(document2, path, updater) {
@@ -3865,6 +5184,11 @@ var WorldOrbit = (() => {
3865
5184
  next.objects = next.objects.filter((object) => object.id !== path.id);
3866
5185
  }
3867
5186
  return next;
5187
+ case "group":
5188
+ if (path.id) {
5189
+ next.groups = next.groups.filter((group) => group.id !== path.id);
5190
+ }
5191
+ return next;
3868
5192
  case "viewpoint":
3869
5193
  if (path.id) {
3870
5194
  system.viewpoints = system.viewpoints.filter((viewpoint) => viewpoint.id !== path.id);
@@ -3875,6 +5199,11 @@ var WorldOrbit = (() => {
3875
5199
  system.annotations = system.annotations.filter((annotation) => annotation.id !== path.id);
3876
5200
  }
3877
5201
  return next;
5202
+ case "relation":
5203
+ if (path.id) {
5204
+ next.relations = next.relations.filter((relation) => relation.id !== path.id);
5205
+ }
5206
+ return next;
3878
5207
  default:
3879
5208
  return next;
3880
5209
  }
@@ -3892,6 +5221,15 @@ var WorldOrbit = (() => {
3892
5221
  id: diagnostic.objectId
3893
5222
  };
3894
5223
  }
5224
+ if (diagnostic.field?.startsWith("group.")) {
5225
+ const parts = diagnostic.field.split(".");
5226
+ if (parts[1] && findGroup(document2, parts[1])) {
5227
+ return {
5228
+ kind: "group",
5229
+ id: parts[1]
5230
+ };
5231
+ }
5232
+ }
3895
5233
  if (diagnostic.field?.startsWith("viewpoint.")) {
3896
5234
  const parts = diagnostic.field.split(".");
3897
5235
  if (parts[1] && findViewpoint(document2.system, parts[1])) {
@@ -3910,6 +5248,15 @@ var WorldOrbit = (() => {
3910
5248
  };
3911
5249
  }
3912
5250
  }
5251
+ if (diagnostic.field?.startsWith("relation.")) {
5252
+ const parts = diagnostic.field.split(".");
5253
+ if (parts[1] && findRelation(document2, parts[1])) {
5254
+ return {
5255
+ kind: "relation",
5256
+ id: parts[1]
5257
+ };
5258
+ }
5259
+ }
3913
5260
  if (diagnostic.field && diagnostic.field in ensureSystem(document2).atlasMetadata) {
3914
5261
  return {
3915
5262
  kind: "metadata",
@@ -3919,9 +5266,11 @@ var WorldOrbit = (() => {
3919
5266
  return null;
3920
5267
  }
3921
5268
  function validateAtlasDocumentWithDiagnostics(document2) {
3922
- const materialized = materializeAtlasDocument(document2);
3923
- const result = validateDocumentWithDiagnostics(materialized);
3924
- return resolveAtlasDiagnostics(document2, result.diagnostics);
5269
+ const diagnostics = [
5270
+ ...document2.diagnostics,
5271
+ ...collectAtlasDiagnostics(document2, document2.version)
5272
+ ];
5273
+ return resolveAtlasDiagnostics(document2, diagnostics);
3925
5274
  }
3926
5275
  function ensureSystem(document2) {
3927
5276
  if (document2.system) {
@@ -3933,6 +5282,12 @@ var WorldOrbit = (() => {
3933
5282
  function findObject(document2, objectId) {
3934
5283
  return document2.objects.find((object) => object.id === objectId) ?? null;
3935
5284
  }
5285
+ function findGroup(document2, groupId) {
5286
+ return document2.groups.find((group) => group.id === groupId) ?? null;
5287
+ }
5288
+ function findRelation(document2, relationId) {
5289
+ return document2.relations.find((relation) => relation.id === relationId) ?? null;
5290
+ }
3936
5291
  function findViewpoint(system, viewpointId) {
3937
5292
  return system?.viewpoints.find((viewpoint) => viewpoint.id === viewpointId) ?? null;
3938
5293
  }
@@ -3943,20 +5298,21 @@ var WorldOrbit = (() => {
3943
5298
  const index = items.findIndex((item) => item.id === value.id);
3944
5299
  if (index === -1) {
3945
5300
  items.push(value);
3946
- items.sort(compareIdLike);
5301
+ items.sort(compareIdLike2);
3947
5302
  return;
3948
5303
  }
3949
5304
  items[index] = value;
3950
5305
  }
3951
- function compareIdLike(left, right) {
5306
+ function compareIdLike2(left, right) {
3952
5307
  return left.id.localeCompare(right.id);
3953
5308
  }
3954
5309
 
3955
5310
  // packages/core/dist/load.js
3956
- var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0)?$/i;
5311
+ var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1)?$/i;
5312
+ var ATLAS_SCHEMA_21_PATTERN = /^schema\s+2\.1$/i;
3957
5313
  var LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
3958
5314
  function detectWorldOrbitSchemaVersion(source) {
3959
- for (const line of source.split(/\r?\n/)) {
5315
+ for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
3960
5316
  const trimmed = line.trim();
3961
5317
  if (!trimmed) {
3962
5318
  continue;
@@ -3964,6 +5320,9 @@ var WorldOrbit = (() => {
3964
5320
  if (LEGACY_DRAFT_SCHEMA_PATTERN.test(trimmed)) {
3965
5321
  return "2.0-draft";
3966
5322
  }
5323
+ if (ATLAS_SCHEMA_21_PATTERN.test(trimmed)) {
5324
+ return "2.1";
5325
+ }
3967
5326
  if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
3968
5327
  return "2.0";
3969
5328
  }
@@ -3971,6 +5330,49 @@ var WorldOrbit = (() => {
3971
5330
  }
3972
5331
  return "1.0";
3973
5332
  }
5333
+ function stripCommentsForSchemaDetection(source) {
5334
+ const chars = [...source];
5335
+ let inString = false;
5336
+ let inBlockComment = false;
5337
+ for (let index = 0; index < chars.length; index++) {
5338
+ const ch = chars[index];
5339
+ const next = chars[index + 1];
5340
+ if (inBlockComment) {
5341
+ if (ch === "*" && next === "/") {
5342
+ chars[index] = " ";
5343
+ chars[index + 1] = " ";
5344
+ inBlockComment = false;
5345
+ index++;
5346
+ continue;
5347
+ }
5348
+ if (ch !== "\n" && ch !== "\r") {
5349
+ chars[index] = " ";
5350
+ }
5351
+ continue;
5352
+ }
5353
+ if (!inString && ch === "/" && next === "*") {
5354
+ chars[index] = " ";
5355
+ chars[index + 1] = " ";
5356
+ inBlockComment = true;
5357
+ index++;
5358
+ continue;
5359
+ }
5360
+ if (!inString && ch === "#") {
5361
+ chars[index] = " ";
5362
+ let inner = index + 1;
5363
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
5364
+ chars[inner] = " ";
5365
+ inner++;
5366
+ }
5367
+ index = inner - 1;
5368
+ continue;
5369
+ }
5370
+ if (ch === '"' && chars[index - 1] !== "\\") {
5371
+ inString = !inString;
5372
+ }
5373
+ }
5374
+ return chars.join("");
5375
+ }
3974
5376
  function loadWorldOrbitSource(source) {
3975
5377
  const result = loadWorldOrbitSourceWithDiagnostics(source);
3976
5378
  if (!result.ok || !result.value) {
@@ -3981,36 +5383,36 @@ var WorldOrbit = (() => {
3981
5383
  }
3982
5384
  function loadWorldOrbitSourceWithDiagnostics(source) {
3983
5385
  const schemaVersion = detectWorldOrbitSchemaVersion(source);
3984
- if (schemaVersion === "2.0" || schemaVersion === "2.0-draft") {
5386
+ if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1") {
3985
5387
  return loadAtlasSourceWithDiagnostics(source, schemaVersion);
3986
5388
  }
3987
5389
  let ast;
3988
5390
  try {
3989
5391
  ast = parseWorldOrbit(source);
3990
- } catch (error) {
5392
+ } catch (error2) {
3991
5393
  return {
3992
5394
  ok: false,
3993
5395
  value: null,
3994
- diagnostics: [diagnosticFromError(error, "parse")]
5396
+ diagnostics: [diagnosticFromError(error2, "parse")]
3995
5397
  };
3996
5398
  }
3997
5399
  let document2;
3998
5400
  try {
3999
5401
  document2 = normalizeDocument(ast);
4000
- } catch (error) {
5402
+ } catch (error2) {
4001
5403
  return {
4002
5404
  ok: false,
4003
5405
  value: null,
4004
- diagnostics: [diagnosticFromError(error, "normalize")]
5406
+ diagnostics: [diagnosticFromError(error2, "normalize")]
4005
5407
  };
4006
5408
  }
4007
5409
  try {
4008
5410
  validateDocument(document2);
4009
- } catch (error) {
5411
+ } catch (error2) {
4010
5412
  return {
4011
5413
  ok: false,
4012
5414
  value: null,
4013
- diagnostics: [diagnosticFromError(error, "validate")]
5415
+ diagnostics: [diagnosticFromError(error2, "validate")]
4014
5416
  };
4015
5417
  }
4016
5418
  return {
@@ -4030,30 +5432,29 @@ var WorldOrbit = (() => {
4030
5432
  let atlasDocument;
4031
5433
  try {
4032
5434
  atlasDocument = parseWorldOrbitAtlas(source);
4033
- } catch (error) {
5435
+ } catch (error2) {
4034
5436
  return {
4035
5437
  ok: false,
4036
5438
  value: null,
4037
- diagnostics: [diagnosticFromError(error, "parse", "load.atlas.failed")]
5439
+ diagnostics: [diagnosticFromError(error2, "parse", "load.atlas.failed")]
4038
5440
  };
4039
5441
  }
4040
- let document2;
4041
- try {
4042
- document2 = materializeAtlasDocument(atlasDocument);
4043
- } catch (error) {
5442
+ const atlasDiagnostics = [...atlasDocument.diagnostics];
5443
+ if (atlasDiagnostics.some((diagnostic) => diagnostic.severity === "error")) {
4044
5444
  return {
4045
5445
  ok: false,
4046
5446
  value: null,
4047
- diagnostics: [diagnosticFromError(error, "normalize", "load.atlas.materialize.failed")]
5447
+ diagnostics: atlasDiagnostics
4048
5448
  };
4049
5449
  }
5450
+ let document2;
4050
5451
  try {
4051
- validateDocument(document2);
4052
- } catch (error) {
5452
+ document2 = materializeAtlasDocument(atlasDocument);
5453
+ } catch (error2) {
4053
5454
  return {
4054
5455
  ok: false,
4055
5456
  value: null,
4056
- diagnostics: [diagnosticFromError(error, "validate", "load.atlas.validate.failed")]
5457
+ diagnostics: [diagnosticFromError(error2, "normalize", "load.atlas.materialize.failed")]
4057
5458
  };
4058
5459
  }
4059
5460
  const loaded = {
@@ -4062,12 +5463,12 @@ var WorldOrbit = (() => {
4062
5463
  document: document2,
4063
5464
  atlasDocument,
4064
5465
  draftDocument: atlasDocument,
4065
- diagnostics: [...atlasDocument.diagnostics]
5466
+ diagnostics: atlasDiagnostics
4066
5467
  };
4067
5468
  return {
4068
5469
  ok: true,
4069
5470
  value: loaded,
4070
- diagnostics: [...atlasDocument.diagnostics]
5471
+ diagnostics: atlasDiagnostics
4071
5472
  };
4072
5473
  }
4073
5474
 
@@ -4138,6 +5539,7 @@ var WorldOrbit = (() => {
4138
5539
  var DEFAULT_LAYERS = {
4139
5540
  background: true,
4140
5541
  guides: true,
5542
+ relations: true,
4141
5543
  orbits: true,
4142
5544
  objects: true,
4143
5545
  labels: true,
@@ -4152,6 +5554,7 @@ var WorldOrbit = (() => {
4152
5554
  backgroundGlow: "rgba(240, 180, 100, 0.18)",
4153
5555
  panel: "rgba(7, 17, 27, 0.9)",
4154
5556
  panelLine: "rgba(168, 207, 242, 0.18)",
5557
+ relation: "rgba(240, 180, 100, 0.42)",
4155
5558
  orbit: "rgba(163, 209, 255, 0.24)",
4156
5559
  orbitBand: "rgba(255, 190, 120, 0.28)",
4157
5560
  guide: "rgba(255, 255, 255, 0.04)",
@@ -4174,6 +5577,7 @@ var WorldOrbit = (() => {
4174
5577
  backgroundGlow: "rgba(120, 255, 215, 0.16)",
4175
5578
  panel: "rgba(7, 20, 30, 0.9)",
4176
5579
  panelLine: "rgba(120, 255, 215, 0.16)",
5580
+ relation: "rgba(156, 231, 255, 0.42)",
4177
5581
  orbit: "rgba(120, 255, 215, 0.2)",
4178
5582
  orbitBand: "rgba(137, 185, 255, 0.24)",
4179
5583
  guide: "rgba(255, 255, 255, 0.035)",
@@ -4196,6 +5600,7 @@ var WorldOrbit = (() => {
4196
5600
  backgroundGlow: "rgba(255, 127, 95, 0.18)",
4197
5601
  panel: "rgba(24, 9, 13, 0.9)",
4198
5602
  panelLine: "rgba(255, 166, 149, 0.16)",
5603
+ relation: "rgba(255, 178, 125, 0.42)",
4199
5604
  orbit: "rgba(255, 188, 164, 0.22)",
4200
5605
  orbitBand: "rgba(255, 214, 139, 0.24)",
4201
5606
  guide: "rgba(255, 255, 255, 0.03)",
@@ -4348,6 +5753,7 @@ var WorldOrbit = (() => {
4348
5753
  return {
4349
5754
  background: viewpoint.layers.background,
4350
5755
  guides: viewpoint.layers.guides,
5756
+ relations: viewpoint.layers.relations,
4351
5757
  orbits: viewpoint.layers["orbits-front"] === void 0 && viewpoint.layers["orbits-back"] === void 0 ? void 0 : viewpoint.layers["orbits-front"] !== false || viewpoint.layers["orbits-back"] !== false,
4352
5758
  objects: viewpoint.layers.objects,
4353
5759
  labels: viewpoint.layers.labels,
@@ -4385,7 +5791,11 @@ var WorldOrbit = (() => {
4385
5791
  return false;
4386
5792
  }
4387
5793
  if (filter.groupIds?.length && (!object.groupId || !filter.groupIds.includes(object.groupId))) {
4388
- return false;
5794
+ const hasSemanticMatch = object.semanticGroupIds.length > 0 && filter.groupIds.some((groupId) => object.semanticGroupIds.includes(groupId));
5795
+ const hasLegacyMatch = Boolean(object.groupId && filter.groupIds.includes(object.groupId));
5796
+ if (!hasSemanticMatch && !hasLegacyMatch) {
5797
+ return false;
5798
+ }
4389
5799
  }
4390
5800
  if (filter.tags?.length) {
4391
5801
  const objectTags = Array.isArray(object.object.properties.tags) ? object.object.properties.tags.filter((entry) => typeof entry === "string") : [];
@@ -4629,6 +6039,7 @@ var WorldOrbit = (() => {
4629
6039
  const imageDefinitions = buildImageDefinitions(visibleObjects);
4630
6040
  const orbitMarkup = layers.orbits ? renderOrbitLayer(scene, visibleObjectIds, layers.structures) : { back: "", front: "" };
4631
6041
  const leaderMarkup = layers.guides ? scene.leaders.filter((leader) => !leader.hidden).filter((leader) => visibleObjectIds.has(leader.objectId)).filter((leader) => layers.structures || !isStructureLike(leader.object)).map((leader) => `<line class="wo-leader wo-leader-${leader.mode}" x1="${leader.x1}" y1="${leader.y1}" x2="${leader.x2}" y2="${leader.y2}" data-render-id="${escapeXml(leader.renderId)}" data-group-id="${escapeAttribute(leader.groupId ?? "")}" />`).join("") : "";
6042
+ const relationMarkup = layers.relations ? scene.relations.filter((relation) => !relation.hidden).filter((relation) => visibleObjectIds.has(relation.fromObjectId) && visibleObjectIds.has(relation.toObjectId)).map((relation) => `<line class="wo-relation" x1="${relation.x1}" y1="${relation.y1}" x2="${relation.x2}" y2="${relation.y2}" data-render-id="${escapeXml(relation.renderId)}" data-relation-id="${escapeAttribute(relation.relationId)}" />`).join("") : "";
4632
6043
  const objectMarkup = layers.objects ? visibleObjects.map((object) => renderSceneObject(object, options.selectedObjectId ?? null, theme)).join("") : "";
4633
6044
  const labelMarkup = layers.labels ? visibleLabels.map((label) => renderSceneLabel(scene, label, options.selectedObjectId ?? null)).join("") : "";
4634
6045
  const metadataMarkup = layers.metadata ? `<text class="wo-title" x="56" y="64">${escapeXml(scene.title)}</text>
@@ -4663,6 +6074,7 @@ var WorldOrbit = (() => {
4663
6074
  .wo-orbit-back { opacity: 0.38; stroke-dasharray: 8 6; }
4664
6075
  .wo-orbit-front { opacity: 0.9; }
4665
6076
  .wo-orbit-band { stroke: ${theme.orbitBand}; stroke-linecap: round; }
6077
+ .wo-relation { stroke: ${theme.relation}; stroke-width: 2; stroke-dasharray: 10 6; }
4666
6078
  .wo-leader { stroke: ${theme.leader}; stroke-width: 1.5; stroke-dasharray: 6 5; }
4667
6079
  .wo-label { fill: ${theme.ink}; font-family: ${theme.fontFamily}; font-weight: 600; letter-spacing: 0.02em; }
4668
6080
  .wo-label-secondary { fill: ${theme.muted}; font-family: ${theme.fontFamily}; font-weight: 500; }
@@ -4696,6 +6108,7 @@ var WorldOrbit = (() => {
4696
6108
  <g data-worldorbit-world-content="true">
4697
6109
  ${layers.orbits ? `<g data-layer-id="orbits-back">${orbitMarkup.back}</g>` : ""}
4698
6110
  ${layers.guides ? `<g data-layer-id="guides">${leaderMarkup}</g>` : ""}
6111
+ ${layers.relations ? `<g data-layer-id="relations">${relationMarkup}</g>` : ""}
4699
6112
  ${layers.objects ? `<g data-layer-id="objects">${objectMarkup}</g>` : ""}
4700
6113
  ${layers.orbits ? `<g data-layer-id="orbits-front">${orbitMarkup.front}</g>` : ""}
4701
6114
  ${layers.labels ? `<g data-layer-id="labels">${labelMarkup}</g>` : ""}
@@ -5267,6 +6680,41 @@ var WorldOrbit = (() => {
5267
6680
  });
5268
6681
  }
5269
6682
  const placement = details.object.placement;
6683
+ if (details.object.groups?.length) {
6684
+ fields.set("groups", {
6685
+ key: "groups",
6686
+ label: "Groups",
6687
+ value: details.object.groups.join(", ")
6688
+ });
6689
+ }
6690
+ if (details.object.epoch) {
6691
+ fields.set("epoch", {
6692
+ key: "epoch",
6693
+ label: "Epoch",
6694
+ value: details.object.epoch
6695
+ });
6696
+ }
6697
+ if (details.object.referencePlane) {
6698
+ fields.set("referencePlane", {
6699
+ key: "referencePlane",
6700
+ label: "Reference Plane",
6701
+ value: details.object.referencePlane
6702
+ });
6703
+ }
6704
+ if (details.object.tidalLock !== void 0) {
6705
+ fields.set("tidalLock", {
6706
+ key: "tidalLock",
6707
+ label: "Tidal Lock",
6708
+ value: details.object.tidalLock ? "true" : "false"
6709
+ });
6710
+ }
6711
+ if (details.object.resonance) {
6712
+ fields.set("resonance", {
6713
+ key: "resonance",
6714
+ label: "Resonance",
6715
+ value: `${details.object.resonance.targetObjectId} ${details.object.resonance.ratio}`
6716
+ });
6717
+ }
5270
6718
  if (placement?.mode === "at") {
5271
6719
  fields.set("placement", {
5272
6720
  key: "placement",
@@ -5941,8 +7389,10 @@ var WorldOrbit = (() => {
5941
7389
  renderObject,
5942
7390
  label: scene.labels.find((label) => label.objectId === renderObject.objectId && !label.hidden) ?? null,
5943
7391
  group: scene.groups.find((group) => group.renderId === renderObject.groupId) ?? null,
7392
+ semanticGroups: scene.semanticGroups.filter((group) => renderObject.semanticGroupIds.includes(group.id)),
5944
7393
  orbit: scene.orbitVisuals.find((orbit) => orbit.objectId === renderObject.objectId && !orbit.hidden) ?? null,
5945
7394
  relatedOrbits: scene.orbitVisuals.filter((orbit) => !orbit.hidden && (orbit.objectId === renderObject.objectId || renderObject.ancestorIds.includes(orbit.objectId) || renderObject.childIds.includes(orbit.objectId))),
7395
+ relations: scene.relations.filter((relation) => !relation.hidden && (relation.fromObjectId === renderObject.objectId || relation.toObjectId === renderObject.objectId)),
5946
7396
  parent: getObjectById(renderObject.parentId),
5947
7397
  children: renderObject.childIds.map((childId) => getObjectById(childId)).filter(Boolean),
5948
7398
  ancestors: renderObject.ancestorIds.map((ancestorId) => getObjectById(ancestorId)).filter(Boolean),
@@ -6567,6 +8017,7 @@ var WorldOrbit = (() => {
6567
8017
  const controls = {
6568
8018
  search: options.controls?.search ?? true,
6569
8019
  typeFilter: options.controls?.typeFilter ?? true,
8020
+ groupFilter: options.controls?.groupFilter ?? true,
6570
8021
  viewpointSelect: options.controls?.viewpointSelect ?? true,
6571
8022
  inspector: options.controls?.inspector ?? true,
6572
8023
  bookmarks: options.controls?.bookmarks ?? true
@@ -6576,6 +8027,7 @@ var WorldOrbit = (() => {
6576
8027
  const toolbar = container.querySelector("[data-atlas-toolbar]");
6577
8028
  const searchInput = container.querySelector("[data-atlas-search]");
6578
8029
  const typeFilterSelect = container.querySelector("[data-atlas-type-filter]");
8030
+ const groupFilterSelect = container.querySelector("[data-atlas-group-filter]");
6579
8031
  const viewpointSelect = container.querySelector("[data-atlas-viewpoint]");
6580
8032
  const bookmarkButton = container.querySelector("[data-atlas-bookmark]");
6581
8033
  const bookmarkList = container.querySelector("[data-atlas-bookmarks]");
@@ -6588,6 +8040,7 @@ var WorldOrbit = (() => {
6588
8040
  const baseFilter = normalizeViewerFilter(options.initialFilter ?? null);
6589
8041
  let searchQuery = options.initialQuery?.trim() ?? baseFilter?.query ?? "";
6590
8042
  let objectTypeFilter = options.initialObjectType ?? (baseFilter?.objectTypes?.length === 1 ? baseFilter.objectTypes[0] : null);
8043
+ let groupFilter = baseFilter?.groupIds?.[0] ?? null;
6591
8044
  let bookmarks = [];
6592
8045
  let viewer;
6593
8046
  viewer = createInteractiveViewer(stage, {
@@ -6635,6 +8088,7 @@ var WorldOrbit = (() => {
6635
8088
  });
6636
8089
  applyCurrentFilter();
6637
8090
  populateViewpoints();
8091
+ populateGroups();
6638
8092
  syncControlsFromFilter(viewer.getFilter());
6639
8093
  renderBookmarks();
6640
8094
  updateSearchResults();
@@ -6647,6 +8101,10 @@ var WorldOrbit = (() => {
6647
8101
  objectTypeFilter = typeFilterSelect.value || null;
6648
8102
  applyCurrentFilter();
6649
8103
  });
8104
+ groupFilterSelect?.addEventListener("change", () => {
8105
+ groupFilter = groupFilterSelect.value || null;
8106
+ applyCurrentFilter();
8107
+ });
6650
8108
  viewpointSelect?.addEventListener("change", () => {
6651
8109
  const activeViewer = requireViewer();
6652
8110
  if (!viewpointSelect.value) {
@@ -6788,6 +8246,7 @@ var WorldOrbit = (() => {
6788
8246
  return api;
6789
8247
  function refreshAfterInputChange() {
6790
8248
  populateViewpoints();
8249
+ populateGroups();
6791
8250
  applyCurrentFilter();
6792
8251
  renderBookmarks();
6793
8252
  updateSearchResults();
@@ -6804,19 +8263,23 @@ var WorldOrbit = (() => {
6804
8263
  query: searchQuery || void 0,
6805
8264
  objectTypes: objectTypeFilter ? [objectTypeFilter] : void 0,
6806
8265
  tags: baseFilter?.tags,
6807
- groupIds: baseFilter?.groupIds,
8266
+ groupIds: groupFilter ? [groupFilter] : baseFilter?.groupIds,
6808
8267
  includeAncestors: baseFilter?.includeAncestors ?? true
6809
8268
  });
6810
8269
  }
6811
8270
  function syncControlsFromFilter(filter) {
6812
8271
  searchQuery = filter?.query?.trim() ?? "";
6813
8272
  objectTypeFilter = filter?.objectTypes?.length === 1 ? filter.objectTypes[0] : null;
8273
+ groupFilter = filter?.groupIds?.length === 1 ? filter.groupIds[0] : null;
6814
8274
  if (searchInput && document.activeElement !== searchInput) {
6815
8275
  searchInput.value = searchQuery;
6816
8276
  }
6817
8277
  if (typeFilterSelect) {
6818
8278
  typeFilterSelect.value = objectTypeFilter ?? "";
6819
8279
  }
8280
+ if (groupFilterSelect) {
8281
+ groupFilterSelect.value = groupFilter ?? "";
8282
+ }
6820
8283
  }
6821
8284
  function populateViewpoints() {
6822
8285
  if (!viewpointSelect) {
@@ -6830,6 +8293,17 @@ var WorldOrbit = (() => {
6830
8293
  ].join("");
6831
8294
  viewpointSelect.value = active;
6832
8295
  }
8296
+ function populateGroups() {
8297
+ if (!groupFilterSelect) {
8298
+ return;
8299
+ }
8300
+ const activeViewer = requireViewer();
8301
+ groupFilterSelect.innerHTML = [
8302
+ `<option value="">All groups</option>`,
8303
+ ...activeViewer.getScene().semanticGroups.map((group) => `<option value="${escapeHtml2(group.id)}">${escapeHtml2(group.label)}</option>`)
8304
+ ].join("");
8305
+ groupFilterSelect.value = groupFilter ?? "";
8306
+ }
6833
8307
  function syncViewpointControl() {
6834
8308
  if (!viewpointSelect) {
6835
8309
  return;
@@ -6863,6 +8337,8 @@ var WorldOrbit = (() => {
6863
8337
  projection: activeViewer.getScene().projection,
6864
8338
  renderPreset: activeViewer.getScene().renderPreset,
6865
8339
  groupCount: activeViewer.getScene().groups.length,
8340
+ semanticGroupCount: activeViewer.getScene().semanticGroups.length,
8341
+ relationCount: activeViewer.getScene().relations.length,
6866
8342
  viewpointCount: activeViewer.getScene().viewpoints.length
6867
8343
  }
6868
8344
  };
@@ -6895,6 +8371,12 @@ var WorldOrbit = (() => {
6895
8371
  <option value="phenomenon">Phenomenon</option>
6896
8372
  </select>
6897
8373
  </label>` : "",
8374
+ controls.groupFilter ? `<label class="wo-atlas-field">
8375
+ <span>Group</span>
8376
+ <select data-atlas-group-filter>
8377
+ <option value="">All groups</option>
8378
+ </select>
8379
+ </label>` : "",
6898
8380
  controls.viewpointSelect ? `<label class="wo-atlas-field">
6899
8381
  <span>Viewpoint</span>
6900
8382
  <select data-atlas-viewpoint>