worldorbit 2.5.15 → 2.5.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,8 +19,8 @@ var WorldOrbit = (() => {
19
19
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
20
 
21
21
  // packages/viewer/dist/index.js
22
- var dist_exports = {};
23
- __export(dist_exports, {
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
24
  DEFAULT_VIEWER_STATE: () => DEFAULT_VIEWER_STATE,
25
25
  WORLD_LAYER_ID: () => WORLD_LAYER_ID,
26
26
  clampScale: () => clampScale,
@@ -60,6 +60,7 @@ var WorldOrbit = (() => {
60
60
  var DEFAULT_LAYERS = {
61
61
  background: true,
62
62
  guides: true,
63
+ relations: true,
63
64
  orbits: true,
64
65
  objects: true,
65
66
  labels: true,
@@ -74,6 +75,7 @@ var WorldOrbit = (() => {
74
75
  backgroundGlow: "rgba(240, 180, 100, 0.18)",
75
76
  panel: "rgba(7, 17, 27, 0.9)",
76
77
  panelLine: "rgba(168, 207, 242, 0.18)",
78
+ relation: "rgba(240, 180, 100, 0.42)",
77
79
  orbit: "rgba(163, 209, 255, 0.24)",
78
80
  orbitBand: "rgba(255, 190, 120, 0.28)",
79
81
  guide: "rgba(255, 255, 255, 0.04)",
@@ -96,6 +98,7 @@ var WorldOrbit = (() => {
96
98
  backgroundGlow: "rgba(120, 255, 215, 0.16)",
97
99
  panel: "rgba(7, 20, 30, 0.9)",
98
100
  panelLine: "rgba(120, 255, 215, 0.16)",
101
+ relation: "rgba(156, 231, 255, 0.42)",
99
102
  orbit: "rgba(120, 255, 215, 0.2)",
100
103
  orbitBand: "rgba(137, 185, 255, 0.24)",
101
104
  guide: "rgba(255, 255, 255, 0.035)",
@@ -118,6 +121,7 @@ var WorldOrbit = (() => {
118
121
  backgroundGlow: "rgba(255, 127, 95, 0.18)",
119
122
  panel: "rgba(24, 9, 13, 0.9)",
120
123
  panelLine: "rgba(255, 166, 149, 0.16)",
124
+ relation: "rgba(255, 178, 125, 0.42)",
121
125
  orbit: "rgba(255, 188, 164, 0.22)",
122
126
  orbitBand: "rgba(255, 214, 139, 0.24)",
123
127
  guide: "rgba(255, 255, 255, 0.03)",
@@ -270,6 +274,7 @@ var WorldOrbit = (() => {
270
274
  return {
271
275
  background: viewpoint.layers.background,
272
276
  guides: viewpoint.layers.guides,
277
+ relations: viewpoint.layers.relations,
273
278
  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,
274
279
  objects: viewpoint.layers.objects,
275
280
  labels: viewpoint.layers.labels,
@@ -307,7 +312,11 @@ var WorldOrbit = (() => {
307
312
  return false;
308
313
  }
309
314
  if (filter.groupIds?.length && (!object.groupId || !filter.groupIds.includes(object.groupId))) {
310
- return false;
315
+ const hasSemanticMatch = object.semanticGroupIds.length > 0 && filter.groupIds.some((groupId) => object.semanticGroupIds.includes(groupId));
316
+ const hasLegacyMatch = Boolean(object.groupId && filter.groupIds.includes(object.groupId));
317
+ if (!hasSemanticMatch && !hasLegacyMatch) {
318
+ return false;
319
+ }
311
320
  }
312
321
  if (filter.tags?.length) {
313
322
  const objectTags = Array.isArray(object.object.properties.tags) ? object.object.properties.tags.filter((entry) => typeof entry === "string") : [];
@@ -674,13 +683,13 @@ var WorldOrbit = (() => {
674
683
  function unitFamilyAllowsUnit(family, unit) {
675
684
  switch (family) {
676
685
  case "distance":
677
- return unit === null || ["au", "km", "re", "sol"].includes(unit);
686
+ return unit === null || ["au", "km", "m", "ly", "pc", "kpc", "re", "sol"].includes(unit);
678
687
  case "radius":
679
- return unit === null || ["km", "re", "sol"].includes(unit);
688
+ return unit === null || ["km", "m", "re", "rj", "sol"].includes(unit);
680
689
  case "mass":
681
- return unit === null || ["me", "sol"].includes(unit);
690
+ return unit === null || ["me", "mj", "sol"].includes(unit);
682
691
  case "duration":
683
- return unit === null || ["h", "d", "y"].includes(unit);
692
+ return unit === null || ["s", "min", "h", "d", "y", "ky", "my", "gy"].includes(unit);
684
693
  case "angle":
685
694
  return unit === null || unit === "deg";
686
695
  case "generic":
@@ -884,7 +893,7 @@ var WorldOrbit = (() => {
884
893
  }
885
894
 
886
895
  // packages/core/dist/normalize.js
887
- var UNIT_PATTERN = /^(-?\d+(?:\.\d+)?)(au|km|re|sol|me|d|y|h|deg)?$/;
896
+ var UNIT_PATTERN = /^(-?\d+(?:\.\d+)?)(kpc|min|mj|rj|ky|my|gy|au|km|me|re|pc|ly|deg|sol|K|m|s|h|d|y)?$/;
888
897
  var BOOLEAN_VALUES = /* @__PURE__ */ new Map([
889
898
  ["true", true],
890
899
  ["false", false],
@@ -909,7 +918,10 @@ var WorldOrbit = (() => {
909
918
  return {
910
919
  format: "worldorbit",
911
920
  version: "1.0",
921
+ schemaVersion: "1.0",
912
922
  system,
923
+ groups: [],
924
+ relations: [],
913
925
  objects
914
926
  };
915
927
  }
@@ -919,13 +931,17 @@ var WorldOrbit = (() => {
919
931
  const fieldMap = collectFields(mergedFields);
920
932
  const placement = extractPlacement(node.objectType, fieldMap);
921
933
  const properties = normalizeProperties(fieldMap);
922
- const info = normalizeInfo(node.infoEntries);
934
+ const info2 = normalizeInfo(node.infoEntries);
923
935
  if (node.objectType === "system") {
924
936
  return {
925
937
  type: "system",
926
938
  id: node.name,
939
+ title: typeof properties.title === "string" ? properties.title : null,
940
+ description: null,
941
+ epoch: null,
942
+ referencePlane: null,
927
943
  properties,
928
- info
944
+ info: info2
929
945
  };
930
946
  }
931
947
  return {
@@ -933,7 +949,7 @@ var WorldOrbit = (() => {
933
949
  id: node.name,
934
950
  properties,
935
951
  placement,
936
- info
952
+ info: info2
937
953
  };
938
954
  }
939
955
  function validateFieldCompatibility(objectType, fields) {
@@ -1063,14 +1079,14 @@ var WorldOrbit = (() => {
1063
1079
  }
1064
1080
  }
1065
1081
  function normalizeInfo(entries) {
1066
- const info = {};
1082
+ const info2 = {};
1067
1083
  for (const entry of entries) {
1068
- if (entry.key in info) {
1084
+ if (entry.key in info2) {
1069
1085
  throw WorldOrbitError.fromLocation(`Duplicate info key "${entry.key}"`, entry.location);
1070
1086
  }
1071
- info[entry.key] = entry.value;
1087
+ info2[entry.key] = entry.value;
1072
1088
  }
1073
- return info;
1089
+ return info2;
1074
1090
  }
1075
1091
  function parseAtReference(target, location) {
1076
1092
  if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
@@ -1241,37 +1257,41 @@ var WorldOrbit = (() => {
1241
1257
  }
1242
1258
 
1243
1259
  // packages/core/dist/diagnostics.js
1244
- function diagnosticFromError(error, source, code = `${source}.failed`) {
1245
- if (error instanceof WorldOrbitError) {
1260
+ function diagnosticFromError(error2, source, code = `${source}.failed`) {
1261
+ if (error2 instanceof WorldOrbitError) {
1246
1262
  return {
1247
1263
  code,
1248
1264
  severity: "error",
1249
1265
  source,
1250
- message: error.message,
1251
- line: error.line,
1252
- column: error.column
1266
+ message: error2.message,
1267
+ line: error2.line,
1268
+ column: error2.column
1253
1269
  };
1254
1270
  }
1255
- if (error instanceof Error) {
1271
+ if (error2 instanceof Error) {
1256
1272
  return {
1257
1273
  code,
1258
1274
  severity: "error",
1259
1275
  source,
1260
- message: error.message
1276
+ message: error2.message
1261
1277
  };
1262
1278
  }
1263
1279
  return {
1264
1280
  code,
1265
1281
  severity: "error",
1266
1282
  source,
1267
- message: String(error)
1283
+ message: String(error2)
1268
1284
  };
1269
1285
  }
1270
1286
 
1271
1287
  // packages/core/dist/scene.js
1272
1288
  var AU_IN_KM = 1495978707e-1;
1273
1289
  var EARTH_RADIUS_IN_KM = 6371;
1290
+ var JUPITER_RADIUS_IN_KM = 71492;
1274
1291
  var SOLAR_RADIUS_IN_KM = 695700;
1292
+ var LY_IN_AU = 63241.077;
1293
+ var PC_IN_AU = 206264.806;
1294
+ var KPC_IN_AU = 206264806;
1275
1295
  var ISO_FLATTENING = 0.68;
1276
1296
  var MIN_ISO_MINOR_SCALE = 0.2;
1277
1297
  var ARC_SAMPLE_COUNT = 28;
@@ -1391,8 +1411,10 @@ var WorldOrbit = (() => {
1391
1411
  const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
1392
1412
  const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
1393
1413
  const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
1394
- const layers = createSceneLayers(orbitVisuals, leaders, objects, labels);
1414
+ const relations = createSceneRelations(document2, objects);
1415
+ const layers = createSceneLayers(orbitVisuals, relations, leaders, objects, labels);
1395
1416
  const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
1417
+ const semanticGroups = createSceneSemanticGroups(document2, objects);
1396
1418
  const viewpoints = createSceneViewpoints(document2, projection, frame.preset, relationships, objectMap);
1397
1419
  const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
1398
1420
  return {
@@ -1402,7 +1424,7 @@ var WorldOrbit = (() => {
1402
1424
  renderPreset: frame.preset,
1403
1425
  projection,
1404
1426
  scaleModel,
1405
- title: String(document2.system?.properties.title ?? document2.system?.id ?? "WorldOrbit") || "WorldOrbit",
1427
+ title: String(document2.system?.title ?? document2.system?.properties.title ?? document2.system?.id ?? "WorldOrbit") || "WorldOrbit",
1406
1428
  subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
1407
1429
  systemId,
1408
1430
  viewMode: projection,
@@ -1418,9 +1440,11 @@ var WorldOrbit = (() => {
1418
1440
  contentBounds,
1419
1441
  layers,
1420
1442
  groups,
1443
+ semanticGroups,
1421
1444
  viewpoints,
1422
1445
  objects,
1423
1446
  orbitVisuals,
1447
+ relations,
1424
1448
  leaders,
1425
1449
  labels
1426
1450
  };
@@ -1530,6 +1554,7 @@ var WorldOrbit = (() => {
1530
1554
  }
1531
1555
  function createSceneObject(position, scaleModel, relationships) {
1532
1556
  const { object, x, y, radius, sortKey, anchorX, anchorY } = position;
1557
+ const renderPriority = object.renderHints?.renderPriority ?? 0;
1533
1558
  return {
1534
1559
  renderId: createRenderId(object.id),
1535
1560
  objectId: object.id,
@@ -1538,11 +1563,12 @@ var WorldOrbit = (() => {
1538
1563
  ancestorIds: relationships.ancestorIds.get(object.id) ?? [],
1539
1564
  childIds: relationships.childIds.get(object.id) ?? [],
1540
1565
  groupId: relationships.groupIds.get(object.id) ?? null,
1566
+ semanticGroupIds: [...object.groups ?? []],
1541
1567
  x,
1542
1568
  y,
1543
1569
  radius,
1544
1570
  visualRadius: visualExtentForObject(object, radius, scaleModel),
1545
- sortKey,
1571
+ sortKey: sortKey + renderPriority * 1e-3,
1546
1572
  anchorX,
1547
1573
  anchorY,
1548
1574
  label: object.id,
@@ -1559,6 +1585,7 @@ var WorldOrbit = (() => {
1559
1585
  object: draft.object,
1560
1586
  parentId: draft.parentId,
1561
1587
  groupId,
1588
+ semanticGroupIds: [...draft.object.groups ?? []],
1562
1589
  kind: draft.kind,
1563
1590
  cx: draft.cx,
1564
1591
  cy: draft.cy,
@@ -1570,7 +1597,7 @@ var WorldOrbit = (() => {
1570
1597
  bandThickness: draft.bandThickness,
1571
1598
  frontArcPath: draft.frontArcPath,
1572
1599
  backArcPath: draft.backArcPath,
1573
- hidden: draft.object.properties.hidden === true
1600
+ hidden: draft.object.properties.hidden === true || draft.object.renderHints?.renderOrbit === false
1574
1601
  };
1575
1602
  }
1576
1603
  function createLeaderLine(draft) {
@@ -1579,6 +1606,7 @@ var WorldOrbit = (() => {
1579
1606
  objectId: draft.object.id,
1580
1607
  object: draft.object,
1581
1608
  groupId: draft.groupId,
1609
+ semanticGroupIds: [...draft.object.groups ?? []],
1582
1610
  x1: draft.x1,
1583
1611
  y1: draft.y1,
1584
1612
  x2: draft.x2,
@@ -1590,7 +1618,7 @@ var WorldOrbit = (() => {
1590
1618
  function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1591
1619
  const labels = [];
1592
1620
  const occupied = [];
1593
- const visibleObjects = [...objects].filter((object) => !object.hidden).sort((left, right) => left.sortKey - right.sortKey);
1621
+ const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort((left, right) => left.sortKey - right.sortKey);
1594
1622
  for (const object of visibleObjects) {
1595
1623
  const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1596
1624
  const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
@@ -1610,6 +1638,7 @@ var WorldOrbit = (() => {
1610
1638
  objectId: object.objectId,
1611
1639
  object: object.object,
1612
1640
  groupId: object.groupId,
1641
+ semanticGroupIds: [...object.semanticGroupIds],
1613
1642
  label: object.label,
1614
1643
  secondaryLabel: object.secondaryLabel,
1615
1644
  x: object.x,
@@ -1622,7 +1651,7 @@ var WorldOrbit = (() => {
1622
1651
  }
1623
1652
  return labels;
1624
1653
  }
1625
- function createSceneLayers(orbitVisuals, leaders, objects, labels) {
1654
+ function createSceneLayers(orbitVisuals, relations, leaders, objects, labels) {
1626
1655
  const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1627
1656
  const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1628
1657
  return [
@@ -1633,6 +1662,10 @@ var WorldOrbit = (() => {
1633
1662
  },
1634
1663
  { id: "orbits-back", renderIds: backOrbitIds },
1635
1664
  { id: "orbits-front", renderIds: frontOrbitIds },
1665
+ {
1666
+ id: "relations",
1667
+ renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId)
1668
+ },
1636
1669
  {
1637
1670
  id: "objects",
1638
1671
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
@@ -1697,6 +1730,36 @@ var WorldOrbit = (() => {
1697
1730
  }
1698
1731
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1699
1732
  }
1733
+ function createSceneSemanticGroups(document2, objects) {
1734
+ return [...document2.groups].map((group) => ({
1735
+ id: group.id,
1736
+ label: group.label,
1737
+ summary: group.summary,
1738
+ color: group.color,
1739
+ tags: [...group.tags],
1740
+ hidden: group.hidden,
1741
+ objectIds: objects.filter((object) => !object.hidden && object.semanticGroupIds.includes(group.id)).map((object) => object.objectId)
1742
+ })).sort((left, right) => left.label.localeCompare(right.label));
1743
+ }
1744
+ function createSceneRelations(document2, objects) {
1745
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1746
+ return document2.relations.map((relation) => {
1747
+ const from = objectMap.get(relation.from);
1748
+ const to = objectMap.get(relation.to);
1749
+ return {
1750
+ renderId: `${createRenderId(relation.id)}-relation`,
1751
+ relationId: relation.id,
1752
+ relation,
1753
+ fromObjectId: relation.from,
1754
+ toObjectId: relation.to,
1755
+ x1: from?.x ?? 0,
1756
+ y1: from?.y ?? 0,
1757
+ x2: to?.x ?? 0,
1758
+ y2: to?.y ?? 0,
1759
+ hidden: relation.hidden || !from || !to || from.hidden || to.hidden
1760
+ };
1761
+ }).sort((left, right) => left.relation.id.localeCompare(right.relation.id));
1762
+ }
1700
1763
  function createSceneViewpoints(document2, projection, preset, relationships, objectMap) {
1701
1764
  const generatedOverview = createGeneratedOverviewViewpoint(document2, projection, preset);
1702
1765
  const drafts = /* @__PURE__ */ new Map();
@@ -1714,7 +1777,7 @@ var WorldOrbit = (() => {
1714
1777
  }
1715
1778
  const field = fieldParts.join(".").toLowerCase();
1716
1779
  const draft = drafts.get(id) ?? { id };
1717
- applyViewpointField(draft, field, value, projection, preset, relationships, objectMap);
1780
+ applyViewpointField(draft, field, value, document2, projection, preset, relationships, objectMap);
1718
1781
  drafts.set(id, draft);
1719
1782
  }
1720
1783
  const viewpoints = [...drafts.values()].map((draft) => finalizeViewpointDraft(draft, projection, preset, objectMap)).filter(Boolean);
@@ -1742,7 +1805,8 @@ var WorldOrbit = (() => {
1742
1805
  });
1743
1806
  }
1744
1807
  function createGeneratedOverviewViewpoint(document2, projection, preset) {
1745
- const label = document2.system?.properties.title ? `${String(document2.system.properties.title)} Overview` : "Overview";
1808
+ const title = document2.system?.title ?? document2.system?.properties.title;
1809
+ const label = title ? `${String(title)} Overview` : "Overview";
1746
1810
  return {
1747
1811
  id: "overview",
1748
1812
  label,
@@ -1758,7 +1822,7 @@ var WorldOrbit = (() => {
1758
1822
  generated: true
1759
1823
  };
1760
1824
  }
1761
- function applyViewpointField(draft, field, value, projection, preset, relationships, objectMap) {
1825
+ function applyViewpointField(draft, field, value, document2, projection, preset, relationships, objectMap) {
1762
1826
  const normalizedValue = value.trim();
1763
1827
  switch (field) {
1764
1828
  case "label":
@@ -1825,7 +1889,7 @@ var WorldOrbit = (() => {
1825
1889
  case "groups":
1826
1890
  draft.filter = {
1827
1891
  ...draft.filter ?? createEmptyViewpointFilter(),
1828
- groupIds: parseViewpointGroups(normalizedValue, relationships, objectMap)
1892
+ groupIds: parseViewpointGroups(normalizedValue, document2, relationships, objectMap)
1829
1893
  };
1830
1894
  return;
1831
1895
  }
@@ -1898,7 +1962,7 @@ var WorldOrbit = (() => {
1898
1962
  next["orbits-front"] = enabled;
1899
1963
  continue;
1900
1964
  }
1901
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1965
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1902
1966
  next[rawLayer] = enabled;
1903
1967
  }
1904
1968
  }
@@ -1907,8 +1971,11 @@ var WorldOrbit = (() => {
1907
1971
  function parseViewpointObjectTypes(value) {
1908
1972
  return splitListValue(value).filter((entry) => entry === "star" || entry === "planet" || entry === "moon" || entry === "belt" || entry === "asteroid" || entry === "comet" || entry === "ring" || entry === "structure" || entry === "phenomenon");
1909
1973
  }
1910
- function parseViewpointGroups(value, relationships, objectMap) {
1974
+ function parseViewpointGroups(value, document2, relationships, objectMap) {
1911
1975
  return splitListValue(value).map((entry) => {
1976
+ if (document2.schemaVersion === "2.1" || document2.groups.some((group) => group.id === entry)) {
1977
+ return entry;
1978
+ }
1912
1979
  if (entry.startsWith("wo-") && entry.endsWith("-group")) {
1913
1980
  return entry;
1914
1981
  }
@@ -2039,8 +2106,9 @@ var WorldOrbit = (() => {
2039
2106
  }
2040
2107
  const orbiting = [...context.orbitChildren.get(object.id) ?? []].sort(compareOrbiting);
2041
2108
  const orbitMetricContext = computeOrbitMetricContext(orbiting, parent.radius, context.spacingFactor, context.scaleModel);
2109
+ const orbitRadiiPx = resolveOrbitRadiiPx(orbiting, orbitMetricContext);
2042
2110
  orbiting.forEach((child, index) => {
2043
- const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, context);
2111
+ const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, orbitRadiiPx[index] ?? orbitMetricContext.innerPx, context);
2044
2112
  orbitDrafts.push({
2045
2113
  object: child,
2046
2114
  parentId: object.id,
@@ -2114,7 +2182,8 @@ var WorldOrbit = (() => {
2114
2182
  metricSpread: 0,
2115
2183
  innerPx,
2116
2184
  stepPx,
2117
- pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx)
2185
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
2186
+ minimumGapPx: stepPx * 0.42
2118
2187
  };
2119
2188
  }
2120
2189
  const minMetric = Math.min(...presentMetrics);
@@ -2127,10 +2196,11 @@ var WorldOrbit = (() => {
2127
2196
  metricSpread,
2128
2197
  innerPx,
2129
2198
  stepPx,
2130
- pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx)
2199
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
2200
+ minimumGapPx: stepPx * 0.42
2131
2201
  };
2132
2202
  }
2133
- function resolveOrbitGeometry(object, index, count, parent, metricContext, context) {
2203
+ function resolveOrbitGeometry(object, index, count, parent, metricContext, orbitRadiusPx, context) {
2134
2204
  const placement = object.placement;
2135
2205
  const band = object.type === "belt" || object.type === "ring";
2136
2206
  if (!placement || placement.mode !== "orbit") {
@@ -2148,7 +2218,7 @@ var WorldOrbit = (() => {
2148
2218
  };
2149
2219
  }
2150
2220
  const eccentricity = clampNumber(typeof placement.eccentricity === "number" ? placement.eccentricity : 0, 0, 0.92);
2151
- const semiMajor = resolveOrbitRadiusPx(object, index, metricContext);
2221
+ const semiMajor = orbitRadiusPx;
2152
2222
  const baseMinor = Math.max(semiMajor * Math.sqrt(1 - eccentricity * eccentricity), semiMajor * 0.18);
2153
2223
  const inclinationDeg = unitValueToDegrees(placement.inclination) ?? 0;
2154
2224
  const inclinationScale = context.projection === "isometric" ? Math.max(MIN_ISO_MINOR_SCALE, Math.cos(degreesToRadians(inclinationDeg))) * ISO_FLATTENING : 1;
@@ -2178,15 +2248,19 @@ var WorldOrbit = (() => {
2178
2248
  objectY: objectPoint.y
2179
2249
  };
2180
2250
  }
2181
- function resolveOrbitRadiusPx(object, index, metricContext) {
2182
- const metric = orbitMetric(object);
2183
- if (metric === null) {
2184
- return metricContext.innerPx + index * metricContext.stepPx;
2185
- }
2186
- if (metricContext.metricSpread > 0) {
2187
- return metricContext.innerPx + (metric - metricContext.minMetric) / metricContext.metricSpread * metricContext.pixelSpread;
2188
- }
2189
- return metricContext.innerPx + Math.log10(metric + 1) * metricContext.stepPx;
2251
+ function resolveOrbitRadiusPx(metric, metricContext) {
2252
+ return metricContext.innerPx + metricContext.stepPx * log2(Math.max(metric, 0) + 1);
2253
+ }
2254
+ function resolveOrbitRadiiPx(objects, metricContext) {
2255
+ const radii = [];
2256
+ objects.forEach((object, index) => {
2257
+ const metric = orbitMetric(object);
2258
+ const fallbackRadius = metricContext.innerPx + index * metricContext.stepPx;
2259
+ const baseRadius = metric === null ? fallbackRadius : resolveOrbitRadiusPx(metric, metricContext);
2260
+ const minimumRadius = index === 0 ? metricContext.innerPx : (radii[index - 1] ?? metricContext.innerPx) + metricContext.minimumGapPx;
2261
+ radii.push(Math.max(baseRadius, minimumRadius));
2262
+ });
2263
+ return radii;
2190
2264
  }
2191
2265
  function orbitMetric(object) {
2192
2266
  if (!object.placement || object.placement.mode !== "orbit") {
@@ -2194,6 +2268,9 @@ var WorldOrbit = (() => {
2194
2268
  }
2195
2269
  return toDistanceMetric(object.placement.semiMajor ?? object.placement.distance ?? null);
2196
2270
  }
2271
+ function log2(value) {
2272
+ return Math.log(value) / Math.log(2);
2273
+ }
2197
2274
  function resolveOrbitPhase(phase, index, count) {
2198
2275
  const degreeValue = phase ? unitValueToDegrees(phase) : null;
2199
2276
  if (degreeValue !== null) {
@@ -2517,8 +2594,18 @@ var WorldOrbit = (() => {
2517
2594
  return value.value;
2518
2595
  case "km":
2519
2596
  return value.value / AU_IN_KM;
2597
+ case "m":
2598
+ return value.value / 1e3 / AU_IN_KM;
2599
+ case "ly":
2600
+ return value.value * LY_IN_AU;
2601
+ case "pc":
2602
+ return value.value * PC_IN_AU;
2603
+ case "kpc":
2604
+ return value.value * KPC_IN_AU;
2520
2605
  case "re":
2521
2606
  return value.value * EARTH_RADIUS_IN_KM / AU_IN_KM;
2607
+ case "rj":
2608
+ return value.value * JUPITER_RADIUS_IN_KM / AU_IN_KM;
2522
2609
  case "sol":
2523
2610
  return value.value * SOLAR_RADIUS_IN_KM / AU_IN_KM;
2524
2611
  default:
@@ -2658,19 +2745,37 @@ var WorldOrbit = (() => {
2658
2745
  const system = document2.system ? {
2659
2746
  type: "system",
2660
2747
  id: document2.system.id,
2748
+ title: document2.system.title,
2749
+ description: document2.system.description,
2750
+ epoch: document2.system.epoch,
2751
+ referencePlane: document2.system.referencePlane,
2661
2752
  properties: materializeDraftSystemProperties(document2.system),
2662
2753
  info: materializeDraftSystemInfo(document2.system)
2663
2754
  } : null;
2664
2755
  return {
2665
2756
  format: "worldorbit",
2666
2757
  version: "1.0",
2758
+ schemaVersion: document2.version,
2667
2759
  system,
2760
+ groups: structuredClone(document2.groups ?? []),
2761
+ relations: structuredClone(document2.relations ?? []),
2668
2762
  objects: document2.objects.map(cloneWorldOrbitObject)
2669
2763
  };
2670
2764
  }
2671
2765
  function cloneWorldOrbitObject(object) {
2672
2766
  return {
2673
2767
  ...object,
2768
+ groups: object.groups ? [...object.groups] : void 0,
2769
+ resonance: object.resonance ? { ...object.resonance } : object.resonance,
2770
+ renderHints: object.renderHints ? { ...object.renderHints } : object.renderHints,
2771
+ deriveRules: object.deriveRules ? object.deriveRules.map((rule) => ({ ...rule })) : void 0,
2772
+ validationRules: object.validationRules ? object.validationRules.map((rule) => ({ ...rule })) : void 0,
2773
+ lockedFields: object.lockedFields ? [...object.lockedFields] : void 0,
2774
+ tolerances: object.tolerances ? object.tolerances.map((entry) => ({
2775
+ field: entry.field,
2776
+ 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
2777
+ })) : void 0,
2778
+ typedBlocks: object.typedBlocks ? Object.fromEntries(Object.entries(object.typedBlocks).map(([key, block]) => [key, { ...block ?? {} }])) : void 0,
2674
2779
  properties: cloneProperties(object.properties),
2675
2780
  placement: object.placement ? structuredClone(object.placement) : null,
2676
2781
  info: { ...object.info }
@@ -2706,71 +2811,80 @@ var WorldOrbit = (() => {
2706
2811
  if (system.defaults.units) {
2707
2812
  properties.units = system.defaults.units;
2708
2813
  }
2814
+ if (system.description) {
2815
+ properties.description = system.description;
2816
+ }
2817
+ if (system.epoch) {
2818
+ properties.epoch = system.epoch;
2819
+ }
2820
+ if (system.referencePlane) {
2821
+ properties.referencePlane = system.referencePlane;
2822
+ }
2709
2823
  return properties;
2710
2824
  }
2711
2825
  function materializeDraftSystemInfo(system) {
2712
- const info = {
2826
+ const info2 = {
2713
2827
  ...system.atlasMetadata
2714
2828
  };
2715
2829
  if (system.defaults.theme) {
2716
- info["atlas.theme"] = system.defaults.theme;
2830
+ info2["atlas.theme"] = system.defaults.theme;
2717
2831
  }
2718
2832
  for (const viewpoint of system.viewpoints) {
2719
2833
  const prefix = `viewpoint.${viewpoint.id}`;
2720
- info[`${prefix}.label`] = viewpoint.label;
2834
+ info2[`${prefix}.label`] = viewpoint.label;
2721
2835
  if (viewpoint.summary) {
2722
- info[`${prefix}.summary`] = viewpoint.summary;
2836
+ info2[`${prefix}.summary`] = viewpoint.summary;
2723
2837
  }
2724
2838
  if (viewpoint.focusObjectId) {
2725
- info[`${prefix}.focus`] = viewpoint.focusObjectId;
2839
+ info2[`${prefix}.focus`] = viewpoint.focusObjectId;
2726
2840
  }
2727
2841
  if (viewpoint.selectedObjectId) {
2728
- info[`${prefix}.select`] = viewpoint.selectedObjectId;
2842
+ info2[`${prefix}.select`] = viewpoint.selectedObjectId;
2729
2843
  }
2730
2844
  if (viewpoint.projection) {
2731
- info[`${prefix}.projection`] = viewpoint.projection;
2845
+ info2[`${prefix}.projection`] = viewpoint.projection;
2732
2846
  }
2733
2847
  if (viewpoint.preset) {
2734
- info[`${prefix}.preset`] = viewpoint.preset;
2848
+ info2[`${prefix}.preset`] = viewpoint.preset;
2735
2849
  }
2736
2850
  if (viewpoint.zoom !== null) {
2737
- info[`${prefix}.zoom`] = String(viewpoint.zoom);
2851
+ info2[`${prefix}.zoom`] = String(viewpoint.zoom);
2738
2852
  }
2739
2853
  if (viewpoint.rotationDeg !== 0) {
2740
- info[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2854
+ info2[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2741
2855
  }
2742
2856
  const serializedLayers = serializeViewpointLayers(viewpoint.layers);
2743
2857
  if (serializedLayers) {
2744
- info[`${prefix}.layers`] = serializedLayers;
2858
+ info2[`${prefix}.layers`] = serializedLayers;
2745
2859
  }
2746
2860
  if (viewpoint.filter?.query) {
2747
- info[`${prefix}.query`] = viewpoint.filter.query;
2861
+ info2[`${prefix}.query`] = viewpoint.filter.query;
2748
2862
  }
2749
2863
  if ((viewpoint.filter?.objectTypes.length ?? 0) > 0) {
2750
- info[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
2864
+ info2[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
2751
2865
  }
2752
2866
  if ((viewpoint.filter?.tags.length ?? 0) > 0) {
2753
- info[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
2867
+ info2[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
2754
2868
  }
2755
2869
  if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2756
- info[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2870
+ info2[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2757
2871
  }
2758
2872
  }
2759
2873
  for (const annotation of system.annotations) {
2760
2874
  const prefix = `annotation.${annotation.id}`;
2761
- info[`${prefix}.label`] = annotation.label;
2875
+ info2[`${prefix}.label`] = annotation.label;
2762
2876
  if (annotation.targetObjectId) {
2763
- info[`${prefix}.target`] = annotation.targetObjectId;
2877
+ info2[`${prefix}.target`] = annotation.targetObjectId;
2764
2878
  }
2765
- info[`${prefix}.body`] = annotation.body;
2879
+ info2[`${prefix}.body`] = annotation.body;
2766
2880
  if (annotation.tags.length > 0) {
2767
- info[`${prefix}.tags`] = annotation.tags.join(" ");
2881
+ info2[`${prefix}.tags`] = annotation.tags.join(" ");
2768
2882
  }
2769
2883
  if (annotation.sourceObjectId) {
2770
- info[`${prefix}.source`] = annotation.sourceObjectId;
2884
+ info2[`${prefix}.source`] = annotation.sourceObjectId;
2771
2885
  }
2772
2886
  }
2773
- return info;
2887
+ return info2;
2774
2888
  }
2775
2889
  function serializeViewpointLayers(layers) {
2776
2890
  const tokens = [];
@@ -2779,7 +2893,7 @@ var WorldOrbit = (() => {
2779
2893
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2780
2894
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2781
2895
  }
2782
- for (const key of ["background", "guides", "objects", "labels", "metadata"]) {
2896
+ for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
2783
2897
  if (layers[key] !== void 0) {
2784
2898
  tokens.push(layers[key] ? key : `-${key}`);
2785
2899
  }
@@ -2787,21 +2901,530 @@ var WorldOrbit = (() => {
2787
2901
  return tokens.join(" ");
2788
2902
  }
2789
2903
 
2904
+ // packages/core/dist/atlas-utils.js
2905
+ 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)?$/;
2906
+ var BOOLEAN_VALUES2 = /* @__PURE__ */ new Map([
2907
+ ["true", true],
2908
+ ["false", false],
2909
+ ["yes", true],
2910
+ ["no", false]
2911
+ ]);
2912
+ var URL_SCHEME_PATTERN2 = /^[A-Za-z][A-Za-z0-9+.-]*:/;
2913
+ function normalizeIdentifier(value) {
2914
+ return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
2915
+ }
2916
+ function humanizeIdentifier2(value) {
2917
+ return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
2918
+ }
2919
+ function parseAtlasUnitValue(input, location, fieldKey) {
2920
+ const match = input.match(UNIT_PATTERN2);
2921
+ if (!match) {
2922
+ throw WorldOrbitError.fromLocation(`Invalid unit value "${input}"`, location);
2923
+ }
2924
+ const unitValue = {
2925
+ value: Number(match[1]),
2926
+ unit: match[2] ?? null
2927
+ };
2928
+ if (fieldKey) {
2929
+ const schema = getFieldSchema(fieldKey);
2930
+ if (schema?.unitFamily && !unitFamilyAllowsUnit(schema.unitFamily, unitValue.unit)) {
2931
+ throw WorldOrbitError.fromLocation(`Unit "${unitValue.unit ?? "none"}" is not valid for "${fieldKey}"`, location);
2932
+ }
2933
+ }
2934
+ return unitValue;
2935
+ }
2936
+ function tryParseAtlasUnitValue(input) {
2937
+ const match = input.match(UNIT_PATTERN2);
2938
+ if (!match) {
2939
+ return null;
2940
+ }
2941
+ return {
2942
+ value: Number(match[1]),
2943
+ unit: match[2] ?? null
2944
+ };
2945
+ }
2946
+ function parseAtlasNumber(input, key, location) {
2947
+ const value = Number(input);
2948
+ if (!Number.isFinite(value)) {
2949
+ throw WorldOrbitError.fromLocation(`Invalid numeric value "${input}" for "${key}"`, location);
2950
+ }
2951
+ return value;
2952
+ }
2953
+ function parseAtlasBoolean(input, key, location) {
2954
+ const parsed = BOOLEAN_VALUES2.get(input.toLowerCase());
2955
+ if (parsed === void 0) {
2956
+ throw WorldOrbitError.fromLocation(`Invalid boolean value "${input}" for "${key}"`, location);
2957
+ }
2958
+ return parsed;
2959
+ }
2960
+ function parseAtlasAtReference(target, location) {
2961
+ if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
2962
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
2963
+ }
2964
+ const pairedMatch = target.match(/^([A-Za-z0-9._-]+)-([A-Za-z0-9._-]+):(L[1-5])$/);
2965
+ if (pairedMatch) {
2966
+ return {
2967
+ kind: "lagrange",
2968
+ primary: pairedMatch[1],
2969
+ secondary: pairedMatch[2],
2970
+ point: pairedMatch[3]
2971
+ };
2972
+ }
2973
+ const simpleMatch = target.match(/^([A-Za-z0-9._-]+):(L[1-5])$/);
2974
+ if (simpleMatch) {
2975
+ return {
2976
+ kind: "lagrange",
2977
+ primary: simpleMatch[1],
2978
+ secondary: null,
2979
+ point: simpleMatch[2]
2980
+ };
2981
+ }
2982
+ if (/^[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
2983
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
2984
+ }
2985
+ const anchorMatch = target.match(/^([A-Za-z0-9._-]+):([A-Za-z0-9._-]+)$/);
2986
+ if (anchorMatch) {
2987
+ return {
2988
+ kind: "anchor",
2989
+ objectId: anchorMatch[1],
2990
+ anchor: anchorMatch[2]
2991
+ };
2992
+ }
2993
+ return {
2994
+ kind: "named",
2995
+ name: target
2996
+ };
2997
+ }
2998
+ function validateAtlasImageSource(value, location) {
2999
+ if (!value) {
3000
+ throw WorldOrbitError.fromLocation('Field "image" must not be empty', location);
3001
+ }
3002
+ if (value.startsWith("//")) {
3003
+ throw WorldOrbitError.fromLocation('Field "image" must use a relative path, root-relative path, or an http/https URL', location);
3004
+ }
3005
+ const schemeMatch = value.match(URL_SCHEME_PATTERN2);
3006
+ if (!schemeMatch) {
3007
+ return;
3008
+ }
3009
+ const scheme = schemeMatch[0].slice(0, -1).toLowerCase();
3010
+ if (scheme !== "http" && scheme !== "https") {
3011
+ throw WorldOrbitError.fromLocation(`Field "image" does not support the "${scheme}" scheme`, location);
3012
+ }
3013
+ }
3014
+ function normalizeLegacyScalarValue(key, values, location) {
3015
+ const schema = getFieldSchema(key);
3016
+ if (!schema) {
3017
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
3018
+ }
3019
+ if (schema.arity === "single" && values.length !== 1) {
3020
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
3021
+ }
3022
+ switch (schema.kind) {
3023
+ case "list":
3024
+ return values;
3025
+ case "boolean":
3026
+ return parseAtlasBoolean(singleAtlasValue(values, key, location), key, location);
3027
+ case "number":
3028
+ return parseAtlasNumber(singleAtlasValue(values, key, location), key, location);
3029
+ case "unit":
3030
+ return parseAtlasUnitValue(singleAtlasValue(values, key, location), location, key);
3031
+ case "string": {
3032
+ const value = values.join(" ").trim();
3033
+ if (key === "image") {
3034
+ validateAtlasImageSource(value, location);
3035
+ }
3036
+ return value;
3037
+ }
3038
+ }
3039
+ }
3040
+ function ensureAtlasFieldSupported(key, objectType, location) {
3041
+ const schema = getFieldSchema(key);
3042
+ if (!schema) {
3043
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
3044
+ }
3045
+ if (!schema.objectTypes.includes(objectType)) {
3046
+ throw WorldOrbitError.fromLocation(`Field "${key}" is not valid on "${objectType}"`, location);
3047
+ }
3048
+ }
3049
+ function singleAtlasValue(values, key, location) {
3050
+ if (values.length !== 1) {
3051
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
3052
+ }
3053
+ return values[0];
3054
+ }
3055
+
3056
+ // packages/core/dist/atlas-validate.js
3057
+ var SURFACE_TARGET_TYPES2 = /* @__PURE__ */ new Set(["star", "planet", "moon", "asteroid", "comet"]);
3058
+ var EARTH_MASSES_PER_SOLAR = 332946.0487;
3059
+ var JUPITER_MASSES_PER_SOLAR = 1047.3486;
3060
+ var AU_IN_KM2 = 1495978707e-1;
3061
+ var EARTH_RADIUS_IN_KM2 = 6371;
3062
+ var SOLAR_RADIUS_IN_KM2 = 695700;
3063
+ var LY_IN_AU2 = 63241.077;
3064
+ var PC_IN_AU2 = 206264.806;
3065
+ var KPC_IN_AU2 = 206264806;
3066
+ function collectAtlasDiagnostics(document2, sourceSchemaVersion) {
3067
+ const diagnostics = [];
3068
+ const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
3069
+ const groupIds = new Set(document2.groups.map((group) => group.id));
3070
+ if (!document2.system) {
3071
+ diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
3072
+ }
3073
+ const knownIds = /* @__PURE__ */ new Map();
3074
+ for (const [kind, ids] of [
3075
+ ["group", document2.groups.map((group) => group.id)],
3076
+ ["viewpoint", document2.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
3077
+ ["annotation", document2.system?.annotations.map((annotation) => annotation.id) ?? []],
3078
+ ["relation", document2.relations.map((relation) => relation.id)],
3079
+ ["object", document2.objects.map((object) => object.id)]
3080
+ ]) {
3081
+ for (const id of ids) {
3082
+ const previous = knownIds.get(id);
3083
+ if (previous) {
3084
+ diagnostics.push(error("validate.id.duplicate", `Duplicate ${kind} id "${id}" already used by ${previous}.`));
3085
+ } else {
3086
+ knownIds.set(id, kind);
3087
+ }
3088
+ }
3089
+ }
3090
+ for (const relation of document2.relations) {
3091
+ validateRelation(relation, objectMap, diagnostics);
3092
+ }
3093
+ for (const viewpoint of document2.system?.viewpoints ?? []) {
3094
+ validateViewpointFilter(viewpoint.filter, groupIds, sourceSchemaVersion, diagnostics, viewpoint.id);
3095
+ }
3096
+ for (const object of document2.objects) {
3097
+ validateObject(object, document2.system, objectMap, groupIds, diagnostics);
3098
+ }
3099
+ return diagnostics;
3100
+ }
3101
+ function validateRelation(relation, objectMap, diagnostics) {
3102
+ if (!relation.from) {
3103
+ diagnostics.push(error("validate.relation.from.required", `Relation "${relation.id}" is missing a "from" target.`));
3104
+ } else if (!objectMap.has(relation.from)) {
3105
+ diagnostics.push(error("validate.relation.from.unknown", `Unknown relation source "${relation.from}" on "${relation.id}".`));
3106
+ }
3107
+ if (!relation.to) {
3108
+ diagnostics.push(error("validate.relation.to.required", `Relation "${relation.id}" is missing a "to" target.`));
3109
+ } else if (!objectMap.has(relation.to)) {
3110
+ diagnostics.push(error("validate.relation.to.unknown", `Unknown relation target "${relation.to}" on "${relation.id}".`));
3111
+ }
3112
+ if (!relation.kind) {
3113
+ diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
3114
+ }
3115
+ }
3116
+ function validateViewpointFilter(filter, groupIds, sourceSchemaVersion, diagnostics, viewpointId) {
3117
+ if (!filter || sourceSchemaVersion !== "2.1") {
3118
+ return;
3119
+ }
3120
+ for (const groupId of filter.groupIds) {
3121
+ if (!groupIds.has(groupId)) {
3122
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`));
3123
+ }
3124
+ }
3125
+ }
3126
+ function validateObject(object, system, objectMap, groupIds, diagnostics) {
3127
+ const placement = object.placement;
3128
+ const orbitPlacement = placement?.mode === "orbit" ? placement : null;
3129
+ const parentObject = placement?.mode === "orbit" ? objectMap.get(placement.target) ?? null : null;
3130
+ if (object.groups) {
3131
+ for (const groupId of object.groups) {
3132
+ if (!groupIds.has(groupId)) {
3133
+ diagnostics.push(warn("validate.group.unknown", `Unknown group "${groupId}" on "${object.id}".`, object.id, "groups"));
3134
+ }
3135
+ }
3136
+ }
3137
+ if (orbitPlacement) {
3138
+ if (!objectMap.has(orbitPlacement.target)) {
3139
+ diagnostics.push(error("validate.orbit.target.unknown", `Unknown placement target "${orbitPlacement.target}" on "${object.id}".`, object.id, "orbit"));
3140
+ }
3141
+ if (orbitPlacement.distance && orbitPlacement.semiMajor) {
3142
+ diagnostics.push(error("validate.orbit.distanceConflict", `Object "${object.id}" cannot declare both "distance" and "semiMajor".`, object.id, "distance"));
3143
+ }
3144
+ if (orbitPlacement.phase && !object.epoch && !system?.epoch) {
3145
+ diagnostics.push(warn("validate.phase.epochMissing", `Object "${object.id}" sets "phase" without an object or system epoch.`, object.id, "phase"));
3146
+ }
3147
+ if (orbitPlacement.inclination && !object.referencePlane && !system?.referencePlane) {
3148
+ diagnostics.push(warn("validate.inclination.referencePlaneMissing", `Object "${object.id}" sets "inclination" without an object or system reference plane.`, object.id, "inclination"));
3149
+ }
3150
+ if (orbitPlacement.period && !massInSolar(parentObject?.properties.mass)) {
3151
+ diagnostics.push(warn("validate.period.massMissing", `Object "${object.id}" sets "period" but its central mass cannot be derived.`, object.id, "period"));
3152
+ }
3153
+ }
3154
+ if (placement?.mode === "surface") {
3155
+ const target = objectMap.get(placement.target);
3156
+ if (!target) {
3157
+ diagnostics.push(error("validate.surface.target.unknown", `Unknown placement target "${placement.target}" on "${object.id}".`, object.id, "surface"));
3158
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
3159
+ diagnostics.push(error("validate.surface.target.invalid", `Surface target "${placement.target}" on "${object.id}" is not surface-capable.`, object.id, "surface"));
3160
+ }
3161
+ }
3162
+ if (placement?.mode === "at") {
3163
+ if (object.type !== "structure" && object.type !== "phenomenon") {
3164
+ diagnostics.push(error("validate.at.objectType", `Only structures and phenomena may use "at" placement; found "${object.type}" on "${object.id}".`, object.id, "at"));
3165
+ }
3166
+ if (!validateAtTarget(object, objectMap, diagnostics)) {
3167
+ diagnostics.push(error("validate.at.target.unknown", `Unknown at-reference target "${placement.target}" on "${object.id}".`, object.id, "at"));
3168
+ }
3169
+ }
3170
+ if (object.resonance) {
3171
+ const target = objectMap.get(object.resonance.targetObjectId);
3172
+ if (!target) {
3173
+ diagnostics.push(error("validate.resonance.target.unknown", `Unknown resonance target "${object.resonance.targetObjectId}" on "${object.id}".`, object.id, "resonance"));
3174
+ } else if (object.placement?.mode !== "orbit" || target.placement?.mode !== "orbit" || object.placement.target !== target.placement.target) {
3175
+ diagnostics.push(warn("validate.resonance.orbitMismatch", `Resonance target "${object.resonance.targetObjectId}" on "${object.id}" does not share a compatible orbital parent.`, object.id, "resonance"));
3176
+ }
3177
+ }
3178
+ for (const rule of object.deriveRules ?? []) {
3179
+ if (rule.field !== "period" || rule.strategy !== "kepler") {
3180
+ diagnostics.push(warn("validate.derive.unsupported", `Unsupported derive rule "${rule.field} ${rule.strategy}" on "${object.id}".`, object.id, "derive"));
3181
+ continue;
3182
+ }
3183
+ const derivedPeriodDays = keplerPeriodDays(object, parentObject);
3184
+ if (derivedPeriodDays === null) {
3185
+ diagnostics.push(warn("validate.derive.inputsMissing", `Object "${object.id}" requests "derive period kepler" but lacks enough input data.`, object.id, "derive"));
3186
+ continue;
3187
+ }
3188
+ if (!orbitPlacement?.period) {
3189
+ diagnostics.push(info("validate.derive.period.available", `Object "${object.id}" can derive a Kepler period of ${formatDays(derivedPeriodDays)}.`, object.id, "derive"));
3190
+ }
3191
+ }
3192
+ for (const rule of object.validationRules ?? []) {
3193
+ if (rule.rule !== "kepler") {
3194
+ diagnostics.push(warn("validate.rule.unsupported", `Unsupported validation rule "${rule.rule}" on "${object.id}".`, object.id, "validate"));
3195
+ continue;
3196
+ }
3197
+ const actualPeriodDays = durationInDays(orbitPlacement?.period);
3198
+ const derivedPeriodDays = keplerPeriodDays(object, parentObject);
3199
+ if (actualPeriodDays === null || derivedPeriodDays === null) {
3200
+ continue;
3201
+ }
3202
+ const toleranceDays = toleranceForField(object, "period");
3203
+ if (Math.abs(actualPeriodDays - derivedPeriodDays) > toleranceDays) {
3204
+ diagnostics.push(error("validate.kepler.mismatch", `Object "${object.id}" fails Kepler validation for "period".`, object.id, "validate"));
3205
+ }
3206
+ }
3207
+ }
3208
+ function validateAtTarget(object, objectMap, diagnostics) {
3209
+ const reference = object.placement?.mode === "at" ? object.placement.reference : null;
3210
+ if (!reference) {
3211
+ return true;
3212
+ }
3213
+ if (reference.kind === "named") {
3214
+ return objectMap.has(reference.name);
3215
+ }
3216
+ if (reference.kind === "anchor") {
3217
+ if (!objectMap.has(reference.objectId)) {
3218
+ diagnostics.push(error("validate.anchor.target.unknown", `Unknown anchor target "${reference.objectId}" on "${object.id}".`, object.id, "at"));
3219
+ return false;
3220
+ }
3221
+ return true;
3222
+ }
3223
+ if (!objectMap.has(reference.primary)) {
3224
+ diagnostics.push(error("validate.lagrange.primary.unknown", `Unknown Lagrange reference "${reference.primary}" on "${object.id}".`, object.id, "at"));
3225
+ return false;
3226
+ }
3227
+ if (reference.secondary && !objectMap.has(reference.secondary)) {
3228
+ diagnostics.push(error("validate.lagrange.secondary.unknown", `Unknown Lagrange reference "${reference.secondary}" on "${object.id}".`, object.id, "at"));
3229
+ return false;
3230
+ }
3231
+ return true;
3232
+ }
3233
+ function keplerPeriodDays(object, parentObject) {
3234
+ const placement = object.placement;
3235
+ if (!placement || placement.mode !== "orbit") {
3236
+ return null;
3237
+ }
3238
+ const semiMajorAu = distanceInAu(placement.semiMajor ?? placement.distance);
3239
+ const centralMassSolar = massInSolar(parentObject?.properties.mass);
3240
+ if (semiMajorAu === null || centralMassSolar === null || centralMassSolar <= 0) {
3241
+ return null;
3242
+ }
3243
+ const periodYears = Math.sqrt(semiMajorAu ** 3 / centralMassSolar);
3244
+ return periodYears * 365.25;
3245
+ }
3246
+ function distanceInAu(value) {
3247
+ if (!value)
3248
+ return null;
3249
+ switch (value.unit) {
3250
+ case null:
3251
+ case "au":
3252
+ return value.value;
3253
+ case "km":
3254
+ return value.value / AU_IN_KM2;
3255
+ case "m":
3256
+ return value.value / (AU_IN_KM2 * 1e3);
3257
+ case "ly":
3258
+ return value.value * LY_IN_AU2;
3259
+ case "pc":
3260
+ return value.value * PC_IN_AU2;
3261
+ case "kpc":
3262
+ return value.value * KPC_IN_AU2;
3263
+ case "re":
3264
+ return value.value * EARTH_RADIUS_IN_KM2 / AU_IN_KM2;
3265
+ case "sol":
3266
+ return value.value * SOLAR_RADIUS_IN_KM2 / AU_IN_KM2;
3267
+ default:
3268
+ return null;
3269
+ }
3270
+ }
3271
+ function massInSolar(value) {
3272
+ if (!value || typeof value !== "object" || !("value" in value)) {
3273
+ return null;
3274
+ }
3275
+ const unitValue = value;
3276
+ switch (unitValue.unit) {
3277
+ case null:
3278
+ case "sol":
3279
+ return unitValue.value;
3280
+ case "me":
3281
+ return unitValue.value / EARTH_MASSES_PER_SOLAR;
3282
+ case "mj":
3283
+ return unitValue.value / JUPITER_MASSES_PER_SOLAR;
3284
+ default:
3285
+ return null;
3286
+ }
3287
+ }
3288
+ function durationInDays(value) {
3289
+ if (!value)
3290
+ return null;
3291
+ switch (value.unit) {
3292
+ case null:
3293
+ case "d":
3294
+ return value.value;
3295
+ case "s":
3296
+ return value.value / 86400;
3297
+ case "min":
3298
+ return value.value / 1440;
3299
+ case "h":
3300
+ return value.value / 24;
3301
+ case "y":
3302
+ return value.value * 365.25;
3303
+ case "ky":
3304
+ return value.value * 365250;
3305
+ case "my":
3306
+ return value.value * 36525e4;
3307
+ case "gy":
3308
+ return value.value * 36525e7;
3309
+ default:
3310
+ return null;
3311
+ }
3312
+ }
3313
+ function toleranceForField(object, field) {
3314
+ const tolerance = object.tolerances?.find((entry) => entry.field === field)?.value;
3315
+ if (typeof tolerance === "number") {
3316
+ return tolerance;
3317
+ }
3318
+ if (tolerance && typeof tolerance === "object" && "value" in tolerance) {
3319
+ return durationInDays(tolerance) ?? 0;
3320
+ }
3321
+ return 0;
3322
+ }
3323
+ function formatDays(days) {
3324
+ return `${Math.round(days * 100) / 100}d`;
3325
+ }
3326
+ function error(code, message, objectId, field) {
3327
+ return { code, severity: "error", source: "validate", message, objectId, field };
3328
+ }
3329
+ function warn(code, message, objectId, field) {
3330
+ return { code, severity: "warning", source: "validate", message, objectId, field };
3331
+ }
3332
+ function info(code, message, objectId, field) {
3333
+ return { code, severity: "info", source: "validate", message, objectId, field };
3334
+ }
3335
+
2790
3336
  // packages/core/dist/draft-parse.js
3337
+ var STRUCTURED_TYPED_BLOCKS = /* @__PURE__ */ new Set([
3338
+ "climate",
3339
+ "habitability",
3340
+ "settlement"
3341
+ ]);
3342
+ var DRAFT_OBJECT_FIELD_SPECS = /* @__PURE__ */ new Map();
3343
+ for (const key of [
3344
+ "orbit",
3345
+ "distance",
3346
+ "semiMajor",
3347
+ "eccentricity",
3348
+ "period",
3349
+ "angle",
3350
+ "inclination",
3351
+ "phase",
3352
+ "at",
3353
+ "surface",
3354
+ "free",
3355
+ "kind",
3356
+ "class",
3357
+ "culture",
3358
+ "tags",
3359
+ "color",
3360
+ "image",
3361
+ "hidden",
3362
+ "radius",
3363
+ "mass",
3364
+ "density",
3365
+ "gravity",
3366
+ "temperature",
3367
+ "albedo",
3368
+ "atmosphere",
3369
+ "inner",
3370
+ "outer",
3371
+ "on",
3372
+ "source",
3373
+ "cycle"
3374
+ ]) {
3375
+ const schema = getFieldSchema(key);
3376
+ if (schema) {
3377
+ DRAFT_OBJECT_FIELD_SPECS.set(key, {
3378
+ key,
3379
+ version: "2.0",
3380
+ inlineMode: schema.arity === "multiple" ? "multiple" : "single",
3381
+ allowRepeat: false,
3382
+ legacySchema: schema
3383
+ });
3384
+ }
3385
+ }
3386
+ for (const spec of [
3387
+ { key: "groups", inlineMode: "multiple", allowRepeat: false },
3388
+ { key: "epoch", inlineMode: "single", allowRepeat: false },
3389
+ { key: "referencePlane", inlineMode: "single", allowRepeat: false },
3390
+ { key: "tidalLock", inlineMode: "single", allowRepeat: false },
3391
+ { key: "renderLabel", inlineMode: "single", allowRepeat: false },
3392
+ { key: "renderOrbit", inlineMode: "single", allowRepeat: false },
3393
+ { key: "renderPriority", inlineMode: "single", allowRepeat: false },
3394
+ { key: "resonance", inlineMode: "pair", allowRepeat: false },
3395
+ { key: "derive", inlineMode: "pair", allowRepeat: true },
3396
+ { key: "validate", inlineMode: "single", allowRepeat: true },
3397
+ { key: "locked", inlineMode: "multiple", allowRepeat: false },
3398
+ { key: "tolerance", inlineMode: "pair", allowRepeat: true }
3399
+ ]) {
3400
+ DRAFT_OBJECT_FIELD_SPECS.set(spec.key, {
3401
+ key: spec.key,
3402
+ version: "2.1",
3403
+ inlineMode: spec.inlineMode,
3404
+ allowRepeat: spec.allowRepeat
3405
+ });
3406
+ }
3407
+ var DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
2791
3408
  function parseWorldOrbitAtlas(source) {
2792
- return parseAtlasSource(source, "2.0");
3409
+ return parseAtlasSource(source);
2793
3410
  }
2794
- function parseAtlasSource(source, outputVersion) {
2795
- const lines = source.split(/\r?\n/);
3411
+ function parseAtlasSource(source, forcedOutputVersion) {
3412
+ const prepared = preprocessAtlasSource(source);
3413
+ const lines = prepared.source.split(/\r?\n/);
3414
+ const diagnostics = [];
2796
3415
  let sawSchemaHeader = false;
2797
- let schemaVersion = "2.0";
3416
+ let sourceSchemaVersion = "2.0";
2798
3417
  let system = null;
2799
3418
  let section = null;
2800
3419
  const objectNodes = [];
3420
+ const groups = [];
3421
+ const relations = [];
2801
3422
  let sawDefaults = false;
2802
3423
  let sawAtlas = false;
2803
3424
  const viewpointIds = /* @__PURE__ */ new Set();
2804
3425
  const annotationIds = /* @__PURE__ */ new Set();
3426
+ const groupIds = /* @__PURE__ */ new Set();
3427
+ const relationIds = /* @__PURE__ */ new Set();
2805
3428
  for (let index = 0; index < lines.length; index++) {
2806
3429
  const rawLine = lines[index];
2807
3430
  const lineNumber = index + 1;
@@ -2817,15 +3440,22 @@ var WorldOrbit = (() => {
2817
3440
  continue;
2818
3441
  }
2819
3442
  if (!sawSchemaHeader) {
2820
- schemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
3443
+ sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
2821
3444
  sawSchemaHeader = true;
3445
+ if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
3446
+ diagnostics.push({
3447
+ code: "parse.schema21.commentCompatibility",
3448
+ severity: "warning",
3449
+ source: "parse",
3450
+ message: `Comments require schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
3451
+ line: prepared.comments[0].line,
3452
+ column: prepared.comments[0].column
3453
+ });
3454
+ }
2822
3455
  continue;
2823
3456
  }
2824
3457
  if (indent === 0) {
2825
- section = startTopLevelSection(tokens, lineNumber, system, objectNodes, viewpointIds, annotationIds, {
2826
- sawDefaults,
2827
- sawAtlas
2828
- });
3458
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, { sawDefaults, sawAtlas });
2829
3459
  if (section.kind === "system") {
2830
3460
  system = section.system;
2831
3461
  } else if (section.kind === "defaults") {
@@ -2843,48 +3473,57 @@ var WorldOrbit = (() => {
2843
3473
  if (!sawSchemaHeader) {
2844
3474
  throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
2845
3475
  }
2846
- const ast = {
2847
- type: "document",
2848
- objects: objectNodes
2849
- };
2850
- const normalizedObjects = normalizeDocument(ast).objects;
2851
- validateDocument({
2852
- format: "worldorbit",
2853
- version: "1.0",
2854
- system: null,
2855
- objects: normalizedObjects
2856
- });
2857
- const diagnostics = schemaVersion === "2.0-draft" && outputVersion === "2.0" ? [
2858
- {
2859
- code: "load.schema.deprecatedDraft",
2860
- severity: "warning",
2861
- source: "upgrade",
2862
- message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".'
2863
- }
2864
- ] : [];
2865
- return {
3476
+ const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
3477
+ const outputVersion = forcedOutputVersion ?? (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
3478
+ const baseDocument = {
2866
3479
  format: "worldorbit",
2867
- version: outputVersion,
2868
3480
  sourceVersion: "1.0",
2869
3481
  system,
2870
- objects: normalizedObjects,
3482
+ groups,
3483
+ relations,
3484
+ objects,
2871
3485
  diagnostics
2872
3486
  };
3487
+ if (outputVersion === "2.0-draft") {
3488
+ const document3 = {
3489
+ ...baseDocument,
3490
+ version: "2.0-draft",
3491
+ schemaVersion: "2.0-draft"
3492
+ };
3493
+ document3.diagnostics.push(...collectAtlasDiagnostics(document3, sourceSchemaVersion));
3494
+ return document3;
3495
+ }
3496
+ const document2 = {
3497
+ ...baseDocument,
3498
+ version: outputVersion,
3499
+ schemaVersion: outputVersion
3500
+ };
3501
+ if (sourceSchemaVersion === "2.0-draft") {
3502
+ document2.diagnostics.push({
3503
+ code: "load.schema.deprecatedDraft",
3504
+ severity: "warning",
3505
+ source: "upgrade",
3506
+ message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".'
3507
+ });
3508
+ }
3509
+ document2.diagnostics.push(...collectAtlasDiagnostics(document2, sourceSchemaVersion));
3510
+ return document2;
2873
3511
  }
2874
3512
  function assertDraftSchemaHeader(tokens, line) {
2875
- if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || tokens[1].value.toLowerCase() !== "2.0-draft" && tokens[1].value.toLowerCase() !== "2.0") {
2876
- throw new WorldOrbitError('Expected atlas header "schema 2.0" or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
3513
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
3514
+ throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
2877
3515
  }
2878
- return tokens[1].value.toLowerCase() === "2.0-draft" ? "2.0-draft" : "2.0";
3516
+ const version = tokens[1].value.toLowerCase();
3517
+ return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
2879
3518
  }
2880
- function startTopLevelSection(tokens, line, system, objectNodes, viewpointIds, annotationIds, flags) {
3519
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, flags) {
2881
3520
  const keyword = tokens[0]?.value.toLowerCase();
2882
3521
  switch (keyword) {
2883
3522
  case "system":
2884
3523
  if (system) {
2885
3524
  throw new WorldOrbitError('Atlas section "system" may only appear once', line, tokens[0].column);
2886
3525
  }
2887
- return startSystemSection(tokens, line);
3526
+ return startSystemSection(tokens, line, sourceSchemaVersion, diagnostics);
2888
3527
  case "defaults":
2889
3528
  if (!system) {
2890
3529
  throw new WorldOrbitError('Atlas section "defaults" requires a preceding system declaration', line, tokens[0].column);
@@ -2920,13 +3559,19 @@ var WorldOrbit = (() => {
2920
3559
  throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
2921
3560
  }
2922
3561
  return startAnnotationSection(tokens, line, system, annotationIds);
3562
+ case "group":
3563
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "group", { line, column: tokens[0].column });
3564
+ return startGroupSection(tokens, line, groups, groupIds);
3565
+ case "relation":
3566
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
3567
+ return startRelationSection(tokens, line, relations, relationIds);
2923
3568
  case "object":
2924
- return startObjectSection(tokens, line, objectNodes);
3569
+ return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
2925
3570
  default:
2926
3571
  throw new WorldOrbitError(`Unknown atlas section "${tokens[0]?.value ?? ""}"`, line, tokens[0]?.column ?? 1);
2927
3572
  }
2928
3573
  }
2929
- function startSystemSection(tokens, line) {
3574
+ function startSystemSection(tokens, line, sourceSchemaVersion, diagnostics) {
2930
3575
  if (tokens.length !== 2) {
2931
3576
  throw new WorldOrbitError("Invalid atlas system declaration", line, tokens[0]?.column ?? 1);
2932
3577
  }
@@ -2934,6 +3579,9 @@ var WorldOrbit = (() => {
2934
3579
  type: "system",
2935
3580
  id: tokens[1].value,
2936
3581
  title: null,
3582
+ description: null,
3583
+ epoch: null,
3584
+ referencePlane: null,
2937
3585
  defaults: {
2938
3586
  view: "topdown",
2939
3587
  scale: null,
@@ -2948,6 +3596,8 @@ var WorldOrbit = (() => {
2948
3596
  return {
2949
3597
  kind: "system",
2950
3598
  system,
3599
+ sourceSchemaVersion,
3600
+ diagnostics,
2951
3601
  seenFields: /* @__PURE__ */ new Set()
2952
3602
  };
2953
3603
  }
@@ -3013,7 +3663,64 @@ var WorldOrbit = (() => {
3013
3663
  seenFields: /* @__PURE__ */ new Set()
3014
3664
  };
3015
3665
  }
3016
- function startObjectSection(tokens, line, objectNodes) {
3666
+ function startGroupSection(tokens, line, groups, groupIds) {
3667
+ if (tokens.length !== 2) {
3668
+ throw new WorldOrbitError("Invalid group declaration", line, tokens[0]?.column ?? 1);
3669
+ }
3670
+ const id = normalizeIdentifier(tokens[1].value);
3671
+ if (!id) {
3672
+ throw new WorldOrbitError("Group id must not be empty", line, tokens[1].column);
3673
+ }
3674
+ if (groupIds.has(id)) {
3675
+ throw new WorldOrbitError(`Duplicate group id "${id}"`, line, tokens[1].column);
3676
+ }
3677
+ const group = {
3678
+ id,
3679
+ label: humanizeIdentifier2(id),
3680
+ summary: "",
3681
+ color: null,
3682
+ tags: [],
3683
+ hidden: false
3684
+ };
3685
+ groups.push(group);
3686
+ groupIds.add(id);
3687
+ return {
3688
+ kind: "group",
3689
+ group,
3690
+ seenFields: /* @__PURE__ */ new Set()
3691
+ };
3692
+ }
3693
+ function startRelationSection(tokens, line, relations, relationIds) {
3694
+ if (tokens.length !== 2) {
3695
+ throw new WorldOrbitError("Invalid relation declaration", line, tokens[0]?.column ?? 1);
3696
+ }
3697
+ const id = normalizeIdentifier(tokens[1].value);
3698
+ if (!id) {
3699
+ throw new WorldOrbitError("Relation id must not be empty", line, tokens[1].column);
3700
+ }
3701
+ if (relationIds.has(id)) {
3702
+ throw new WorldOrbitError(`Duplicate relation id "${id}"`, line, tokens[1].column);
3703
+ }
3704
+ const relation = {
3705
+ id,
3706
+ from: "",
3707
+ to: "",
3708
+ kind: "",
3709
+ label: null,
3710
+ summary: null,
3711
+ tags: [],
3712
+ color: null,
3713
+ hidden: false
3714
+ };
3715
+ relations.push(relation);
3716
+ relationIds.add(id);
3717
+ return {
3718
+ kind: "relation",
3719
+ relation,
3720
+ seenFields: /* @__PURE__ */ new Set()
3721
+ };
3722
+ }
3723
+ function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
3017
3724
  if (tokens.length < 3) {
3018
3725
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
3019
3726
  }
@@ -3024,12 +3731,11 @@ var WorldOrbit = (() => {
3024
3731
  throw new WorldOrbitError(`Unknown object type "${objectTypeToken.value}"`, line, objectTypeToken.column);
3025
3732
  }
3026
3733
  const objectNode = {
3027
- type: "object",
3028
3734
  objectType,
3029
- name: idToken.value,
3030
- inlineFields: parseInlineFields2(tokens.slice(3), line),
3031
- blockFields: [],
3735
+ id: idToken.value,
3736
+ fields: parseInlineObjectFields(tokens.slice(3), line, objectType, sourceSchemaVersion, diagnostics),
3032
3737
  infoEntries: [],
3738
+ typedBlockEntries: {},
3033
3739
  location: {
3034
3740
  line,
3035
3741
  column: objectTypeToken.column
@@ -3039,8 +3745,12 @@ var WorldOrbit = (() => {
3039
3745
  return {
3040
3746
  kind: "object",
3041
3747
  objectNode,
3042
- inInfoBlock: false,
3043
- infoIndent: null
3748
+ sourceSchemaVersion,
3749
+ diagnostics,
3750
+ activeBlock: null,
3751
+ blockIndent: null,
3752
+ seenInfoKeys: /* @__PURE__ */ new Set(),
3753
+ seenTypedBlockKeys: {}
3044
3754
  };
3045
3755
  }
3046
3756
  function handleSectionLine(section, indent, tokens, line) {
@@ -3060,6 +3770,12 @@ var WorldOrbit = (() => {
3060
3770
  case "annotation":
3061
3771
  applyAnnotationField(section, tokens, line);
3062
3772
  return;
3773
+ case "group":
3774
+ applyGroupField(section, tokens, line);
3775
+ return;
3776
+ case "relation":
3777
+ applyRelationField(section, tokens, line);
3778
+ return;
3063
3779
  case "object":
3064
3780
  applyObjectField(section, indent, tokens, line);
3065
3781
  return;
@@ -3067,10 +3783,35 @@ var WorldOrbit = (() => {
3067
3783
  }
3068
3784
  function applySystemField(section, tokens, line) {
3069
3785
  const key = requireUniqueField(tokens, section.seenFields, line);
3070
- if (key !== "title") {
3071
- throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
3786
+ const value = joinFieldValue(tokens, line);
3787
+ switch (key) {
3788
+ case "title":
3789
+ section.system.title = value;
3790
+ return;
3791
+ case "description":
3792
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
3793
+ line,
3794
+ column: tokens[0].column
3795
+ });
3796
+ section.system.description = value;
3797
+ return;
3798
+ case "epoch":
3799
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
3800
+ line,
3801
+ column: tokens[0].column
3802
+ });
3803
+ section.system.epoch = value;
3804
+ return;
3805
+ case "referenceplane":
3806
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "referencePlane", {
3807
+ line,
3808
+ column: tokens[0].column
3809
+ });
3810
+ section.system.referencePlane = value;
3811
+ return;
3812
+ default:
3813
+ throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
3072
3814
  }
3073
- section.system.title = joinFieldValue(tokens, line);
3074
3815
  }
3075
3816
  function applyDefaultsField(section, tokens, line) {
3076
3817
  const key = requireUniqueField(tokens, section.seenFields, line);
@@ -3101,14 +3842,11 @@ var WorldOrbit = (() => {
3101
3842
  section.metadataIndent = null;
3102
3843
  }
3103
3844
  if (section.inMetadata) {
3104
- if (tokens.length < 2) {
3105
- throw new WorldOrbitError("Invalid atlas metadata entry", line, tokens[0]?.column ?? 1);
3845
+ const entry = parseInfoLikeEntry(tokens, line, "Invalid atlas metadata entry");
3846
+ if (entry.key in section.system.atlasMetadata) {
3847
+ throw new WorldOrbitError(`Duplicate atlas metadata key "${entry.key}"`, line, tokens[0].column);
3106
3848
  }
3107
- const key = tokens[0].value;
3108
- if (key in section.system.atlasMetadata) {
3109
- throw new WorldOrbitError(`Duplicate atlas metadata key "${key}"`, line, tokens[0].column);
3110
- }
3111
- section.system.atlasMetadata[key] = joinFieldValue(tokens, line);
3849
+ section.system.atlasMetadata[entry.key] = entry.value;
3112
3850
  return;
3113
3851
  }
3114
3852
  if (tokens.length === 1 && tokens[0].value.toLowerCase() === "metadata") {
@@ -3187,44 +3925,125 @@ var WorldOrbit = (() => {
3187
3925
  filter.groupIds = parseTokenList(tokens.slice(1), line, "groups");
3188
3926
  break;
3189
3927
  default:
3190
- throw new WorldOrbitError(`Unknown viewpoint filter field "${tokens[0].value}"`, line, tokens[0].column);
3928
+ throw new WorldOrbitError(`Unknown viewpoint filter field "${tokens[0].value}"`, line, tokens[0].column);
3929
+ }
3930
+ section.viewpoint.filter = filter;
3931
+ }
3932
+ function applyAnnotationField(section, tokens, line) {
3933
+ const key = requireUniqueField(tokens, section.seenFields, line);
3934
+ switch (key) {
3935
+ case "label":
3936
+ section.annotation.label = joinFieldValue(tokens, line);
3937
+ return;
3938
+ case "target":
3939
+ section.annotation.targetObjectId = joinFieldValue(tokens, line);
3940
+ return;
3941
+ case "body":
3942
+ section.annotation.body = joinFieldValue(tokens, line);
3943
+ return;
3944
+ case "tags":
3945
+ section.annotation.tags = parseTokenList(tokens.slice(1), line, "tags");
3946
+ return;
3947
+ default:
3948
+ throw new WorldOrbitError(`Unknown annotation field "${tokens[0].value}"`, line, tokens[0].column);
3949
+ }
3950
+ }
3951
+ function applyGroupField(section, tokens, line) {
3952
+ const key = requireUniqueField(tokens, section.seenFields, line);
3953
+ switch (key) {
3954
+ case "label":
3955
+ section.group.label = joinFieldValue(tokens, line);
3956
+ return;
3957
+ case "summary":
3958
+ section.group.summary = joinFieldValue(tokens, line);
3959
+ return;
3960
+ case "color":
3961
+ section.group.color = joinFieldValue(tokens, line);
3962
+ return;
3963
+ case "tags":
3964
+ section.group.tags = parseTokenList(tokens.slice(1), line, "tags");
3965
+ return;
3966
+ case "hidden":
3967
+ section.group.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
3968
+ line,
3969
+ column: tokens[0].column
3970
+ });
3971
+ return;
3972
+ default:
3973
+ throw new WorldOrbitError(`Unknown group field "${tokens[0].value}"`, line, tokens[0].column);
3191
3974
  }
3192
- section.viewpoint.filter = filter;
3193
3975
  }
3194
- function applyAnnotationField(section, tokens, line) {
3976
+ function applyRelationField(section, tokens, line) {
3195
3977
  const key = requireUniqueField(tokens, section.seenFields, line);
3196
3978
  switch (key) {
3197
- case "label":
3198
- section.annotation.label = joinFieldValue(tokens, line);
3979
+ case "from":
3980
+ section.relation.from = joinFieldValue(tokens, line);
3199
3981
  return;
3200
- case "target":
3201
- section.annotation.targetObjectId = joinFieldValue(tokens, line);
3982
+ case "to":
3983
+ section.relation.to = joinFieldValue(tokens, line);
3202
3984
  return;
3203
- case "body":
3204
- section.annotation.body = joinFieldValue(tokens, line);
3985
+ case "kind":
3986
+ section.relation.kind = joinFieldValue(tokens, line);
3987
+ return;
3988
+ case "label":
3989
+ section.relation.label = joinFieldValue(tokens, line);
3990
+ return;
3991
+ case "summary":
3992
+ section.relation.summary = joinFieldValue(tokens, line);
3205
3993
  return;
3206
3994
  case "tags":
3207
- section.annotation.tags = parseTokenList(tokens.slice(1), line, "tags");
3995
+ section.relation.tags = parseTokenList(tokens.slice(1), line, "tags");
3996
+ return;
3997
+ case "color":
3998
+ section.relation.color = joinFieldValue(tokens, line);
3999
+ return;
4000
+ case "hidden":
4001
+ section.relation.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4002
+ line,
4003
+ column: tokens[0].column
4004
+ });
3208
4005
  return;
3209
4006
  default:
3210
- throw new WorldOrbitError(`Unknown annotation field "${tokens[0].value}"`, line, tokens[0].column);
4007
+ throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
3211
4008
  }
3212
4009
  }
3213
4010
  function applyObjectField(section, indent, tokens, line) {
3214
- if (tokens.length === 1 && tokens[0].value === "info") {
3215
- section.inInfoBlock = true;
3216
- section.infoIndent = indent;
3217
- return;
3218
- }
3219
- if (section.inInfoBlock && indent <= (section.infoIndent ?? 0)) {
3220
- section.inInfoBlock = false;
3221
- section.infoIndent = null;
4011
+ if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
4012
+ section.activeBlock = null;
4013
+ section.blockIndent = null;
4014
+ }
4015
+ if (tokens.length === 1) {
4016
+ const blockName = tokens[0].value.toLowerCase();
4017
+ if (blockName === "info" || STRUCTURED_TYPED_BLOCKS.has(blockName)) {
4018
+ if (blockName !== "info") {
4019
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, blockName, { line, column: tokens[0].column });
4020
+ }
4021
+ section.activeBlock = blockName;
4022
+ section.blockIndent = indent;
4023
+ return;
4024
+ }
3222
4025
  }
3223
- if (section.inInfoBlock) {
3224
- section.objectNode.infoEntries.push(parseInfoEntry2(tokens, line));
4026
+ if (section.activeBlock) {
4027
+ const entry = parseInfoLikeEntry(tokens, line, `Invalid ${section.activeBlock} entry`);
4028
+ if (section.activeBlock === "info") {
4029
+ if (section.seenInfoKeys.has(entry.key)) {
4030
+ throw new WorldOrbitError(`Duplicate info key "${entry.key}"`, line, tokens[0].column);
4031
+ }
4032
+ section.seenInfoKeys.add(entry.key);
4033
+ section.objectNode.infoEntries.push(entry);
4034
+ return;
4035
+ }
4036
+ const typedBlock = section.activeBlock;
4037
+ const seenKeys = section.seenTypedBlockKeys[typedBlock] ?? (section.seenTypedBlockKeys[typedBlock] = /* @__PURE__ */ new Set());
4038
+ if (seenKeys.has(entry.key)) {
4039
+ throw new WorldOrbitError(`Duplicate ${typedBlock} key "${entry.key}"`, line, tokens[0].column);
4040
+ }
4041
+ seenKeys.add(entry.key);
4042
+ const entries = section.objectNode.typedBlockEntries[typedBlock] ?? (section.objectNode.typedBlockEntries[typedBlock] = []);
4043
+ entries.push(entry);
3225
4044
  return;
3226
4045
  }
3227
- section.objectNode.blockFields.push(parseField2(tokens, line));
4046
+ section.objectNode.fields.push(parseObjectField(tokens, line, section.objectNode.objectType, section.sourceSchemaVersion, section.diagnostics));
3228
4047
  }
3229
4048
  function requireUniqueField(tokens, seenFields, line) {
3230
4049
  if (tokens.length < 2) {
@@ -3244,50 +4063,40 @@ var WorldOrbit = (() => {
3244
4063
  return tokens.slice(1).map((token) => token.value).join(" ").trim();
3245
4064
  }
3246
4065
  function parseObjectTypeTokens(tokens, line) {
3247
- if (tokens.length === 0) {
3248
- throw new WorldOrbitError("Missing value for atlas field", line);
3249
- }
3250
- return tokens.map((token) => {
3251
- const value = token.value;
3252
- if (value !== "star" && value !== "planet" && value !== "moon" && value !== "belt" && value !== "asteroid" && value !== "comet" && value !== "ring" && value !== "structure" && value !== "phenomenon") {
3253
- throw new WorldOrbitError(`Unknown viewpoint object type "${token.value}"`, line, token.column);
3254
- }
3255
- return value;
3256
- });
3257
- }
3258
- function parseTokenList(tokens, line, field) {
3259
- if (tokens.length === 0) {
3260
- throw new WorldOrbitError(`Missing value for field "${field}"`, line);
3261
- }
3262
- return tokens.map((token) => token.value);
4066
+ 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");
3263
4067
  }
3264
4068
  function parseLayerTokens(tokens, line) {
3265
- if (tokens.length === 0) {
3266
- throw new WorldOrbitError('Missing value for field "layers"', line);
3267
- }
3268
- const next = {};
3269
- for (const token of tokens) {
3270
- const enabled = !token.value.startsWith("-") && !token.value.startsWith("!");
3271
- const rawLayer = token.value.replace(/^[-!]+/, "").toLowerCase();
3272
- if (rawLayer === "orbits") {
3273
- next["orbits-back"] = enabled;
3274
- next["orbits-front"] = enabled;
4069
+ const layers = {};
4070
+ for (const token of parseTokenList(tokens, line, "layers")) {
4071
+ const enabled = !token.startsWith("-") && !token.startsWith("!");
4072
+ const raw = token.replace(/^[-!]+/, "").toLowerCase();
4073
+ if (raw === "orbits") {
4074
+ layers["orbits-back"] = enabled;
4075
+ layers["orbits-front"] = enabled;
3275
4076
  continue;
3276
4077
  }
3277
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
3278
- next[rawLayer] = enabled;
3279
- continue;
4078
+ if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "objects" || raw === "labels" || raw === "metadata") {
4079
+ layers[raw] = enabled;
3280
4080
  }
3281
- throw new WorldOrbitError(`Unknown layer token "${token.value}"`, line, token.column);
3282
4081
  }
3283
- return next;
4082
+ return layers;
4083
+ }
4084
+ function parseTokenList(tokens, line, fieldName) {
4085
+ if (tokens.length === 0) {
4086
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, 1);
4087
+ }
4088
+ const values = tokens.map((token) => token.value).filter(Boolean);
4089
+ if (values.length === 0) {
4090
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, tokens[0]?.column ?? 1);
4091
+ }
4092
+ return values;
3284
4093
  }
3285
4094
  function parseProjectionValue(value, line, column) {
3286
4095
  const normalized = value.toLowerCase();
3287
- if (normalized === "topdown" || normalized === "isometric") {
3288
- return normalized;
4096
+ if (normalized !== "topdown" && normalized !== "isometric") {
4097
+ throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
3289
4098
  }
3290
- throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
4099
+ return normalized;
3291
4100
  }
3292
4101
  function parsePresetValue(value, line, column) {
3293
4102
  const normalized = value.toLowerCase();
@@ -3297,16 +4106,16 @@ var WorldOrbit = (() => {
3297
4106
  throw new WorldOrbitError(`Unknown render preset "${value}"`, line, column);
3298
4107
  }
3299
4108
  function parsePositiveNumber2(value, line, column, field) {
3300
- const parsed = Number(value);
3301
- if (!Number.isFinite(parsed) || parsed <= 0) {
3302
- throw new WorldOrbitError(`Field "${field}" expects a positive number`, line, column);
4109
+ const parsed = parseFiniteNumber2(value, line, column, field);
4110
+ if (parsed <= 0) {
4111
+ throw new WorldOrbitError(`Field "${field}" must be greater than zero`, line, column);
3303
4112
  }
3304
4113
  return parsed;
3305
4114
  }
3306
4115
  function parseFiniteNumber2(value, line, column, field) {
3307
4116
  const parsed = Number(value);
3308
4117
  if (!Number.isFinite(parsed)) {
3309
- throw new WorldOrbitError(`Field "${field}" expects a finite number`, line, column);
4118
+ throw new WorldOrbitError(`Invalid numeric value "${value}" for "${field}"`, line, column);
3310
4119
  }
3311
4120
  return parsed;
3312
4121
  }
@@ -3318,28 +4127,43 @@ var WorldOrbit = (() => {
3318
4127
  groupIds: []
3319
4128
  };
3320
4129
  }
3321
- function parseInlineFields2(tokens, line) {
4130
+ function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
3322
4131
  const fields = [];
3323
4132
  let index = 0;
3324
4133
  while (index < tokens.length) {
3325
4134
  const keyToken = tokens[index];
3326
- const schema = getFieldSchema(keyToken.value);
3327
- if (!schema) {
4135
+ const spec = getDraftObjectFieldSpec(keyToken.value);
4136
+ if (!spec) {
3328
4137
  throw new WorldOrbitError(`Unknown field "${keyToken.value}"`, line, keyToken.column);
3329
4138
  }
4139
+ if (spec.version === "2.1") {
4140
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, keyToken.value, {
4141
+ line,
4142
+ column: keyToken.column
4143
+ });
4144
+ }
3330
4145
  index++;
3331
4146
  const valueTokens = [];
3332
- if (schema.arity === "multiple") {
3333
- while (index < tokens.length && !isKnownFieldKey(tokens[index].value)) {
3334
- valueTokens.push(tokens[index]);
3335
- index++;
3336
- }
3337
- } else {
4147
+ if (spec.inlineMode === "single") {
3338
4148
  const nextToken = tokens[index];
3339
4149
  if (nextToken) {
3340
4150
  valueTokens.push(nextToken);
3341
4151
  index++;
3342
4152
  }
4153
+ } else if (spec.inlineMode === "pair") {
4154
+ for (let count = 0; count < 2; count++) {
4155
+ const nextToken = tokens[index];
4156
+ if (!nextToken) {
4157
+ break;
4158
+ }
4159
+ valueTokens.push(nextToken);
4160
+ index++;
4161
+ }
4162
+ } else {
4163
+ while (index < tokens.length && !DRAFT_OBJECT_FIELD_KEYS.has(tokens[index].value)) {
4164
+ valueTokens.push(tokens[index]);
4165
+ index++;
4166
+ }
3343
4167
  }
3344
4168
  if (valueTokens.length === 0) {
3345
4169
  throw new WorldOrbitError(`Missing value for field "${keyToken.value}"`, line, keyToken.column);
@@ -3351,25 +4175,35 @@ var WorldOrbit = (() => {
3351
4175
  location: { line, column: keyToken.column }
3352
4176
  });
3353
4177
  }
4178
+ validateDraftObjectFieldCompatibility(fields, objectType);
3354
4179
  return fields;
3355
4180
  }
3356
- function parseField2(tokens, line) {
4181
+ function parseObjectField(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
3357
4182
  if (tokens.length < 2) {
3358
4183
  throw new WorldOrbitError("Invalid field line", line, tokens[0]?.column ?? 1);
3359
4184
  }
3360
- if (!getFieldSchema(tokens[0].value)) {
4185
+ const spec = getDraftObjectFieldSpec(tokens[0].value);
4186
+ if (!spec) {
3361
4187
  throw new WorldOrbitError(`Unknown field "${tokens[0].value}"`, line, tokens[0].column);
3362
4188
  }
3363
- return {
4189
+ if (spec.version === "2.1") {
4190
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, tokens[0].value, {
4191
+ line,
4192
+ column: tokens[0].column
4193
+ });
4194
+ }
4195
+ const field = {
3364
4196
  type: "field",
3365
4197
  key: tokens[0].value,
3366
4198
  values: tokens.slice(1).map((token) => token.value),
3367
4199
  location: { line, column: tokens[0].column }
3368
4200
  };
4201
+ validateDraftObjectFieldCompatibility([field], objectType);
4202
+ return field;
3369
4203
  }
3370
- function parseInfoEntry2(tokens, line) {
4204
+ function parseInfoLikeEntry(tokens, line, errorMessage) {
3371
4205
  if (tokens.length < 2) {
3372
- throw new WorldOrbitError("Invalid info entry", line, tokens[0]?.column ?? 1);
4206
+ throw new WorldOrbitError(errorMessage, line, tokens[0]?.column ?? 1);
3373
4207
  }
3374
4208
  return {
3375
4209
  type: "info-entry",
@@ -3378,18 +4212,348 @@ var WorldOrbit = (() => {
3378
4212
  location: { line, column: tokens[0].column }
3379
4213
  };
3380
4214
  }
3381
- function normalizeIdentifier(value) {
3382
- return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
4215
+ function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
4216
+ const fieldMap = collectDraftFields(node.fields);
4217
+ const placement = extractDraftPlacement(node.objectType, fieldMap);
4218
+ const properties = normalizeDraftProperties(node.objectType, fieldMap);
4219
+ const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
4220
+ const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
4221
+ const referencePlane = parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0]);
4222
+ const tidalLock = fieldMap.has("tidalLock") ? parseAtlasBoolean(singleFieldValue2(fieldMap.get("tidalLock")[0]), "tidalLock", fieldMap.get("tidalLock")[0].location) : void 0;
4223
+ const resonance = fieldMap.has("resonance") ? parseResonanceField(fieldMap.get("resonance")[0]) : void 0;
4224
+ const renderHints = extractRenderHints(fieldMap);
4225
+ const deriveRules = fieldMap.get("derive")?.map((field) => parseDeriveField(field));
4226
+ const validationRules = fieldMap.get("validate")?.map((field) => ({
4227
+ rule: singleFieldValue2(field)
4228
+ }));
4229
+ const lockedFields = fieldMap.has("locked") ? [...new Set(fieldMap.get("locked").flatMap((field) => field.values))] : void 0;
4230
+ const tolerances = fieldMap.get("tolerance")?.map((field) => parseToleranceField(field));
4231
+ const typedBlocks = normalizeTypedBlocks(node.typedBlockEntries);
4232
+ const info2 = normalizeInfoEntries(node.infoEntries, "info");
4233
+ const object = {
4234
+ type: node.objectType,
4235
+ id: node.id,
4236
+ properties,
4237
+ placement,
4238
+ info: info2
4239
+ };
4240
+ if (groups.length > 0)
4241
+ object.groups = groups;
4242
+ if (epoch)
4243
+ object.epoch = epoch;
4244
+ if (referencePlane)
4245
+ object.referencePlane = referencePlane;
4246
+ if (tidalLock !== void 0)
4247
+ object.tidalLock = tidalLock;
4248
+ if (resonance)
4249
+ object.resonance = resonance;
4250
+ if (renderHints)
4251
+ object.renderHints = renderHints;
4252
+ if (deriveRules?.length)
4253
+ object.deriveRules = deriveRules;
4254
+ if (validationRules?.length)
4255
+ object.validationRules = validationRules;
4256
+ if (lockedFields?.length)
4257
+ object.lockedFields = lockedFields;
4258
+ if (tolerances?.length)
4259
+ object.tolerances = tolerances;
4260
+ if (typedBlocks && Object.keys(typedBlocks).length > 0)
4261
+ object.typedBlocks = typedBlocks;
4262
+ if (sourceSchemaVersion !== "2.1") {
4263
+ 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) {
4264
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
4265
+ }
4266
+ }
4267
+ return object;
4268
+ }
4269
+ function collectDraftFields(fields) {
4270
+ const grouped = /* @__PURE__ */ new Map();
4271
+ for (const field of fields) {
4272
+ const spec = getDraftObjectFieldSpec(field.key);
4273
+ if (!spec) {
4274
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
4275
+ }
4276
+ if (!spec.allowRepeat && grouped.has(field.key)) {
4277
+ throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
4278
+ }
4279
+ const existing = grouped.get(field.key) ?? [];
4280
+ existing.push(field);
4281
+ grouped.set(field.key, existing);
4282
+ }
4283
+ return grouped;
3383
4284
  }
3384
- function humanizeIdentifier2(value) {
3385
- return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
4285
+ function extractDraftPlacement(objectType, fieldMap) {
4286
+ const orbitField = fieldMap.get("orbit")?.[0];
4287
+ const atField = fieldMap.get("at")?.[0];
4288
+ const surfaceField = fieldMap.get("surface")?.[0];
4289
+ const freeField = fieldMap.get("free")?.[0];
4290
+ const count = [orbitField, atField, surfaceField, freeField].filter(Boolean).length;
4291
+ if (count > 1) {
4292
+ const conflictingField = orbitField ?? atField ?? surfaceField ?? freeField;
4293
+ throw WorldOrbitError.fromLocation("Object has multiple placement modes", conflictingField?.location);
4294
+ }
4295
+ if (orbitField) {
4296
+ return {
4297
+ mode: "orbit",
4298
+ target: singleFieldValue2(orbitField),
4299
+ distance: parseOptionalUnitField(fieldMap.get("distance")?.[0], "distance"),
4300
+ semiMajor: parseOptionalUnitField(fieldMap.get("semiMajor")?.[0], "semiMajor"),
4301
+ eccentricity: parseOptionalNumberField(fieldMap.get("eccentricity")?.[0], "eccentricity"),
4302
+ period: parseOptionalUnitField(fieldMap.get("period")?.[0], "period"),
4303
+ angle: parseOptionalUnitField(fieldMap.get("angle")?.[0], "angle"),
4304
+ inclination: parseOptionalUnitField(fieldMap.get("inclination")?.[0], "inclination"),
4305
+ phase: parseOptionalUnitField(fieldMap.get("phase")?.[0], "phase")
4306
+ };
4307
+ }
4308
+ if (atField) {
4309
+ const target = singleFieldValue2(atField);
4310
+ return {
4311
+ mode: "at",
4312
+ target,
4313
+ reference: parseAtlasAtReference(target, atField.location)
4314
+ };
4315
+ }
4316
+ if (surfaceField) {
4317
+ return {
4318
+ mode: "surface",
4319
+ target: singleFieldValue2(surfaceField)
4320
+ };
4321
+ }
4322
+ if (freeField) {
4323
+ const raw = singleFieldValue2(freeField);
4324
+ const distance = tryParseAtlasUnitValue(raw);
4325
+ return {
4326
+ mode: "free",
4327
+ distance: distance ?? void 0,
4328
+ descriptor: distance ? void 0 : raw
4329
+ };
4330
+ }
4331
+ return null;
4332
+ }
4333
+ function normalizeDraftProperties(objectType, fieldMap) {
4334
+ const properties = {};
4335
+ for (const [key, fields] of fieldMap.entries()) {
4336
+ const field = fields[0];
4337
+ const spec = getDraftObjectFieldSpec(key);
4338
+ if (!field || !spec?.legacySchema || spec.legacySchema.placement) {
4339
+ continue;
4340
+ }
4341
+ ensureAtlasFieldSupported(key, objectType, field.location);
4342
+ properties[key] = normalizeLegacyScalarValue(key, field.values, field.location);
4343
+ }
4344
+ return properties;
4345
+ }
4346
+ function normalizeInfoEntries(entries, label) {
4347
+ const normalized = {};
4348
+ for (const entry of entries) {
4349
+ if (entry.key in normalized) {
4350
+ throw WorldOrbitError.fromLocation(`Duplicate ${label} key "${entry.key}"`, entry.location);
4351
+ }
4352
+ normalized[entry.key] = entry.value;
4353
+ }
4354
+ return normalized;
4355
+ }
4356
+ function normalizeTypedBlocks(typedBlockEntries) {
4357
+ const typedBlocks = {};
4358
+ for (const blockName of Object.keys(typedBlockEntries)) {
4359
+ const entries = typedBlockEntries[blockName];
4360
+ if (entries?.length) {
4361
+ typedBlocks[blockName] = normalizeInfoEntries(entries, blockName);
4362
+ }
4363
+ }
4364
+ return typedBlocks;
4365
+ }
4366
+ function extractRenderHints(fieldMap) {
4367
+ const renderHints = {};
4368
+ const renderLabelField = fieldMap.get("renderLabel")?.[0];
4369
+ const renderOrbitField = fieldMap.get("renderOrbit")?.[0];
4370
+ const renderPriorityField = fieldMap.get("renderPriority")?.[0];
4371
+ if (renderLabelField) {
4372
+ renderHints.renderLabel = parseAtlasBoolean(singleFieldValue2(renderLabelField), "renderLabel", renderLabelField.location);
4373
+ }
4374
+ if (renderOrbitField) {
4375
+ renderHints.renderOrbit = parseAtlasBoolean(singleFieldValue2(renderOrbitField), "renderOrbit", renderOrbitField.location);
4376
+ }
4377
+ if (renderPriorityField) {
4378
+ renderHints.renderPriority = parseAtlasNumber(singleFieldValue2(renderPriorityField), "renderPriority", renderPriorityField.location);
4379
+ }
4380
+ return Object.keys(renderHints).length > 0 ? renderHints : void 0;
4381
+ }
4382
+ function parseResonanceField(field) {
4383
+ if (field.values.length !== 2) {
4384
+ throw WorldOrbitError.fromLocation('Field "resonance" expects "<targetObjectId> <ratio>"', field.location);
4385
+ }
4386
+ const ratio = field.values[1];
4387
+ if (!/^\d+:\d+$/.test(ratio)) {
4388
+ throw WorldOrbitError.fromLocation(`Invalid resonance ratio "${ratio}"`, field.location);
4389
+ }
4390
+ return {
4391
+ targetObjectId: field.values[0],
4392
+ ratio
4393
+ };
4394
+ }
4395
+ function parseDeriveField(field) {
4396
+ if (field.values.length !== 2) {
4397
+ throw WorldOrbitError.fromLocation('Field "derive" expects "<field> <strategy>"', field.location);
4398
+ }
4399
+ return {
4400
+ field: field.values[0],
4401
+ strategy: field.values[1]
4402
+ };
4403
+ }
4404
+ function parseToleranceField(field) {
4405
+ if (field.values.length !== 2) {
4406
+ throw WorldOrbitError.fromLocation('Field "tolerance" expects "<field> <value>"', field.location);
4407
+ }
4408
+ const rawValue = field.values[1];
4409
+ const unitValue = tryParseAtlasUnitValue(rawValue);
4410
+ const numericValue2 = Number(rawValue);
4411
+ return {
4412
+ field: field.values[0],
4413
+ value: unitValue ?? (Number.isFinite(numericValue2) ? numericValue2 : rawValue)
4414
+ };
4415
+ }
4416
+ function parseOptionalTokenList(field) {
4417
+ return field ? [...new Set(field.values)] : [];
4418
+ }
4419
+ function parseOptionalJoinedValue(field) {
4420
+ if (!field) {
4421
+ return null;
4422
+ }
4423
+ return field.values.join(" ").trim() || null;
4424
+ }
4425
+ function parseOptionalUnitField(field, key) {
4426
+ return field ? parseAtlasUnitValue(singleFieldValue2(field), field.location, key) : void 0;
4427
+ }
4428
+ function parseOptionalNumberField(field, key) {
4429
+ return field ? parseAtlasNumber(singleFieldValue2(field), key, field.location) : void 0;
4430
+ }
4431
+ function singleFieldValue2(field) {
4432
+ return singleAtlasValue(field.values, field.key, field.location);
4433
+ }
4434
+ function getDraftObjectFieldSpec(key) {
4435
+ return DRAFT_OBJECT_FIELD_SPECS.get(key);
4436
+ }
4437
+ function validateDraftObjectFieldCompatibility(fields, objectType) {
4438
+ for (const field of fields) {
4439
+ const spec = getDraftObjectFieldSpec(field.key);
4440
+ if (!spec) {
4441
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
4442
+ }
4443
+ if (spec.legacySchema) {
4444
+ ensureAtlasFieldSupported(field.key, objectType, field.location);
4445
+ continue;
4446
+ }
4447
+ if ((field.key === "renderLabel" || field.key === "renderOrbit" || field.key === "tidalLock") && field.values.length !== 1) {
4448
+ throw WorldOrbitError.fromLocation(`Field "${field.key}" expects exactly one value`, field.location);
4449
+ }
4450
+ }
4451
+ }
4452
+ function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
4453
+ if (sourceSchemaVersion === "2.1") {
4454
+ return;
4455
+ }
4456
+ diagnostics.push({
4457
+ code: "parse.schema21.featureCompatibility",
4458
+ severity: "warning",
4459
+ source: "parse",
4460
+ message: `Feature "${featureName}" requires schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
4461
+ line: location.line,
4462
+ column: location.column
4463
+ });
4464
+ }
4465
+ function preprocessAtlasSource(source) {
4466
+ const chars = [...source];
4467
+ const comments = [];
4468
+ let inString = false;
4469
+ let inBlockComment = false;
4470
+ let blockCommentStart = null;
4471
+ let line = 1;
4472
+ let column = 1;
4473
+ for (let index = 0; index < chars.length; index++) {
4474
+ const ch = chars[index];
4475
+ const next = chars[index + 1];
4476
+ if (inBlockComment) {
4477
+ if (ch === "*" && next === "/") {
4478
+ chars[index] = " ";
4479
+ chars[index + 1] = " ";
4480
+ inBlockComment = false;
4481
+ blockCommentStart = null;
4482
+ index++;
4483
+ column += 2;
4484
+ continue;
4485
+ }
4486
+ if (ch !== "\n" && ch !== "\r") {
4487
+ chars[index] = " ";
4488
+ }
4489
+ if (ch === "\n") {
4490
+ line++;
4491
+ column = 1;
4492
+ } else {
4493
+ column++;
4494
+ }
4495
+ continue;
4496
+ }
4497
+ if (!inString && ch === "/" && next === "*") {
4498
+ comments.push({ kind: "block", line, column });
4499
+ chars[index] = " ";
4500
+ chars[index + 1] = " ";
4501
+ inBlockComment = true;
4502
+ blockCommentStart = { line, column };
4503
+ index++;
4504
+ column += 2;
4505
+ continue;
4506
+ }
4507
+ if (!inString && ch === "#" && !isHexColorLiteral(chars, index)) {
4508
+ comments.push({ kind: "line", line, column });
4509
+ chars[index] = " ";
4510
+ let inner = index + 1;
4511
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
4512
+ chars[inner] = " ";
4513
+ inner++;
4514
+ }
4515
+ column += inner - index;
4516
+ index = inner - 1;
4517
+ continue;
4518
+ }
4519
+ if (ch === '"' && chars[index - 1] !== "\\") {
4520
+ inString = !inString;
4521
+ }
4522
+ if (ch === "\n") {
4523
+ line++;
4524
+ column = 1;
4525
+ } else {
4526
+ column++;
4527
+ }
4528
+ }
4529
+ if (inBlockComment) {
4530
+ throw WorldOrbitError.fromLocation("Unclosed block comment", blockCommentStart ?? void 0);
4531
+ }
4532
+ return {
4533
+ source: chars.join(""),
4534
+ comments
4535
+ };
4536
+ }
4537
+ function isHexColorLiteral(chars, start) {
4538
+ let index = start + 1;
4539
+ let length = 0;
4540
+ while (index < chars.length && /[0-9a-f]/i.test(chars[index] ?? "")) {
4541
+ index++;
4542
+ length++;
4543
+ }
4544
+ if (![3, 4, 6, 8].includes(length)) {
4545
+ return false;
4546
+ }
4547
+ const next = chars[index];
4548
+ return next === void 0 || next === " " || next === " " || next === "\r" || next === "\n";
3386
4549
  }
3387
4550
 
3388
4551
  // packages/core/dist/load.js
3389
- var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0)?$/i;
4552
+ var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1)?$/i;
4553
+ var ATLAS_SCHEMA_21_PATTERN = /^schema\s+2\.1$/i;
3390
4554
  var LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
3391
4555
  function detectWorldOrbitSchemaVersion(source) {
3392
- for (const line of source.split(/\r?\n/)) {
4556
+ for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
3393
4557
  const trimmed = line.trim();
3394
4558
  if (!trimmed) {
3395
4559
  continue;
@@ -3397,6 +4561,9 @@ var WorldOrbit = (() => {
3397
4561
  if (LEGACY_DRAFT_SCHEMA_PATTERN.test(trimmed)) {
3398
4562
  return "2.0-draft";
3399
4563
  }
4564
+ if (ATLAS_SCHEMA_21_PATTERN.test(trimmed)) {
4565
+ return "2.1";
4566
+ }
3400
4567
  if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
3401
4568
  return "2.0";
3402
4569
  }
@@ -3404,6 +4571,49 @@ var WorldOrbit = (() => {
3404
4571
  }
3405
4572
  return "1.0";
3406
4573
  }
4574
+ function stripCommentsForSchemaDetection(source) {
4575
+ const chars = [...source];
4576
+ let inString = false;
4577
+ let inBlockComment = false;
4578
+ for (let index = 0; index < chars.length; index++) {
4579
+ const ch = chars[index];
4580
+ const next = chars[index + 1];
4581
+ if (inBlockComment) {
4582
+ if (ch === "*" && next === "/") {
4583
+ chars[index] = " ";
4584
+ chars[index + 1] = " ";
4585
+ inBlockComment = false;
4586
+ index++;
4587
+ continue;
4588
+ }
4589
+ if (ch !== "\n" && ch !== "\r") {
4590
+ chars[index] = " ";
4591
+ }
4592
+ continue;
4593
+ }
4594
+ if (!inString && ch === "/" && next === "*") {
4595
+ chars[index] = " ";
4596
+ chars[index + 1] = " ";
4597
+ inBlockComment = true;
4598
+ index++;
4599
+ continue;
4600
+ }
4601
+ if (!inString && ch === "#") {
4602
+ chars[index] = " ";
4603
+ let inner = index + 1;
4604
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
4605
+ chars[inner] = " ";
4606
+ inner++;
4607
+ }
4608
+ index = inner - 1;
4609
+ continue;
4610
+ }
4611
+ if (ch === '"' && chars[index - 1] !== "\\") {
4612
+ inString = !inString;
4613
+ }
4614
+ }
4615
+ return chars.join("");
4616
+ }
3407
4617
  function loadWorldOrbitSource(source) {
3408
4618
  const result = loadWorldOrbitSourceWithDiagnostics(source);
3409
4619
  if (!result.ok || !result.value) {
@@ -3414,36 +4624,36 @@ var WorldOrbit = (() => {
3414
4624
  }
3415
4625
  function loadWorldOrbitSourceWithDiagnostics(source) {
3416
4626
  const schemaVersion = detectWorldOrbitSchemaVersion(source);
3417
- if (schemaVersion === "2.0" || schemaVersion === "2.0-draft") {
4627
+ if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1") {
3418
4628
  return loadAtlasSourceWithDiagnostics(source, schemaVersion);
3419
4629
  }
3420
4630
  let ast;
3421
4631
  try {
3422
4632
  ast = parseWorldOrbit(source);
3423
- } catch (error) {
4633
+ } catch (error2) {
3424
4634
  return {
3425
4635
  ok: false,
3426
4636
  value: null,
3427
- diagnostics: [diagnosticFromError(error, "parse")]
4637
+ diagnostics: [diagnosticFromError(error2, "parse")]
3428
4638
  };
3429
4639
  }
3430
4640
  let document2;
3431
4641
  try {
3432
4642
  document2 = normalizeDocument(ast);
3433
- } catch (error) {
4643
+ } catch (error2) {
3434
4644
  return {
3435
4645
  ok: false,
3436
4646
  value: null,
3437
- diagnostics: [diagnosticFromError(error, "normalize")]
4647
+ diagnostics: [diagnosticFromError(error2, "normalize")]
3438
4648
  };
3439
4649
  }
3440
4650
  try {
3441
4651
  validateDocument(document2);
3442
- } catch (error) {
4652
+ } catch (error2) {
3443
4653
  return {
3444
4654
  ok: false,
3445
4655
  value: null,
3446
- diagnostics: [diagnosticFromError(error, "validate")]
4656
+ diagnostics: [diagnosticFromError(error2, "validate")]
3447
4657
  };
3448
4658
  }
3449
4659
  return {
@@ -3463,30 +4673,29 @@ var WorldOrbit = (() => {
3463
4673
  let atlasDocument;
3464
4674
  try {
3465
4675
  atlasDocument = parseWorldOrbitAtlas(source);
3466
- } catch (error) {
4676
+ } catch (error2) {
3467
4677
  return {
3468
4678
  ok: false,
3469
4679
  value: null,
3470
- diagnostics: [diagnosticFromError(error, "parse", "load.atlas.failed")]
4680
+ diagnostics: [diagnosticFromError(error2, "parse", "load.atlas.failed")]
3471
4681
  };
3472
4682
  }
3473
- let document2;
3474
- try {
3475
- document2 = materializeAtlasDocument(atlasDocument);
3476
- } catch (error) {
4683
+ const atlasDiagnostics = [...atlasDocument.diagnostics];
4684
+ if (atlasDiagnostics.some((diagnostic) => diagnostic.severity === "error")) {
3477
4685
  return {
3478
4686
  ok: false,
3479
4687
  value: null,
3480
- diagnostics: [diagnosticFromError(error, "normalize", "load.atlas.materialize.failed")]
4688
+ diagnostics: atlasDiagnostics
3481
4689
  };
3482
4690
  }
4691
+ let document2;
3483
4692
  try {
3484
- validateDocument(document2);
3485
- } catch (error) {
4693
+ document2 = materializeAtlasDocument(atlasDocument);
4694
+ } catch (error2) {
3486
4695
  return {
3487
4696
  ok: false,
3488
4697
  value: null,
3489
- diagnostics: [diagnosticFromError(error, "validate", "load.atlas.validate.failed")]
4698
+ diagnostics: [diagnosticFromError(error2, "normalize", "load.atlas.materialize.failed")]
3490
4699
  };
3491
4700
  }
3492
4701
  const loaded = {
@@ -3495,12 +4704,12 @@ var WorldOrbit = (() => {
3495
4704
  document: document2,
3496
4705
  atlasDocument,
3497
4706
  draftDocument: atlasDocument,
3498
- diagnostics: [...atlasDocument.diagnostics]
4707
+ diagnostics: atlasDiagnostics
3499
4708
  };
3500
4709
  return {
3501
4710
  ok: true,
3502
4711
  value: loaded,
3503
- diagnostics: [...atlasDocument.diagnostics]
4712
+ diagnostics: atlasDiagnostics
3504
4713
  };
3505
4714
  }
3506
4715
 
@@ -3683,6 +4892,7 @@ var WorldOrbit = (() => {
3683
4892
  const imageDefinitions = buildImageDefinitions(visibleObjects);
3684
4893
  const orbitMarkup = layers.orbits ? renderOrbitLayer(scene, visibleObjectIds, layers.structures) : { back: "", front: "" };
3685
4894
  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("") : "";
4895
+ 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("") : "";
3686
4896
  const objectMarkup = layers.objects ? visibleObjects.map((object) => renderSceneObject(object, options.selectedObjectId ?? null, theme)).join("") : "";
3687
4897
  const labelMarkup = layers.labels ? visibleLabels.map((label) => renderSceneLabel(scene, label, options.selectedObjectId ?? null)).join("") : "";
3688
4898
  const metadataMarkup = layers.metadata ? `<text class="wo-title" x="56" y="64">${escapeXml(scene.title)}</text>
@@ -3717,6 +4927,7 @@ var WorldOrbit = (() => {
3717
4927
  .wo-orbit-back { opacity: 0.38; stroke-dasharray: 8 6; }
3718
4928
  .wo-orbit-front { opacity: 0.9; }
3719
4929
  .wo-orbit-band { stroke: ${theme.orbitBand}; stroke-linecap: round; }
4930
+ .wo-relation { stroke: ${theme.relation}; stroke-width: 2; stroke-dasharray: 10 6; }
3720
4931
  .wo-leader { stroke: ${theme.leader}; stroke-width: 1.5; stroke-dasharray: 6 5; }
3721
4932
  .wo-label { fill: ${theme.ink}; font-family: ${theme.fontFamily}; font-weight: 600; letter-spacing: 0.02em; }
3722
4933
  .wo-label-secondary { fill: ${theme.muted}; font-family: ${theme.fontFamily}; font-weight: 500; }
@@ -3750,6 +4961,7 @@ var WorldOrbit = (() => {
3750
4961
  <g data-worldorbit-world-content="true">
3751
4962
  ${layers.orbits ? `<g data-layer-id="orbits-back">${orbitMarkup.back}</g>` : ""}
3752
4963
  ${layers.guides ? `<g data-layer-id="guides">${leaderMarkup}</g>` : ""}
4964
+ ${layers.relations ? `<g data-layer-id="relations">${relationMarkup}</g>` : ""}
3753
4965
  ${layers.objects ? `<g data-layer-id="objects">${objectMarkup}</g>` : ""}
3754
4966
  ${layers.orbits ? `<g data-layer-id="orbits-front">${orbitMarkup.front}</g>` : ""}
3755
4967
  ${layers.labels ? `<g data-layer-id="labels">${labelMarkup}</g>` : ""}
@@ -3810,10 +5022,11 @@ var WorldOrbit = (() => {
3810
5022
  function renderSceneObject(sceneObject, selectedObjectId, theme) {
3811
5023
  const { object, x, y, radius, visualRadius } = sceneObject;
3812
5024
  const selectionClass = selectedObjectId === sceneObject.objectId ? " wo-object-selected" : "";
5025
+ const kindClass = object.properties.kind ? ` wo-kind-${String(object.properties.kind).toLowerCase().replace(/[^a-z0-9-]/g, "-")}` : "";
3813
5026
  const palette = resolveObjectPalette(sceneObject, theme);
3814
5027
  const imageMarkup = renderObjectImage(sceneObject);
3815
5028
  const outlineMarkup = imageMarkup ? renderObjectBody(object, x, y, radius, palette, { outlineOnly: true }) : "";
3816
- return `<g class="wo-object wo-object-${object.type}${selectionClass}" data-object-id="${escapeXml(sceneObject.objectId)}" data-parent-id="${escapeAttribute(sceneObject.parentId ?? "")}" data-group-id="${escapeAttribute(sceneObject.groupId ?? "")}" data-render-id="${escapeXml(sceneObject.renderId)}" tabindex="0" role="button" aria-label="${escapeXml(`${object.type} ${sceneObject.objectId}`)}">
5029
+ return `<g class="wo-object wo-object-${object.type}${kindClass}${selectionClass}" data-object-id="${escapeXml(sceneObject.objectId)}" data-parent-id="${escapeAttribute(sceneObject.parentId ?? "")}" data-group-id="${escapeAttribute(sceneObject.groupId ?? "")}" data-render-id="${escapeXml(sceneObject.renderId)}" tabindex="0" role="button" aria-label="${escapeXml(`${object.type} ${sceneObject.objectId}`)}">
3817
5030
  <circle class="wo-selection-ring" cx="${x}" cy="${y}" r="${visualRadius + 8}" />
3818
5031
  ${renderAtmosphere(sceneObject, palette)}
3819
5032
  ${renderObjectBody(object, x, y, radius, palette)}
@@ -3847,8 +5060,33 @@ var WorldOrbit = (() => {
3847
5060
  <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
3848
5061
  case "structure":
3849
5062
  return `<polygon points="${diamondPoints(x, y, radius)}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
3850
- case "phenomenon":
5063
+ case "phenomenon": {
5064
+ const kind = String(object.properties.kind ?? "").toLowerCase().replace(/_/g, "-");
5065
+ if (options.outlineOnly) {
5066
+ if (kind === "black-hole" || kind === "nebula" || kind === "galaxy" || kind === "dwarf-galaxy") {
5067
+ return `<circle cx="${x}" cy="${y}" r="${radius}" fill="transparent" stroke="${palette.stroke}" stroke-width="1.4" />`;
5068
+ }
5069
+ return `<polygon points="${phenomenonPoints(x, y, radius)}" fill="transparent" stroke="${palette.stroke}" stroke-width="1.4" />`;
5070
+ }
5071
+ if (kind === "black-hole") {
5072
+ return `<ellipse cx="${x}" cy="${y}" rx="${radius * 2.4}" ry="${radius * 0.55}" fill="none" stroke="${palette.accentRing ?? palette.stroke}" stroke-width="3.5" />
5073
+ <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="2" />`;
5074
+ }
5075
+ if (kind === "galaxy") {
5076
+ return `<ellipse cx="${x}" cy="${y}" rx="${radius * 2.6}" ry="${radius}" fill="${palette.halo ?? "none"}" stroke="none" />
5077
+ <ellipse cx="${x}" cy="${y}" rx="${radius * 1.5}" ry="${radius * 0.42}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.2" />
5078
+ <circle cx="${x}" cy="${y}" r="${radius * 0.28}" fill="${palette.core ?? "#fff"}" stroke="none" />`;
5079
+ }
5080
+ if (kind === "dwarf-galaxy") {
5081
+ return `<ellipse cx="${x}" cy="${y}" rx="${radius * 1.6}" ry="${radius * 0.55}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1" />
5082
+ <circle cx="${x}" cy="${y}" r="${radius * 0.25}" fill="${palette.core ?? "#fff"}" stroke="none" />`;
5083
+ }
5084
+ if (kind === "nebula") {
5085
+ return `<circle cx="${x}" cy="${y}" r="${radius * 2.2}" fill="${palette.halo ?? "none"}" stroke="none" />
5086
+ <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1" />`;
5087
+ }
3851
5088
  return `<polygon points="${phenomenonPoints(x, y, radius)}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
5089
+ }
3852
5090
  }
3853
5091
  }
3854
5092
  function renderAtmosphere(sceneObject, palette) {
@@ -3917,7 +5155,8 @@ var WorldOrbit = (() => {
3917
5155
  }
3918
5156
  }
3919
5157
  function resolveObjectPalette(sceneObject, theme) {
3920
- const base = basePaletteForType(sceneObject.object.type, theme);
5158
+ const kind = String(sceneObject.object.properties.kind ?? "").toLowerCase().replace(/_/g, "-");
5159
+ const base = basePaletteForType(sceneObject.object.type, kind, theme);
3921
5160
  const customFill = sceneObject.fillColor && isColorLike(sceneObject.fillColor) ? sceneObject.fillColor : base.fill;
3922
5161
  const albedo = numericValue(sceneObject.object.properties.albedo);
3923
5162
  const temperature = numericValue(sceneObject.object.properties.temperature);
@@ -3933,7 +5172,7 @@ var WorldOrbit = (() => {
3933
5172
  tail: sceneObject.object.type === "comet" ? rgbaString(mixColors(fill, "#ffffff", 0.5) ?? fill, 0.72) : void 0
3934
5173
  };
3935
5174
  }
3936
- function basePaletteForType(type, theme) {
5175
+ function basePaletteForType(type, kind, theme) {
3937
5176
  switch (type) {
3938
5177
  case "star":
3939
5178
  return {
@@ -3955,8 +5194,26 @@ var WorldOrbit = (() => {
3955
5194
  case "structure":
3956
5195
  return { fill: theme.accentStrong, stroke: "#fff2ea" };
3957
5196
  case "phenomenon":
3958
- return { fill: "#78ffd7", stroke: "#e9fff7" };
5197
+ return kindPhenomenonPalette(kind);
5198
+ }
5199
+ }
5200
+ function kindPhenomenonPalette(kind) {
5201
+ if (kind === "galaxy") {
5202
+ return { fill: "rgba(165,125,255,0.55)", stroke: "rgba(210,185,255,0.75)", halo: "rgba(160,120,255,0.10)", core: "#ede0ff" };
5203
+ }
5204
+ if (kind === "dwarf-galaxy") {
5205
+ return { fill: "rgba(190,165,255,0.45)", stroke: "rgba(220,205,255,0.75)", core: "#ddd0ff" };
3959
5206
  }
5207
+ if (kind === "black-hole") {
5208
+ return { fill: "#040408", stroke: "#ff6a00", accentRing: "rgba(255,140,20,0.72)" };
5209
+ }
5210
+ if (kind === "nebula") {
5211
+ return { fill: "rgba(105,205,255,0.45)", stroke: "rgba(180,235,255,0.72)", halo: "rgba(100,200,255,0.08)" };
5212
+ }
5213
+ if (kind === "void") {
5214
+ return { fill: "#05080f", stroke: "rgba(130,160,255,0.4)" };
5215
+ }
5216
+ return { fill: "#78ffd7", stroke: "#e9fff7" };
3960
5217
  }
3961
5218
  function applyTemperatureAndAlbedo(baseColor, temperature, albedo, type) {
3962
5219
  let nextColor = baseColor;
@@ -4276,6 +5533,41 @@ var WorldOrbit = (() => {
4276
5533
  });
4277
5534
  }
4278
5535
  const placement = details.object.placement;
5536
+ if (details.object.groups?.length) {
5537
+ fields.set("groups", {
5538
+ key: "groups",
5539
+ label: "Groups",
5540
+ value: details.object.groups.join(", ")
5541
+ });
5542
+ }
5543
+ if (details.object.epoch) {
5544
+ fields.set("epoch", {
5545
+ key: "epoch",
5546
+ label: "Epoch",
5547
+ value: details.object.epoch
5548
+ });
5549
+ }
5550
+ if (details.object.referencePlane) {
5551
+ fields.set("referencePlane", {
5552
+ key: "referencePlane",
5553
+ label: "Reference Plane",
5554
+ value: details.object.referencePlane
5555
+ });
5556
+ }
5557
+ if (details.object.tidalLock !== void 0) {
5558
+ fields.set("tidalLock", {
5559
+ key: "tidalLock",
5560
+ label: "Tidal Lock",
5561
+ value: details.object.tidalLock ? "true" : "false"
5562
+ });
5563
+ }
5564
+ if (details.object.resonance) {
5565
+ fields.set("resonance", {
5566
+ key: "resonance",
5567
+ label: "Resonance",
5568
+ value: `${details.object.resonance.targetObjectId} ${details.object.resonance.ratio}`
5569
+ });
5570
+ }
4279
5571
  if (placement?.mode === "at") {
4280
5572
  fields.set("placement", {
4281
5573
  key: "placement",
@@ -4445,6 +5737,9 @@ var WorldOrbit = (() => {
4445
5737
  touchPoints.set(event.pointerId, point);
4446
5738
  if (touchPoints.size === 2) {
4447
5739
  touchGesture = createTouchGestureState(scene, state, touchPoints);
5740
+ } else if (touchPoints.size === 1) {
5741
+ dragDistance = 0;
5742
+ suppressClick = false;
4448
5743
  }
4449
5744
  return;
4450
5745
  }
@@ -4462,7 +5757,9 @@ var WorldOrbit = (() => {
4462
5757
  if (!behavior.touch || !touchPoints.has(event.pointerId)) {
4463
5758
  return;
4464
5759
  }
4465
- touchPoints.set(event.pointerId, getViewportPointFromClient(event.clientX, event.clientY));
5760
+ const prevPoint = touchPoints.get(event.pointerId);
5761
+ const nextPoint2 = getViewportPointFromClient(event.clientX, event.clientY);
5762
+ touchPoints.set(event.pointerId, nextPoint2);
4466
5763
  if (touchPoints.size === 2) {
4467
5764
  if (!touchGesture) {
4468
5765
  touchGesture = createTouchGestureState(scene, state, touchPoints);
@@ -4473,6 +5770,14 @@ var WorldOrbit = (() => {
4473
5770
  const deltaX2 = current.center.x - touchGesture.startViewportCenter.x;
4474
5771
  const deltaY2 = current.center.y - touchGesture.startViewportCenter.y;
4475
5772
  updateState(panViewerState(zoomedState, deltaX2, deltaY2));
5773
+ } else if (touchPoints.size === 1) {
5774
+ const deltaX2 = nextPoint2.x - prevPoint.x;
5775
+ const deltaY2 = nextPoint2.y - prevPoint.y;
5776
+ dragDistance += Math.abs(deltaX2) + Math.abs(deltaY2);
5777
+ if (dragDistance > 2) {
5778
+ suppressClick = true;
5779
+ }
5780
+ updateState(panViewerState(state, deltaX2, deltaY2));
4476
5781
  }
4477
5782
  return;
4478
5783
  }
@@ -4937,8 +6242,10 @@ var WorldOrbit = (() => {
4937
6242
  renderObject,
4938
6243
  label: scene.labels.find((label) => label.objectId === renderObject.objectId && !label.hidden) ?? null,
4939
6244
  group: scene.groups.find((group) => group.renderId === renderObject.groupId) ?? null,
6245
+ semanticGroups: scene.semanticGroups.filter((group) => renderObject.semanticGroupIds.includes(group.id)),
4940
6246
  orbit: scene.orbitVisuals.find((orbit) => orbit.objectId === renderObject.objectId && !orbit.hidden) ?? null,
4941
6247
  relatedOrbits: scene.orbitVisuals.filter((orbit) => !orbit.hidden && (orbit.objectId === renderObject.objectId || renderObject.ancestorIds.includes(orbit.objectId) || renderObject.childIds.includes(orbit.objectId))),
6248
+ relations: scene.relations.filter((relation) => !relation.hidden && (relation.fromObjectId === renderObject.objectId || relation.toObjectId === renderObject.objectId)),
4942
6249
  parent: getObjectById(renderObject.parentId),
4943
6250
  children: renderObject.childIds.map((childId) => getObjectById(childId)).filter(Boolean),
4944
6251
  ancestors: renderObject.ancestorIds.map((ancestorId) => getObjectById(ancestorId)).filter(Boolean),
@@ -5563,6 +6870,7 @@ var WorldOrbit = (() => {
5563
6870
  const controls = {
5564
6871
  search: options.controls?.search ?? true,
5565
6872
  typeFilter: options.controls?.typeFilter ?? true,
6873
+ groupFilter: options.controls?.groupFilter ?? true,
5566
6874
  viewpointSelect: options.controls?.viewpointSelect ?? true,
5567
6875
  inspector: options.controls?.inspector ?? true,
5568
6876
  bookmarks: options.controls?.bookmarks ?? true
@@ -5572,6 +6880,7 @@ var WorldOrbit = (() => {
5572
6880
  const toolbar = container.querySelector("[data-atlas-toolbar]");
5573
6881
  const searchInput = container.querySelector("[data-atlas-search]");
5574
6882
  const typeFilterSelect = container.querySelector("[data-atlas-type-filter]");
6883
+ const groupFilterSelect = container.querySelector("[data-atlas-group-filter]");
5575
6884
  const viewpointSelect = container.querySelector("[data-atlas-viewpoint]");
5576
6885
  const bookmarkButton = container.querySelector("[data-atlas-bookmark]");
5577
6886
  const bookmarkList = container.querySelector("[data-atlas-bookmarks]");
@@ -5584,6 +6893,7 @@ var WorldOrbit = (() => {
5584
6893
  const baseFilter = normalizeViewerFilter(options.initialFilter ?? null);
5585
6894
  let searchQuery = options.initialQuery?.trim() ?? baseFilter?.query ?? "";
5586
6895
  let objectTypeFilter = options.initialObjectType ?? (baseFilter?.objectTypes?.length === 1 ? baseFilter.objectTypes[0] : null);
6896
+ let groupFilter = baseFilter?.groupIds?.[0] ?? null;
5587
6897
  let bookmarks = [];
5588
6898
  let viewer;
5589
6899
  viewer = createInteractiveViewer(stage, {
@@ -5631,6 +6941,7 @@ var WorldOrbit = (() => {
5631
6941
  });
5632
6942
  applyCurrentFilter();
5633
6943
  populateViewpoints();
6944
+ populateGroups();
5634
6945
  syncControlsFromFilter(viewer.getFilter());
5635
6946
  renderBookmarks();
5636
6947
  updateSearchResults();
@@ -5643,6 +6954,10 @@ var WorldOrbit = (() => {
5643
6954
  objectTypeFilter = typeFilterSelect.value || null;
5644
6955
  applyCurrentFilter();
5645
6956
  });
6957
+ groupFilterSelect?.addEventListener("change", () => {
6958
+ groupFilter = groupFilterSelect.value || null;
6959
+ applyCurrentFilter();
6960
+ });
5646
6961
  viewpointSelect?.addEventListener("change", () => {
5647
6962
  const activeViewer = requireViewer();
5648
6963
  if (!viewpointSelect.value) {
@@ -5784,6 +7099,7 @@ var WorldOrbit = (() => {
5784
7099
  return api;
5785
7100
  function refreshAfterInputChange() {
5786
7101
  populateViewpoints();
7102
+ populateGroups();
5787
7103
  applyCurrentFilter();
5788
7104
  renderBookmarks();
5789
7105
  updateSearchResults();
@@ -5800,19 +7116,23 @@ var WorldOrbit = (() => {
5800
7116
  query: searchQuery || void 0,
5801
7117
  objectTypes: objectTypeFilter ? [objectTypeFilter] : void 0,
5802
7118
  tags: baseFilter?.tags,
5803
- groupIds: baseFilter?.groupIds,
7119
+ groupIds: groupFilter ? [groupFilter] : baseFilter?.groupIds,
5804
7120
  includeAncestors: baseFilter?.includeAncestors ?? true
5805
7121
  });
5806
7122
  }
5807
7123
  function syncControlsFromFilter(filter) {
5808
7124
  searchQuery = filter?.query?.trim() ?? "";
5809
7125
  objectTypeFilter = filter?.objectTypes?.length === 1 ? filter.objectTypes[0] : null;
7126
+ groupFilter = filter?.groupIds?.length === 1 ? filter.groupIds[0] : null;
5810
7127
  if (searchInput && document.activeElement !== searchInput) {
5811
7128
  searchInput.value = searchQuery;
5812
7129
  }
5813
7130
  if (typeFilterSelect) {
5814
7131
  typeFilterSelect.value = objectTypeFilter ?? "";
5815
7132
  }
7133
+ if (groupFilterSelect) {
7134
+ groupFilterSelect.value = groupFilter ?? "";
7135
+ }
5816
7136
  }
5817
7137
  function populateViewpoints() {
5818
7138
  if (!viewpointSelect) {
@@ -5826,6 +7146,17 @@ var WorldOrbit = (() => {
5826
7146
  ].join("");
5827
7147
  viewpointSelect.value = active;
5828
7148
  }
7149
+ function populateGroups() {
7150
+ if (!groupFilterSelect) {
7151
+ return;
7152
+ }
7153
+ const activeViewer = requireViewer();
7154
+ groupFilterSelect.innerHTML = [
7155
+ `<option value="">All groups</option>`,
7156
+ ...activeViewer.getScene().semanticGroups.map((group) => `<option value="${escapeHtml2(group.id)}">${escapeHtml2(group.label)}</option>`)
7157
+ ].join("");
7158
+ groupFilterSelect.value = groupFilter ?? "";
7159
+ }
5829
7160
  function syncViewpointControl() {
5830
7161
  if (!viewpointSelect) {
5831
7162
  return;
@@ -5859,6 +7190,8 @@ var WorldOrbit = (() => {
5859
7190
  projection: activeViewer.getScene().projection,
5860
7191
  renderPreset: activeViewer.getScene().renderPreset,
5861
7192
  groupCount: activeViewer.getScene().groups.length,
7193
+ semanticGroupCount: activeViewer.getScene().semanticGroups.length,
7194
+ relationCount: activeViewer.getScene().relations.length,
5862
7195
  viewpointCount: activeViewer.getScene().viewpoints.length
5863
7196
  }
5864
7197
  };
@@ -5891,6 +7224,12 @@ var WorldOrbit = (() => {
5891
7224
  <option value="phenomenon">Phenomenon</option>
5892
7225
  </select>
5893
7226
  </label>` : "",
7227
+ controls.groupFilter ? `<label class="wo-atlas-field">
7228
+ <span>Group</span>
7229
+ <select data-atlas-group-filter>
7230
+ <option value="">All groups</option>
7231
+ </select>
7232
+ </label>` : "",
5894
7233
  controls.viewpointSelect ? `<label class="wo-atlas-field">
5895
7234
  <span>Viewpoint</span>
5896
7235
  <select data-atlas-viewpoint>
@@ -6034,5 +7373,5 @@ var WorldOrbit = (() => {
6034
7373
  function parseSource(source) {
6035
7374
  return loadWorldOrbitSource(source).document;
6036
7375
  }
6037
- return __toCommonJS(dist_exports);
7376
+ return __toCommonJS(index_exports);
6038
7377
  })();