worldorbit 2.5.13 → 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.
Files changed (41) hide show
  1. package/README.md +37 -11
  2. package/dist/browser/core/dist/index.js +1811 -386
  3. package/dist/browser/editor/dist/index.js +10534 -0
  4. package/dist/browser/markdown/dist/index.js +1477 -221
  5. package/dist/browser/viewer/dist/index.js +1569 -230
  6. package/dist/unpkg/core/dist/index.js +1814 -389
  7. package/dist/unpkg/editor/dist/index.js +10559 -0
  8. package/dist/unpkg/markdown/dist/index.js +1480 -224
  9. package/dist/unpkg/viewer/dist/index.js +1572 -233
  10. package/dist/unpkg/worldorbit-core.min.js +12 -5
  11. package/dist/unpkg/worldorbit-editor.min.js +812 -0
  12. package/dist/unpkg/worldorbit-markdown.min.js +32 -23
  13. package/dist/unpkg/worldorbit-viewer.min.js +55 -41
  14. package/dist/unpkg/worldorbit.js +1713 -231
  15. package/dist/unpkg/worldorbit.min.js +58 -44
  16. package/package.json +3 -2
  17. package/packages/core/README.md +5 -1
  18. package/packages/core/dist/atlas-edit.d.ts +2 -2
  19. package/packages/core/dist/atlas-edit.js +70 -7
  20. package/packages/core/dist/atlas-utils.d.ts +22 -0
  21. package/packages/core/dist/atlas-utils.js +189 -0
  22. package/packages/core/dist/atlas-validate.d.ts +2 -0
  23. package/packages/core/dist/atlas-validate.js +285 -0
  24. package/packages/core/dist/draft-parse.js +786 -153
  25. package/packages/core/dist/draft.d.ts +3 -0
  26. package/packages/core/dist/draft.js +47 -3
  27. package/packages/core/dist/format.js +165 -9
  28. package/packages/core/dist/load.js +58 -13
  29. package/packages/core/dist/normalize.js +7 -0
  30. package/packages/core/dist/scene.js +66 -13
  31. package/packages/core/dist/types.d.ts +97 -3
  32. package/packages/editor/dist/editor.js +44 -0
  33. package/packages/markdown/README.md +1 -1
  34. package/packages/viewer/README.md +2 -1
  35. package/packages/viewer/dist/atlas-state.js +7 -1
  36. package/packages/viewer/dist/atlas-viewer.js +35 -1
  37. package/packages/viewer/dist/render.js +16 -7
  38. package/packages/viewer/dist/theme.js +4 -0
  39. package/packages/viewer/dist/tooltip.js +35 -0
  40. package/packages/viewer/dist/types.d.ts +7 -0
  41. package/packages/viewer/dist/viewer.js +4 -0
@@ -302,13 +302,13 @@
302
302
  function unitFamilyAllowsUnit(family, unit) {
303
303
  switch (family) {
304
304
  case "distance":
305
- return unit === null || ["au", "km", "re", "sol"].includes(unit);
305
+ return unit === null || ["au", "km", "m", "ly", "pc", "kpc", "re", "sol"].includes(unit);
306
306
  case "radius":
307
- return unit === null || ["km", "re", "sol"].includes(unit);
307
+ return unit === null || ["km", "m", "re", "rj", "sol"].includes(unit);
308
308
  case "mass":
309
- return unit === null || ["me", "sol"].includes(unit);
309
+ return unit === null || ["me", "mj", "sol"].includes(unit);
310
310
  case "duration":
311
- return unit === null || ["h", "d", "y"].includes(unit);
311
+ return unit === null || ["s", "min", "h", "d", "y", "ky", "my", "gy"].includes(unit);
312
312
  case "angle":
313
313
  return unit === null || unit === "deg";
314
314
  case "generic":
@@ -512,7 +512,7 @@
512
512
  }
513
513
 
514
514
  // packages/core/dist/normalize.js
515
- var UNIT_PATTERN = /^(-?\d+(?:\.\d+)?)(au|km|re|sol|me|d|y|h|deg)?$/;
515
+ 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)?$/;
516
516
  var BOOLEAN_VALUES = /* @__PURE__ */ new Map([
517
517
  ["true", true],
518
518
  ["false", false],
@@ -537,7 +537,10 @@
537
537
  return {
538
538
  format: "worldorbit",
539
539
  version: "1.0",
540
+ schemaVersion: "1.0",
540
541
  system,
542
+ groups: [],
543
+ relations: [],
541
544
  objects
542
545
  };
543
546
  }
@@ -547,13 +550,17 @@
547
550
  const fieldMap = collectFields(mergedFields);
548
551
  const placement = extractPlacement(node.objectType, fieldMap);
549
552
  const properties = normalizeProperties(fieldMap);
550
- const info = normalizeInfo(node.infoEntries);
553
+ const info2 = normalizeInfo(node.infoEntries);
551
554
  if (node.objectType === "system") {
552
555
  return {
553
556
  type: "system",
554
557
  id: node.name,
558
+ title: typeof properties.title === "string" ? properties.title : null,
559
+ description: null,
560
+ epoch: null,
561
+ referencePlane: null,
555
562
  properties,
556
- info
563
+ info: info2
557
564
  };
558
565
  }
559
566
  return {
@@ -561,7 +568,7 @@
561
568
  id: node.name,
562
569
  properties,
563
570
  placement,
564
- info
571
+ info: info2
565
572
  };
566
573
  }
567
574
  function validateFieldCompatibility(objectType, fields) {
@@ -691,14 +698,14 @@
691
698
  }
692
699
  }
693
700
  function normalizeInfo(entries) {
694
- const info = {};
701
+ const info2 = {};
695
702
  for (const entry of entries) {
696
- if (entry.key in info) {
703
+ if (entry.key in info2) {
697
704
  throw WorldOrbitError.fromLocation(`Duplicate info key "${entry.key}"`, entry.location);
698
705
  }
699
- info[entry.key] = entry.value;
706
+ info2[entry.key] = entry.value;
700
707
  }
701
- return info;
708
+ return info2;
702
709
  }
703
710
  function parseAtReference(target, location) {
704
711
  if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
@@ -869,37 +876,41 @@
869
876
  }
870
877
 
871
878
  // packages/core/dist/diagnostics.js
872
- function diagnosticFromError(error, source, code = `${source}.failed`) {
873
- if (error instanceof WorldOrbitError) {
879
+ function diagnosticFromError(error2, source, code = `${source}.failed`) {
880
+ if (error2 instanceof WorldOrbitError) {
874
881
  return {
875
882
  code,
876
883
  severity: "error",
877
884
  source,
878
- message: error.message,
879
- line: error.line,
880
- column: error.column
885
+ message: error2.message,
886
+ line: error2.line,
887
+ column: error2.column
881
888
  };
882
889
  }
883
- if (error instanceof Error) {
890
+ if (error2 instanceof Error) {
884
891
  return {
885
892
  code,
886
893
  severity: "error",
887
894
  source,
888
- message: error.message
895
+ message: error2.message
889
896
  };
890
897
  }
891
898
  return {
892
899
  code,
893
900
  severity: "error",
894
901
  source,
895
- message: String(error)
902
+ message: String(error2)
896
903
  };
897
904
  }
898
905
 
899
906
  // packages/core/dist/scene.js
900
907
  var AU_IN_KM = 1495978707e-1;
901
908
  var EARTH_RADIUS_IN_KM = 6371;
909
+ var JUPITER_RADIUS_IN_KM = 71492;
902
910
  var SOLAR_RADIUS_IN_KM = 695700;
911
+ var LY_IN_AU = 63241.077;
912
+ var PC_IN_AU = 206264.806;
913
+ var KPC_IN_AU = 206264806;
903
914
  var ISO_FLATTENING = 0.68;
904
915
  var MIN_ISO_MINOR_SCALE = 0.2;
905
916
  var ARC_SAMPLE_COUNT = 28;
@@ -1019,8 +1030,10 @@
1019
1030
  const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
1020
1031
  const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
1021
1032
  const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
1022
- const layers = createSceneLayers(orbitVisuals, leaders, objects, labels);
1033
+ const relations = createSceneRelations(document2, objects);
1034
+ const layers = createSceneLayers(orbitVisuals, relations, leaders, objects, labels);
1023
1035
  const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
1036
+ const semanticGroups = createSceneSemanticGroups(document2, objects);
1024
1037
  const viewpoints = createSceneViewpoints(document2, projection, frame.preset, relationships, objectMap);
1025
1038
  const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
1026
1039
  return {
@@ -1030,7 +1043,7 @@
1030
1043
  renderPreset: frame.preset,
1031
1044
  projection,
1032
1045
  scaleModel,
1033
- title: String(document2.system?.properties.title ?? document2.system?.id ?? "WorldOrbit") || "WorldOrbit",
1046
+ title: String(document2.system?.title ?? document2.system?.properties.title ?? document2.system?.id ?? "WorldOrbit") || "WorldOrbit",
1034
1047
  subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
1035
1048
  systemId,
1036
1049
  viewMode: projection,
@@ -1046,9 +1059,11 @@
1046
1059
  contentBounds,
1047
1060
  layers,
1048
1061
  groups,
1062
+ semanticGroups,
1049
1063
  viewpoints,
1050
1064
  objects,
1051
1065
  orbitVisuals,
1066
+ relations,
1052
1067
  leaders,
1053
1068
  labels
1054
1069
  };
@@ -1147,6 +1162,7 @@
1147
1162
  }
1148
1163
  function createSceneObject(position, scaleModel, relationships) {
1149
1164
  const { object, x, y, radius, sortKey, anchorX, anchorY } = position;
1165
+ const renderPriority = object.renderHints?.renderPriority ?? 0;
1150
1166
  return {
1151
1167
  renderId: createRenderId(object.id),
1152
1168
  objectId: object.id,
@@ -1155,11 +1171,12 @@
1155
1171
  ancestorIds: relationships.ancestorIds.get(object.id) ?? [],
1156
1172
  childIds: relationships.childIds.get(object.id) ?? [],
1157
1173
  groupId: relationships.groupIds.get(object.id) ?? null,
1174
+ semanticGroupIds: [...object.groups ?? []],
1158
1175
  x,
1159
1176
  y,
1160
1177
  radius,
1161
1178
  visualRadius: visualExtentForObject(object, radius, scaleModel),
1162
- sortKey,
1179
+ sortKey: sortKey + renderPriority * 1e-3,
1163
1180
  anchorX,
1164
1181
  anchorY,
1165
1182
  label: object.id,
@@ -1176,6 +1193,7 @@
1176
1193
  object: draft.object,
1177
1194
  parentId: draft.parentId,
1178
1195
  groupId,
1196
+ semanticGroupIds: [...draft.object.groups ?? []],
1179
1197
  kind: draft.kind,
1180
1198
  cx: draft.cx,
1181
1199
  cy: draft.cy,
@@ -1187,7 +1205,7 @@
1187
1205
  bandThickness: draft.bandThickness,
1188
1206
  frontArcPath: draft.frontArcPath,
1189
1207
  backArcPath: draft.backArcPath,
1190
- hidden: draft.object.properties.hidden === true
1208
+ hidden: draft.object.properties.hidden === true || draft.object.renderHints?.renderOrbit === false
1191
1209
  };
1192
1210
  }
1193
1211
  function createLeaderLine(draft) {
@@ -1196,6 +1214,7 @@
1196
1214
  objectId: draft.object.id,
1197
1215
  object: draft.object,
1198
1216
  groupId: draft.groupId,
1217
+ semanticGroupIds: [...draft.object.groups ?? []],
1199
1218
  x1: draft.x1,
1200
1219
  y1: draft.y1,
1201
1220
  x2: draft.x2,
@@ -1207,7 +1226,7 @@
1207
1226
  function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1208
1227
  const labels = [];
1209
1228
  const occupied = [];
1210
- const visibleObjects = [...objects].filter((object) => !object.hidden).sort((left, right) => left.sortKey - right.sortKey);
1229
+ const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort((left, right) => left.sortKey - right.sortKey);
1211
1230
  for (const object of visibleObjects) {
1212
1231
  const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1213
1232
  const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
@@ -1227,6 +1246,7 @@
1227
1246
  objectId: object.objectId,
1228
1247
  object: object.object,
1229
1248
  groupId: object.groupId,
1249
+ semanticGroupIds: [...object.semanticGroupIds],
1230
1250
  label: object.label,
1231
1251
  secondaryLabel: object.secondaryLabel,
1232
1252
  x: object.x,
@@ -1239,7 +1259,7 @@
1239
1259
  }
1240
1260
  return labels;
1241
1261
  }
1242
- function createSceneLayers(orbitVisuals, leaders, objects, labels) {
1262
+ function createSceneLayers(orbitVisuals, relations, leaders, objects, labels) {
1243
1263
  const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1244
1264
  const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1245
1265
  return [
@@ -1250,6 +1270,10 @@
1250
1270
  },
1251
1271
  { id: "orbits-back", renderIds: backOrbitIds },
1252
1272
  { id: "orbits-front", renderIds: frontOrbitIds },
1273
+ {
1274
+ id: "relations",
1275
+ renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId)
1276
+ },
1253
1277
  {
1254
1278
  id: "objects",
1255
1279
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
@@ -1314,6 +1338,36 @@
1314
1338
  }
1315
1339
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1316
1340
  }
1341
+ function createSceneSemanticGroups(document2, objects) {
1342
+ return [...document2.groups].map((group) => ({
1343
+ id: group.id,
1344
+ label: group.label,
1345
+ summary: group.summary,
1346
+ color: group.color,
1347
+ tags: [...group.tags],
1348
+ hidden: group.hidden,
1349
+ objectIds: objects.filter((object) => !object.hidden && object.semanticGroupIds.includes(group.id)).map((object) => object.objectId)
1350
+ })).sort((left, right) => left.label.localeCompare(right.label));
1351
+ }
1352
+ function createSceneRelations(document2, objects) {
1353
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1354
+ return document2.relations.map((relation) => {
1355
+ const from = objectMap.get(relation.from);
1356
+ const to = objectMap.get(relation.to);
1357
+ return {
1358
+ renderId: `${createRenderId(relation.id)}-relation`,
1359
+ relationId: relation.id,
1360
+ relation,
1361
+ fromObjectId: relation.from,
1362
+ toObjectId: relation.to,
1363
+ x1: from?.x ?? 0,
1364
+ y1: from?.y ?? 0,
1365
+ x2: to?.x ?? 0,
1366
+ y2: to?.y ?? 0,
1367
+ hidden: relation.hidden || !from || !to || from.hidden || to.hidden
1368
+ };
1369
+ }).sort((left, right) => left.relation.id.localeCompare(right.relation.id));
1370
+ }
1317
1371
  function createSceneViewpoints(document2, projection, preset, relationships, objectMap) {
1318
1372
  const generatedOverview = createGeneratedOverviewViewpoint(document2, projection, preset);
1319
1373
  const drafts = /* @__PURE__ */ new Map();
@@ -1331,7 +1385,7 @@
1331
1385
  }
1332
1386
  const field = fieldParts.join(".").toLowerCase();
1333
1387
  const draft = drafts.get(id) ?? { id };
1334
- applyViewpointField(draft, field, value, projection, preset, relationships, objectMap);
1388
+ applyViewpointField(draft, field, value, document2, projection, preset, relationships, objectMap);
1335
1389
  drafts.set(id, draft);
1336
1390
  }
1337
1391
  const viewpoints = [...drafts.values()].map((draft) => finalizeViewpointDraft(draft, projection, preset, objectMap)).filter(Boolean);
@@ -1359,7 +1413,8 @@
1359
1413
  });
1360
1414
  }
1361
1415
  function createGeneratedOverviewViewpoint(document2, projection, preset) {
1362
- const label = document2.system?.properties.title ? `${String(document2.system.properties.title)} Overview` : "Overview";
1416
+ const title = document2.system?.title ?? document2.system?.properties.title;
1417
+ const label = title ? `${String(title)} Overview` : "Overview";
1363
1418
  return {
1364
1419
  id: "overview",
1365
1420
  label,
@@ -1375,7 +1430,7 @@
1375
1430
  generated: true
1376
1431
  };
1377
1432
  }
1378
- function applyViewpointField(draft, field, value, projection, preset, relationships, objectMap) {
1433
+ function applyViewpointField(draft, field, value, document2, projection, preset, relationships, objectMap) {
1379
1434
  const normalizedValue = value.trim();
1380
1435
  switch (field) {
1381
1436
  case "label":
@@ -1442,7 +1497,7 @@
1442
1497
  case "groups":
1443
1498
  draft.filter = {
1444
1499
  ...draft.filter ?? createEmptyViewpointFilter(),
1445
- groupIds: parseViewpointGroups(normalizedValue, relationships, objectMap)
1500
+ groupIds: parseViewpointGroups(normalizedValue, document2, relationships, objectMap)
1446
1501
  };
1447
1502
  return;
1448
1503
  }
@@ -1515,7 +1570,7 @@
1515
1570
  next["orbits-front"] = enabled;
1516
1571
  continue;
1517
1572
  }
1518
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1573
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1519
1574
  next[rawLayer] = enabled;
1520
1575
  }
1521
1576
  }
@@ -1524,8 +1579,11 @@
1524
1579
  function parseViewpointObjectTypes(value) {
1525
1580
  return splitListValue(value).filter((entry) => entry === "star" || entry === "planet" || entry === "moon" || entry === "belt" || entry === "asteroid" || entry === "comet" || entry === "ring" || entry === "structure" || entry === "phenomenon");
1526
1581
  }
1527
- function parseViewpointGroups(value, relationships, objectMap) {
1582
+ function parseViewpointGroups(value, document2, relationships, objectMap) {
1528
1583
  return splitListValue(value).map((entry) => {
1584
+ if (document2.schemaVersion === "2.1" || document2.groups.some((group) => group.id === entry)) {
1585
+ return entry;
1586
+ }
1529
1587
  if (entry.startsWith("wo-") && entry.endsWith("-group")) {
1530
1588
  return entry;
1531
1589
  }
@@ -1656,8 +1714,9 @@
1656
1714
  }
1657
1715
  const orbiting = [...context.orbitChildren.get(object.id) ?? []].sort(compareOrbiting);
1658
1716
  const orbitMetricContext = computeOrbitMetricContext(orbiting, parent.radius, context.spacingFactor, context.scaleModel);
1717
+ const orbitRadiiPx = resolveOrbitRadiiPx(orbiting, orbitMetricContext);
1659
1718
  orbiting.forEach((child, index) => {
1660
- const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, context);
1719
+ const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, orbitRadiiPx[index] ?? orbitMetricContext.innerPx, context);
1661
1720
  orbitDrafts.push({
1662
1721
  object: child,
1663
1722
  parentId: object.id,
@@ -1731,7 +1790,8 @@
1731
1790
  metricSpread: 0,
1732
1791
  innerPx,
1733
1792
  stepPx,
1734
- pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx)
1793
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
1794
+ minimumGapPx: stepPx * 0.42
1735
1795
  };
1736
1796
  }
1737
1797
  const minMetric = Math.min(...presentMetrics);
@@ -1744,10 +1804,11 @@
1744
1804
  metricSpread,
1745
1805
  innerPx,
1746
1806
  stepPx,
1747
- pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx)
1807
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
1808
+ minimumGapPx: stepPx * 0.42
1748
1809
  };
1749
1810
  }
1750
- function resolveOrbitGeometry(object, index, count, parent, metricContext, context) {
1811
+ function resolveOrbitGeometry(object, index, count, parent, metricContext, orbitRadiusPx, context) {
1751
1812
  const placement = object.placement;
1752
1813
  const band = object.type === "belt" || object.type === "ring";
1753
1814
  if (!placement || placement.mode !== "orbit") {
@@ -1765,7 +1826,7 @@
1765
1826
  };
1766
1827
  }
1767
1828
  const eccentricity = clampNumber(typeof placement.eccentricity === "number" ? placement.eccentricity : 0, 0, 0.92);
1768
- const semiMajor = resolveOrbitRadiusPx(object, index, metricContext);
1829
+ const semiMajor = orbitRadiusPx;
1769
1830
  const baseMinor = Math.max(semiMajor * Math.sqrt(1 - eccentricity * eccentricity), semiMajor * 0.18);
1770
1831
  const inclinationDeg = unitValueToDegrees(placement.inclination) ?? 0;
1771
1832
  const inclinationScale = context.projection === "isometric" ? Math.max(MIN_ISO_MINOR_SCALE, Math.cos(degreesToRadians(inclinationDeg))) * ISO_FLATTENING : 1;
@@ -1795,15 +1856,19 @@
1795
1856
  objectY: objectPoint.y
1796
1857
  };
1797
1858
  }
1798
- function resolveOrbitRadiusPx(object, index, metricContext) {
1799
- const metric = orbitMetric(object);
1800
- if (metric === null) {
1801
- return metricContext.innerPx + index * metricContext.stepPx;
1802
- }
1803
- if (metricContext.metricSpread > 0) {
1804
- return metricContext.innerPx + (metric - metricContext.minMetric) / metricContext.metricSpread * metricContext.pixelSpread;
1805
- }
1806
- return metricContext.innerPx + Math.log10(metric + 1) * metricContext.stepPx;
1859
+ function resolveOrbitRadiusPx(metric, metricContext) {
1860
+ return metricContext.innerPx + metricContext.stepPx * log2(Math.max(metric, 0) + 1);
1861
+ }
1862
+ function resolveOrbitRadiiPx(objects, metricContext) {
1863
+ const radii = [];
1864
+ objects.forEach((object, index) => {
1865
+ const metric = orbitMetric(object);
1866
+ const fallbackRadius = metricContext.innerPx + index * metricContext.stepPx;
1867
+ const baseRadius = metric === null ? fallbackRadius : resolveOrbitRadiusPx(metric, metricContext);
1868
+ const minimumRadius = index === 0 ? metricContext.innerPx : (radii[index - 1] ?? metricContext.innerPx) + metricContext.minimumGapPx;
1869
+ radii.push(Math.max(baseRadius, minimumRadius));
1870
+ });
1871
+ return radii;
1807
1872
  }
1808
1873
  function orbitMetric(object) {
1809
1874
  if (!object.placement || object.placement.mode !== "orbit") {
@@ -1811,6 +1876,9 @@
1811
1876
  }
1812
1877
  return toDistanceMetric(object.placement.semiMajor ?? object.placement.distance ?? null);
1813
1878
  }
1879
+ function log2(value) {
1880
+ return Math.log(value) / Math.log(2);
1881
+ }
1814
1882
  function resolveOrbitPhase(phase, index, count) {
1815
1883
  const degreeValue = phase ? unitValueToDegrees(phase) : null;
1816
1884
  if (degreeValue !== null) {
@@ -2134,8 +2202,18 @@
2134
2202
  return value.value;
2135
2203
  case "km":
2136
2204
  return value.value / AU_IN_KM;
2205
+ case "m":
2206
+ return value.value / 1e3 / AU_IN_KM;
2207
+ case "ly":
2208
+ return value.value * LY_IN_AU;
2209
+ case "pc":
2210
+ return value.value * PC_IN_AU;
2211
+ case "kpc":
2212
+ return value.value * KPC_IN_AU;
2137
2213
  case "re":
2138
2214
  return value.value * EARTH_RADIUS_IN_KM / AU_IN_KM;
2215
+ case "rj":
2216
+ return value.value * JUPITER_RADIUS_IN_KM / AU_IN_KM;
2139
2217
  case "sol":
2140
2218
  return value.value * SOLAR_RADIUS_IN_KM / AU_IN_KM;
2141
2219
  default:
@@ -2275,19 +2353,37 @@
2275
2353
  const system = document2.system ? {
2276
2354
  type: "system",
2277
2355
  id: document2.system.id,
2356
+ title: document2.system.title,
2357
+ description: document2.system.description,
2358
+ epoch: document2.system.epoch,
2359
+ referencePlane: document2.system.referencePlane,
2278
2360
  properties: materializeDraftSystemProperties(document2.system),
2279
2361
  info: materializeDraftSystemInfo(document2.system)
2280
2362
  } : null;
2281
2363
  return {
2282
2364
  format: "worldorbit",
2283
2365
  version: "1.0",
2366
+ schemaVersion: document2.version,
2284
2367
  system,
2368
+ groups: structuredClone(document2.groups ?? []),
2369
+ relations: structuredClone(document2.relations ?? []),
2285
2370
  objects: document2.objects.map(cloneWorldOrbitObject)
2286
2371
  };
2287
2372
  }
2288
2373
  function cloneWorldOrbitObject(object) {
2289
2374
  return {
2290
2375
  ...object,
2376
+ groups: object.groups ? [...object.groups] : void 0,
2377
+ resonance: object.resonance ? { ...object.resonance } : object.resonance,
2378
+ renderHints: object.renderHints ? { ...object.renderHints } : object.renderHints,
2379
+ deriveRules: object.deriveRules ? object.deriveRules.map((rule) => ({ ...rule })) : void 0,
2380
+ validationRules: object.validationRules ? object.validationRules.map((rule) => ({ ...rule })) : void 0,
2381
+ lockedFields: object.lockedFields ? [...object.lockedFields] : void 0,
2382
+ tolerances: object.tolerances ? object.tolerances.map((entry) => ({
2383
+ field: entry.field,
2384
+ 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
2385
+ })) : void 0,
2386
+ typedBlocks: object.typedBlocks ? Object.fromEntries(Object.entries(object.typedBlocks).map(([key, block]) => [key, { ...block ?? {} }])) : void 0,
2291
2387
  properties: cloneProperties(object.properties),
2292
2388
  placement: object.placement ? structuredClone(object.placement) : null,
2293
2389
  info: { ...object.info }
@@ -2323,71 +2419,80 @@
2323
2419
  if (system.defaults.units) {
2324
2420
  properties.units = system.defaults.units;
2325
2421
  }
2422
+ if (system.description) {
2423
+ properties.description = system.description;
2424
+ }
2425
+ if (system.epoch) {
2426
+ properties.epoch = system.epoch;
2427
+ }
2428
+ if (system.referencePlane) {
2429
+ properties.referencePlane = system.referencePlane;
2430
+ }
2326
2431
  return properties;
2327
2432
  }
2328
2433
  function materializeDraftSystemInfo(system) {
2329
- const info = {
2434
+ const info2 = {
2330
2435
  ...system.atlasMetadata
2331
2436
  };
2332
2437
  if (system.defaults.theme) {
2333
- info["atlas.theme"] = system.defaults.theme;
2438
+ info2["atlas.theme"] = system.defaults.theme;
2334
2439
  }
2335
2440
  for (const viewpoint of system.viewpoints) {
2336
2441
  const prefix = `viewpoint.${viewpoint.id}`;
2337
- info[`${prefix}.label`] = viewpoint.label;
2442
+ info2[`${prefix}.label`] = viewpoint.label;
2338
2443
  if (viewpoint.summary) {
2339
- info[`${prefix}.summary`] = viewpoint.summary;
2444
+ info2[`${prefix}.summary`] = viewpoint.summary;
2340
2445
  }
2341
2446
  if (viewpoint.focusObjectId) {
2342
- info[`${prefix}.focus`] = viewpoint.focusObjectId;
2447
+ info2[`${prefix}.focus`] = viewpoint.focusObjectId;
2343
2448
  }
2344
2449
  if (viewpoint.selectedObjectId) {
2345
- info[`${prefix}.select`] = viewpoint.selectedObjectId;
2450
+ info2[`${prefix}.select`] = viewpoint.selectedObjectId;
2346
2451
  }
2347
2452
  if (viewpoint.projection) {
2348
- info[`${prefix}.projection`] = viewpoint.projection;
2453
+ info2[`${prefix}.projection`] = viewpoint.projection;
2349
2454
  }
2350
2455
  if (viewpoint.preset) {
2351
- info[`${prefix}.preset`] = viewpoint.preset;
2456
+ info2[`${prefix}.preset`] = viewpoint.preset;
2352
2457
  }
2353
2458
  if (viewpoint.zoom !== null) {
2354
- info[`${prefix}.zoom`] = String(viewpoint.zoom);
2459
+ info2[`${prefix}.zoom`] = String(viewpoint.zoom);
2355
2460
  }
2356
2461
  if (viewpoint.rotationDeg !== 0) {
2357
- info[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2462
+ info2[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2358
2463
  }
2359
2464
  const serializedLayers = serializeViewpointLayers(viewpoint.layers);
2360
2465
  if (serializedLayers) {
2361
- info[`${prefix}.layers`] = serializedLayers;
2466
+ info2[`${prefix}.layers`] = serializedLayers;
2362
2467
  }
2363
2468
  if (viewpoint.filter?.query) {
2364
- info[`${prefix}.query`] = viewpoint.filter.query;
2469
+ info2[`${prefix}.query`] = viewpoint.filter.query;
2365
2470
  }
2366
2471
  if ((viewpoint.filter?.objectTypes.length ?? 0) > 0) {
2367
- info[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
2472
+ info2[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
2368
2473
  }
2369
2474
  if ((viewpoint.filter?.tags.length ?? 0) > 0) {
2370
- info[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
2475
+ info2[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
2371
2476
  }
2372
2477
  if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2373
- info[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2478
+ info2[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2374
2479
  }
2375
2480
  }
2376
2481
  for (const annotation of system.annotations) {
2377
2482
  const prefix = `annotation.${annotation.id}`;
2378
- info[`${prefix}.label`] = annotation.label;
2483
+ info2[`${prefix}.label`] = annotation.label;
2379
2484
  if (annotation.targetObjectId) {
2380
- info[`${prefix}.target`] = annotation.targetObjectId;
2485
+ info2[`${prefix}.target`] = annotation.targetObjectId;
2381
2486
  }
2382
- info[`${prefix}.body`] = annotation.body;
2487
+ info2[`${prefix}.body`] = annotation.body;
2383
2488
  if (annotation.tags.length > 0) {
2384
- info[`${prefix}.tags`] = annotation.tags.join(" ");
2489
+ info2[`${prefix}.tags`] = annotation.tags.join(" ");
2385
2490
  }
2386
2491
  if (annotation.sourceObjectId) {
2387
- info[`${prefix}.source`] = annotation.sourceObjectId;
2492
+ info2[`${prefix}.source`] = annotation.sourceObjectId;
2388
2493
  }
2389
2494
  }
2390
- return info;
2495
+ return info2;
2391
2496
  }
2392
2497
  function serializeViewpointLayers(layers) {
2393
2498
  const tokens = [];
@@ -2396,7 +2501,7 @@
2396
2501
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2397
2502
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2398
2503
  }
2399
- for (const key of ["background", "guides", "objects", "labels", "metadata"]) {
2504
+ for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
2400
2505
  if (layers[key] !== void 0) {
2401
2506
  tokens.push(layers[key] ? key : `-${key}`);
2402
2507
  }
@@ -2404,21 +2509,530 @@
2404
2509
  return tokens.join(" ");
2405
2510
  }
2406
2511
 
2512
+ // packages/core/dist/atlas-utils.js
2513
+ 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)?$/;
2514
+ var BOOLEAN_VALUES2 = /* @__PURE__ */ new Map([
2515
+ ["true", true],
2516
+ ["false", false],
2517
+ ["yes", true],
2518
+ ["no", false]
2519
+ ]);
2520
+ var URL_SCHEME_PATTERN2 = /^[A-Za-z][A-Za-z0-9+.-]*:/;
2521
+ function normalizeIdentifier(value) {
2522
+ return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
2523
+ }
2524
+ function humanizeIdentifier2(value) {
2525
+ return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
2526
+ }
2527
+ function parseAtlasUnitValue(input, location, fieldKey) {
2528
+ const match = input.match(UNIT_PATTERN2);
2529
+ if (!match) {
2530
+ throw WorldOrbitError.fromLocation(`Invalid unit value "${input}"`, location);
2531
+ }
2532
+ const unitValue = {
2533
+ value: Number(match[1]),
2534
+ unit: match[2] ?? null
2535
+ };
2536
+ if (fieldKey) {
2537
+ const schema = getFieldSchema(fieldKey);
2538
+ if (schema?.unitFamily && !unitFamilyAllowsUnit(schema.unitFamily, unitValue.unit)) {
2539
+ throw WorldOrbitError.fromLocation(`Unit "${unitValue.unit ?? "none"}" is not valid for "${fieldKey}"`, location);
2540
+ }
2541
+ }
2542
+ return unitValue;
2543
+ }
2544
+ function tryParseAtlasUnitValue(input) {
2545
+ const match = input.match(UNIT_PATTERN2);
2546
+ if (!match) {
2547
+ return null;
2548
+ }
2549
+ return {
2550
+ value: Number(match[1]),
2551
+ unit: match[2] ?? null
2552
+ };
2553
+ }
2554
+ function parseAtlasNumber(input, key, location) {
2555
+ const value = Number(input);
2556
+ if (!Number.isFinite(value)) {
2557
+ throw WorldOrbitError.fromLocation(`Invalid numeric value "${input}" for "${key}"`, location);
2558
+ }
2559
+ return value;
2560
+ }
2561
+ function parseAtlasBoolean(input, key, location) {
2562
+ const parsed = BOOLEAN_VALUES2.get(input.toLowerCase());
2563
+ if (parsed === void 0) {
2564
+ throw WorldOrbitError.fromLocation(`Invalid boolean value "${input}" for "${key}"`, location);
2565
+ }
2566
+ return parsed;
2567
+ }
2568
+ function parseAtlasAtReference(target, location) {
2569
+ if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
2570
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
2571
+ }
2572
+ const pairedMatch = target.match(/^([A-Za-z0-9._-]+)-([A-Za-z0-9._-]+):(L[1-5])$/);
2573
+ if (pairedMatch) {
2574
+ return {
2575
+ kind: "lagrange",
2576
+ primary: pairedMatch[1],
2577
+ secondary: pairedMatch[2],
2578
+ point: pairedMatch[3]
2579
+ };
2580
+ }
2581
+ const simpleMatch = target.match(/^([A-Za-z0-9._-]+):(L[1-5])$/);
2582
+ if (simpleMatch) {
2583
+ return {
2584
+ kind: "lagrange",
2585
+ primary: simpleMatch[1],
2586
+ secondary: null,
2587
+ point: simpleMatch[2]
2588
+ };
2589
+ }
2590
+ if (/^[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
2591
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
2592
+ }
2593
+ const anchorMatch = target.match(/^([A-Za-z0-9._-]+):([A-Za-z0-9._-]+)$/);
2594
+ if (anchorMatch) {
2595
+ return {
2596
+ kind: "anchor",
2597
+ objectId: anchorMatch[1],
2598
+ anchor: anchorMatch[2]
2599
+ };
2600
+ }
2601
+ return {
2602
+ kind: "named",
2603
+ name: target
2604
+ };
2605
+ }
2606
+ function validateAtlasImageSource(value, location) {
2607
+ if (!value) {
2608
+ throw WorldOrbitError.fromLocation('Field "image" must not be empty', location);
2609
+ }
2610
+ if (value.startsWith("//")) {
2611
+ throw WorldOrbitError.fromLocation('Field "image" must use a relative path, root-relative path, or an http/https URL', location);
2612
+ }
2613
+ const schemeMatch = value.match(URL_SCHEME_PATTERN2);
2614
+ if (!schemeMatch) {
2615
+ return;
2616
+ }
2617
+ const scheme = schemeMatch[0].slice(0, -1).toLowerCase();
2618
+ if (scheme !== "http" && scheme !== "https") {
2619
+ throw WorldOrbitError.fromLocation(`Field "image" does not support the "${scheme}" scheme`, location);
2620
+ }
2621
+ }
2622
+ function normalizeLegacyScalarValue(key, values, location) {
2623
+ const schema = getFieldSchema(key);
2624
+ if (!schema) {
2625
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
2626
+ }
2627
+ if (schema.arity === "single" && values.length !== 1) {
2628
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
2629
+ }
2630
+ switch (schema.kind) {
2631
+ case "list":
2632
+ return values;
2633
+ case "boolean":
2634
+ return parseAtlasBoolean(singleAtlasValue(values, key, location), key, location);
2635
+ case "number":
2636
+ return parseAtlasNumber(singleAtlasValue(values, key, location), key, location);
2637
+ case "unit":
2638
+ return parseAtlasUnitValue(singleAtlasValue(values, key, location), location, key);
2639
+ case "string": {
2640
+ const value = values.join(" ").trim();
2641
+ if (key === "image") {
2642
+ validateAtlasImageSource(value, location);
2643
+ }
2644
+ return value;
2645
+ }
2646
+ }
2647
+ }
2648
+ function ensureAtlasFieldSupported(key, objectType, location) {
2649
+ const schema = getFieldSchema(key);
2650
+ if (!schema) {
2651
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
2652
+ }
2653
+ if (!schema.objectTypes.includes(objectType)) {
2654
+ throw WorldOrbitError.fromLocation(`Field "${key}" is not valid on "${objectType}"`, location);
2655
+ }
2656
+ }
2657
+ function singleAtlasValue(values, key, location) {
2658
+ if (values.length !== 1) {
2659
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
2660
+ }
2661
+ return values[0];
2662
+ }
2663
+
2664
+ // packages/core/dist/atlas-validate.js
2665
+ var SURFACE_TARGET_TYPES2 = /* @__PURE__ */ new Set(["star", "planet", "moon", "asteroid", "comet"]);
2666
+ var EARTH_MASSES_PER_SOLAR = 332946.0487;
2667
+ var JUPITER_MASSES_PER_SOLAR = 1047.3486;
2668
+ var AU_IN_KM2 = 1495978707e-1;
2669
+ var EARTH_RADIUS_IN_KM2 = 6371;
2670
+ var SOLAR_RADIUS_IN_KM2 = 695700;
2671
+ var LY_IN_AU2 = 63241.077;
2672
+ var PC_IN_AU2 = 206264.806;
2673
+ var KPC_IN_AU2 = 206264806;
2674
+ function collectAtlasDiagnostics(document2, sourceSchemaVersion) {
2675
+ const diagnostics = [];
2676
+ const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
2677
+ const groupIds = new Set(document2.groups.map((group) => group.id));
2678
+ if (!document2.system) {
2679
+ diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
2680
+ }
2681
+ const knownIds = /* @__PURE__ */ new Map();
2682
+ for (const [kind, ids] of [
2683
+ ["group", document2.groups.map((group) => group.id)],
2684
+ ["viewpoint", document2.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
2685
+ ["annotation", document2.system?.annotations.map((annotation) => annotation.id) ?? []],
2686
+ ["relation", document2.relations.map((relation) => relation.id)],
2687
+ ["object", document2.objects.map((object) => object.id)]
2688
+ ]) {
2689
+ for (const id of ids) {
2690
+ const previous = knownIds.get(id);
2691
+ if (previous) {
2692
+ diagnostics.push(error("validate.id.duplicate", `Duplicate ${kind} id "${id}" already used by ${previous}.`));
2693
+ } else {
2694
+ knownIds.set(id, kind);
2695
+ }
2696
+ }
2697
+ }
2698
+ for (const relation of document2.relations) {
2699
+ validateRelation(relation, objectMap, diagnostics);
2700
+ }
2701
+ for (const viewpoint of document2.system?.viewpoints ?? []) {
2702
+ validateViewpointFilter(viewpoint.filter, groupIds, sourceSchemaVersion, diagnostics, viewpoint.id);
2703
+ }
2704
+ for (const object of document2.objects) {
2705
+ validateObject(object, document2.system, objectMap, groupIds, diagnostics);
2706
+ }
2707
+ return diagnostics;
2708
+ }
2709
+ function validateRelation(relation, objectMap, diagnostics) {
2710
+ if (!relation.from) {
2711
+ diagnostics.push(error("validate.relation.from.required", `Relation "${relation.id}" is missing a "from" target.`));
2712
+ } else if (!objectMap.has(relation.from)) {
2713
+ diagnostics.push(error("validate.relation.from.unknown", `Unknown relation source "${relation.from}" on "${relation.id}".`));
2714
+ }
2715
+ if (!relation.to) {
2716
+ diagnostics.push(error("validate.relation.to.required", `Relation "${relation.id}" is missing a "to" target.`));
2717
+ } else if (!objectMap.has(relation.to)) {
2718
+ diagnostics.push(error("validate.relation.to.unknown", `Unknown relation target "${relation.to}" on "${relation.id}".`));
2719
+ }
2720
+ if (!relation.kind) {
2721
+ diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
2722
+ }
2723
+ }
2724
+ function validateViewpointFilter(filter, groupIds, sourceSchemaVersion, diagnostics, viewpointId) {
2725
+ if (!filter || sourceSchemaVersion !== "2.1") {
2726
+ return;
2727
+ }
2728
+ for (const groupId of filter.groupIds) {
2729
+ if (!groupIds.has(groupId)) {
2730
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`));
2731
+ }
2732
+ }
2733
+ }
2734
+ function validateObject(object, system, objectMap, groupIds, diagnostics) {
2735
+ const placement = object.placement;
2736
+ const orbitPlacement = placement?.mode === "orbit" ? placement : null;
2737
+ const parentObject = placement?.mode === "orbit" ? objectMap.get(placement.target) ?? null : null;
2738
+ if (object.groups) {
2739
+ for (const groupId of object.groups) {
2740
+ if (!groupIds.has(groupId)) {
2741
+ diagnostics.push(warn("validate.group.unknown", `Unknown group "${groupId}" on "${object.id}".`, object.id, "groups"));
2742
+ }
2743
+ }
2744
+ }
2745
+ if (orbitPlacement) {
2746
+ if (!objectMap.has(orbitPlacement.target)) {
2747
+ diagnostics.push(error("validate.orbit.target.unknown", `Unknown placement target "${orbitPlacement.target}" on "${object.id}".`, object.id, "orbit"));
2748
+ }
2749
+ if (orbitPlacement.distance && orbitPlacement.semiMajor) {
2750
+ diagnostics.push(error("validate.orbit.distanceConflict", `Object "${object.id}" cannot declare both "distance" and "semiMajor".`, object.id, "distance"));
2751
+ }
2752
+ if (orbitPlacement.phase && !object.epoch && !system?.epoch) {
2753
+ diagnostics.push(warn("validate.phase.epochMissing", `Object "${object.id}" sets "phase" without an object or system epoch.`, object.id, "phase"));
2754
+ }
2755
+ if (orbitPlacement.inclination && !object.referencePlane && !system?.referencePlane) {
2756
+ diagnostics.push(warn("validate.inclination.referencePlaneMissing", `Object "${object.id}" sets "inclination" without an object or system reference plane.`, object.id, "inclination"));
2757
+ }
2758
+ if (orbitPlacement.period && !massInSolar(parentObject?.properties.mass)) {
2759
+ diagnostics.push(warn("validate.period.massMissing", `Object "${object.id}" sets "period" but its central mass cannot be derived.`, object.id, "period"));
2760
+ }
2761
+ }
2762
+ if (placement?.mode === "surface") {
2763
+ const target = objectMap.get(placement.target);
2764
+ if (!target) {
2765
+ diagnostics.push(error("validate.surface.target.unknown", `Unknown placement target "${placement.target}" on "${object.id}".`, object.id, "surface"));
2766
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
2767
+ diagnostics.push(error("validate.surface.target.invalid", `Surface target "${placement.target}" on "${object.id}" is not surface-capable.`, object.id, "surface"));
2768
+ }
2769
+ }
2770
+ if (placement?.mode === "at") {
2771
+ if (object.type !== "structure" && object.type !== "phenomenon") {
2772
+ diagnostics.push(error("validate.at.objectType", `Only structures and phenomena may use "at" placement; found "${object.type}" on "${object.id}".`, object.id, "at"));
2773
+ }
2774
+ if (!validateAtTarget(object, objectMap, diagnostics)) {
2775
+ diagnostics.push(error("validate.at.target.unknown", `Unknown at-reference target "${placement.target}" on "${object.id}".`, object.id, "at"));
2776
+ }
2777
+ }
2778
+ if (object.resonance) {
2779
+ const target = objectMap.get(object.resonance.targetObjectId);
2780
+ if (!target) {
2781
+ diagnostics.push(error("validate.resonance.target.unknown", `Unknown resonance target "${object.resonance.targetObjectId}" on "${object.id}".`, object.id, "resonance"));
2782
+ } else if (object.placement?.mode !== "orbit" || target.placement?.mode !== "orbit" || object.placement.target !== target.placement.target) {
2783
+ diagnostics.push(warn("validate.resonance.orbitMismatch", `Resonance target "${object.resonance.targetObjectId}" on "${object.id}" does not share a compatible orbital parent.`, object.id, "resonance"));
2784
+ }
2785
+ }
2786
+ for (const rule of object.deriveRules ?? []) {
2787
+ if (rule.field !== "period" || rule.strategy !== "kepler") {
2788
+ diagnostics.push(warn("validate.derive.unsupported", `Unsupported derive rule "${rule.field} ${rule.strategy}" on "${object.id}".`, object.id, "derive"));
2789
+ continue;
2790
+ }
2791
+ const derivedPeriodDays = keplerPeriodDays(object, parentObject);
2792
+ if (derivedPeriodDays === null) {
2793
+ diagnostics.push(warn("validate.derive.inputsMissing", `Object "${object.id}" requests "derive period kepler" but lacks enough input data.`, object.id, "derive"));
2794
+ continue;
2795
+ }
2796
+ if (!orbitPlacement?.period) {
2797
+ diagnostics.push(info("validate.derive.period.available", `Object "${object.id}" can derive a Kepler period of ${formatDays(derivedPeriodDays)}.`, object.id, "derive"));
2798
+ }
2799
+ }
2800
+ for (const rule of object.validationRules ?? []) {
2801
+ if (rule.rule !== "kepler") {
2802
+ diagnostics.push(warn("validate.rule.unsupported", `Unsupported validation rule "${rule.rule}" on "${object.id}".`, object.id, "validate"));
2803
+ continue;
2804
+ }
2805
+ const actualPeriodDays = durationInDays(orbitPlacement?.period);
2806
+ const derivedPeriodDays = keplerPeriodDays(object, parentObject);
2807
+ if (actualPeriodDays === null || derivedPeriodDays === null) {
2808
+ continue;
2809
+ }
2810
+ const toleranceDays = toleranceForField(object, "period");
2811
+ if (Math.abs(actualPeriodDays - derivedPeriodDays) > toleranceDays) {
2812
+ diagnostics.push(error("validate.kepler.mismatch", `Object "${object.id}" fails Kepler validation for "period".`, object.id, "validate"));
2813
+ }
2814
+ }
2815
+ }
2816
+ function validateAtTarget(object, objectMap, diagnostics) {
2817
+ const reference = object.placement?.mode === "at" ? object.placement.reference : null;
2818
+ if (!reference) {
2819
+ return true;
2820
+ }
2821
+ if (reference.kind === "named") {
2822
+ return objectMap.has(reference.name);
2823
+ }
2824
+ if (reference.kind === "anchor") {
2825
+ if (!objectMap.has(reference.objectId)) {
2826
+ diagnostics.push(error("validate.anchor.target.unknown", `Unknown anchor target "${reference.objectId}" on "${object.id}".`, object.id, "at"));
2827
+ return false;
2828
+ }
2829
+ return true;
2830
+ }
2831
+ if (!objectMap.has(reference.primary)) {
2832
+ diagnostics.push(error("validate.lagrange.primary.unknown", `Unknown Lagrange reference "${reference.primary}" on "${object.id}".`, object.id, "at"));
2833
+ return false;
2834
+ }
2835
+ if (reference.secondary && !objectMap.has(reference.secondary)) {
2836
+ diagnostics.push(error("validate.lagrange.secondary.unknown", `Unknown Lagrange reference "${reference.secondary}" on "${object.id}".`, object.id, "at"));
2837
+ return false;
2838
+ }
2839
+ return true;
2840
+ }
2841
+ function keplerPeriodDays(object, parentObject) {
2842
+ const placement = object.placement;
2843
+ if (!placement || placement.mode !== "orbit") {
2844
+ return null;
2845
+ }
2846
+ const semiMajorAu = distanceInAu(placement.semiMajor ?? placement.distance);
2847
+ const centralMassSolar = massInSolar(parentObject?.properties.mass);
2848
+ if (semiMajorAu === null || centralMassSolar === null || centralMassSolar <= 0) {
2849
+ return null;
2850
+ }
2851
+ const periodYears = Math.sqrt(semiMajorAu ** 3 / centralMassSolar);
2852
+ return periodYears * 365.25;
2853
+ }
2854
+ function distanceInAu(value) {
2855
+ if (!value)
2856
+ return null;
2857
+ switch (value.unit) {
2858
+ case null:
2859
+ case "au":
2860
+ return value.value;
2861
+ case "km":
2862
+ return value.value / AU_IN_KM2;
2863
+ case "m":
2864
+ return value.value / (AU_IN_KM2 * 1e3);
2865
+ case "ly":
2866
+ return value.value * LY_IN_AU2;
2867
+ case "pc":
2868
+ return value.value * PC_IN_AU2;
2869
+ case "kpc":
2870
+ return value.value * KPC_IN_AU2;
2871
+ case "re":
2872
+ return value.value * EARTH_RADIUS_IN_KM2 / AU_IN_KM2;
2873
+ case "sol":
2874
+ return value.value * SOLAR_RADIUS_IN_KM2 / AU_IN_KM2;
2875
+ default:
2876
+ return null;
2877
+ }
2878
+ }
2879
+ function massInSolar(value) {
2880
+ if (!value || typeof value !== "object" || !("value" in value)) {
2881
+ return null;
2882
+ }
2883
+ const unitValue = value;
2884
+ switch (unitValue.unit) {
2885
+ case null:
2886
+ case "sol":
2887
+ return unitValue.value;
2888
+ case "me":
2889
+ return unitValue.value / EARTH_MASSES_PER_SOLAR;
2890
+ case "mj":
2891
+ return unitValue.value / JUPITER_MASSES_PER_SOLAR;
2892
+ default:
2893
+ return null;
2894
+ }
2895
+ }
2896
+ function durationInDays(value) {
2897
+ if (!value)
2898
+ return null;
2899
+ switch (value.unit) {
2900
+ case null:
2901
+ case "d":
2902
+ return value.value;
2903
+ case "s":
2904
+ return value.value / 86400;
2905
+ case "min":
2906
+ return value.value / 1440;
2907
+ case "h":
2908
+ return value.value / 24;
2909
+ case "y":
2910
+ return value.value * 365.25;
2911
+ case "ky":
2912
+ return value.value * 365250;
2913
+ case "my":
2914
+ return value.value * 36525e4;
2915
+ case "gy":
2916
+ return value.value * 36525e7;
2917
+ default:
2918
+ return null;
2919
+ }
2920
+ }
2921
+ function toleranceForField(object, field) {
2922
+ const tolerance = object.tolerances?.find((entry) => entry.field === field)?.value;
2923
+ if (typeof tolerance === "number") {
2924
+ return tolerance;
2925
+ }
2926
+ if (tolerance && typeof tolerance === "object" && "value" in tolerance) {
2927
+ return durationInDays(tolerance) ?? 0;
2928
+ }
2929
+ return 0;
2930
+ }
2931
+ function formatDays(days) {
2932
+ return `${Math.round(days * 100) / 100}d`;
2933
+ }
2934
+ function error(code, message, objectId, field) {
2935
+ return { code, severity: "error", source: "validate", message, objectId, field };
2936
+ }
2937
+ function warn(code, message, objectId, field) {
2938
+ return { code, severity: "warning", source: "validate", message, objectId, field };
2939
+ }
2940
+ function info(code, message, objectId, field) {
2941
+ return { code, severity: "info", source: "validate", message, objectId, field };
2942
+ }
2943
+
2407
2944
  // packages/core/dist/draft-parse.js
2945
+ var STRUCTURED_TYPED_BLOCKS = /* @__PURE__ */ new Set([
2946
+ "climate",
2947
+ "habitability",
2948
+ "settlement"
2949
+ ]);
2950
+ var DRAFT_OBJECT_FIELD_SPECS = /* @__PURE__ */ new Map();
2951
+ for (const key of [
2952
+ "orbit",
2953
+ "distance",
2954
+ "semiMajor",
2955
+ "eccentricity",
2956
+ "period",
2957
+ "angle",
2958
+ "inclination",
2959
+ "phase",
2960
+ "at",
2961
+ "surface",
2962
+ "free",
2963
+ "kind",
2964
+ "class",
2965
+ "culture",
2966
+ "tags",
2967
+ "color",
2968
+ "image",
2969
+ "hidden",
2970
+ "radius",
2971
+ "mass",
2972
+ "density",
2973
+ "gravity",
2974
+ "temperature",
2975
+ "albedo",
2976
+ "atmosphere",
2977
+ "inner",
2978
+ "outer",
2979
+ "on",
2980
+ "source",
2981
+ "cycle"
2982
+ ]) {
2983
+ const schema = getFieldSchema(key);
2984
+ if (schema) {
2985
+ DRAFT_OBJECT_FIELD_SPECS.set(key, {
2986
+ key,
2987
+ version: "2.0",
2988
+ inlineMode: schema.arity === "multiple" ? "multiple" : "single",
2989
+ allowRepeat: false,
2990
+ legacySchema: schema
2991
+ });
2992
+ }
2993
+ }
2994
+ for (const spec of [
2995
+ { key: "groups", inlineMode: "multiple", allowRepeat: false },
2996
+ { key: "epoch", inlineMode: "single", allowRepeat: false },
2997
+ { key: "referencePlane", inlineMode: "single", allowRepeat: false },
2998
+ { key: "tidalLock", inlineMode: "single", allowRepeat: false },
2999
+ { key: "renderLabel", inlineMode: "single", allowRepeat: false },
3000
+ { key: "renderOrbit", inlineMode: "single", allowRepeat: false },
3001
+ { key: "renderPriority", inlineMode: "single", allowRepeat: false },
3002
+ { key: "resonance", inlineMode: "pair", allowRepeat: false },
3003
+ { key: "derive", inlineMode: "pair", allowRepeat: true },
3004
+ { key: "validate", inlineMode: "single", allowRepeat: true },
3005
+ { key: "locked", inlineMode: "multiple", allowRepeat: false },
3006
+ { key: "tolerance", inlineMode: "pair", allowRepeat: true }
3007
+ ]) {
3008
+ DRAFT_OBJECT_FIELD_SPECS.set(spec.key, {
3009
+ key: spec.key,
3010
+ version: "2.1",
3011
+ inlineMode: spec.inlineMode,
3012
+ allowRepeat: spec.allowRepeat
3013
+ });
3014
+ }
3015
+ var DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
2408
3016
  function parseWorldOrbitAtlas(source) {
2409
- return parseAtlasSource(source, "2.0");
3017
+ return parseAtlasSource(source);
2410
3018
  }
2411
- function parseAtlasSource(source, outputVersion) {
2412
- const lines = source.split(/\r?\n/);
3019
+ function parseAtlasSource(source, forcedOutputVersion) {
3020
+ const prepared = preprocessAtlasSource(source);
3021
+ const lines = prepared.source.split(/\r?\n/);
3022
+ const diagnostics = [];
2413
3023
  let sawSchemaHeader = false;
2414
- let schemaVersion = "2.0";
3024
+ let sourceSchemaVersion = "2.0";
2415
3025
  let system = null;
2416
3026
  let section = null;
2417
3027
  const objectNodes = [];
3028
+ const groups = [];
3029
+ const relations = [];
2418
3030
  let sawDefaults = false;
2419
3031
  let sawAtlas = false;
2420
3032
  const viewpointIds = /* @__PURE__ */ new Set();
2421
3033
  const annotationIds = /* @__PURE__ */ new Set();
3034
+ const groupIds = /* @__PURE__ */ new Set();
3035
+ const relationIds = /* @__PURE__ */ new Set();
2422
3036
  for (let index = 0; index < lines.length; index++) {
2423
3037
  const rawLine = lines[index];
2424
3038
  const lineNumber = index + 1;
@@ -2434,15 +3048,22 @@
2434
3048
  continue;
2435
3049
  }
2436
3050
  if (!sawSchemaHeader) {
2437
- schemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
3051
+ sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
2438
3052
  sawSchemaHeader = true;
3053
+ if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
3054
+ diagnostics.push({
3055
+ code: "parse.schema21.commentCompatibility",
3056
+ severity: "warning",
3057
+ source: "parse",
3058
+ message: `Comments require schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
3059
+ line: prepared.comments[0].line,
3060
+ column: prepared.comments[0].column
3061
+ });
3062
+ }
2439
3063
  continue;
2440
3064
  }
2441
3065
  if (indent === 0) {
2442
- section = startTopLevelSection(tokens, lineNumber, system, objectNodes, viewpointIds, annotationIds, {
2443
- sawDefaults,
2444
- sawAtlas
2445
- });
3066
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, { sawDefaults, sawAtlas });
2446
3067
  if (section.kind === "system") {
2447
3068
  system = section.system;
2448
3069
  } else if (section.kind === "defaults") {
@@ -2460,48 +3081,57 @@
2460
3081
  if (!sawSchemaHeader) {
2461
3082
  throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
2462
3083
  }
2463
- const ast = {
2464
- type: "document",
2465
- objects: objectNodes
2466
- };
2467
- const normalizedObjects = normalizeDocument(ast).objects;
2468
- validateDocument({
2469
- format: "worldorbit",
2470
- version: "1.0",
2471
- system: null,
2472
- objects: normalizedObjects
2473
- });
2474
- const diagnostics = schemaVersion === "2.0-draft" && outputVersion === "2.0" ? [
2475
- {
2476
- code: "load.schema.deprecatedDraft",
2477
- severity: "warning",
2478
- source: "upgrade",
2479
- message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".'
2480
- }
2481
- ] : [];
2482
- return {
3084
+ const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
3085
+ const outputVersion = forcedOutputVersion ?? (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
3086
+ const baseDocument = {
2483
3087
  format: "worldorbit",
2484
- version: outputVersion,
2485
3088
  sourceVersion: "1.0",
2486
3089
  system,
2487
- objects: normalizedObjects,
3090
+ groups,
3091
+ relations,
3092
+ objects,
2488
3093
  diagnostics
2489
3094
  };
2490
- }
3095
+ if (outputVersion === "2.0-draft") {
3096
+ const document3 = {
3097
+ ...baseDocument,
3098
+ version: "2.0-draft",
3099
+ schemaVersion: "2.0-draft"
3100
+ };
3101
+ document3.diagnostics.push(...collectAtlasDiagnostics(document3, sourceSchemaVersion));
3102
+ return document3;
3103
+ }
3104
+ const document2 = {
3105
+ ...baseDocument,
3106
+ version: outputVersion,
3107
+ schemaVersion: outputVersion
3108
+ };
3109
+ if (sourceSchemaVersion === "2.0-draft") {
3110
+ document2.diagnostics.push({
3111
+ code: "load.schema.deprecatedDraft",
3112
+ severity: "warning",
3113
+ source: "upgrade",
3114
+ message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".'
3115
+ });
3116
+ }
3117
+ document2.diagnostics.push(...collectAtlasDiagnostics(document2, sourceSchemaVersion));
3118
+ return document2;
3119
+ }
2491
3120
  function assertDraftSchemaHeader(tokens, line) {
2492
- if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || tokens[1].value.toLowerCase() !== "2.0-draft" && tokens[1].value.toLowerCase() !== "2.0") {
2493
- throw new WorldOrbitError('Expected atlas header "schema 2.0" or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
3121
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
3122
+ throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
2494
3123
  }
2495
- return tokens[1].value.toLowerCase() === "2.0-draft" ? "2.0-draft" : "2.0";
3124
+ const version = tokens[1].value.toLowerCase();
3125
+ return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
2496
3126
  }
2497
- function startTopLevelSection(tokens, line, system, objectNodes, viewpointIds, annotationIds, flags) {
3127
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, flags) {
2498
3128
  const keyword = tokens[0]?.value.toLowerCase();
2499
3129
  switch (keyword) {
2500
3130
  case "system":
2501
3131
  if (system) {
2502
3132
  throw new WorldOrbitError('Atlas section "system" may only appear once', line, tokens[0].column);
2503
3133
  }
2504
- return startSystemSection(tokens, line);
3134
+ return startSystemSection(tokens, line, sourceSchemaVersion, diagnostics);
2505
3135
  case "defaults":
2506
3136
  if (!system) {
2507
3137
  throw new WorldOrbitError('Atlas section "defaults" requires a preceding system declaration', line, tokens[0].column);
@@ -2537,13 +3167,19 @@
2537
3167
  throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
2538
3168
  }
2539
3169
  return startAnnotationSection(tokens, line, system, annotationIds);
3170
+ case "group":
3171
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "group", { line, column: tokens[0].column });
3172
+ return startGroupSection(tokens, line, groups, groupIds);
3173
+ case "relation":
3174
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
3175
+ return startRelationSection(tokens, line, relations, relationIds);
2540
3176
  case "object":
2541
- return startObjectSection(tokens, line, objectNodes);
3177
+ return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
2542
3178
  default:
2543
3179
  throw new WorldOrbitError(`Unknown atlas section "${tokens[0]?.value ?? ""}"`, line, tokens[0]?.column ?? 1);
2544
3180
  }
2545
3181
  }
2546
- function startSystemSection(tokens, line) {
3182
+ function startSystemSection(tokens, line, sourceSchemaVersion, diagnostics) {
2547
3183
  if (tokens.length !== 2) {
2548
3184
  throw new WorldOrbitError("Invalid atlas system declaration", line, tokens[0]?.column ?? 1);
2549
3185
  }
@@ -2551,6 +3187,9 @@
2551
3187
  type: "system",
2552
3188
  id: tokens[1].value,
2553
3189
  title: null,
3190
+ description: null,
3191
+ epoch: null,
3192
+ referencePlane: null,
2554
3193
  defaults: {
2555
3194
  view: "topdown",
2556
3195
  scale: null,
@@ -2565,6 +3204,8 @@
2565
3204
  return {
2566
3205
  kind: "system",
2567
3206
  system,
3207
+ sourceSchemaVersion,
3208
+ diagnostics,
2568
3209
  seenFields: /* @__PURE__ */ new Set()
2569
3210
  };
2570
3211
  }
@@ -2630,7 +3271,64 @@
2630
3271
  seenFields: /* @__PURE__ */ new Set()
2631
3272
  };
2632
3273
  }
2633
- function startObjectSection(tokens, line, objectNodes) {
3274
+ function startGroupSection(tokens, line, groups, groupIds) {
3275
+ if (tokens.length !== 2) {
3276
+ throw new WorldOrbitError("Invalid group declaration", line, tokens[0]?.column ?? 1);
3277
+ }
3278
+ const id = normalizeIdentifier(tokens[1].value);
3279
+ if (!id) {
3280
+ throw new WorldOrbitError("Group id must not be empty", line, tokens[1].column);
3281
+ }
3282
+ if (groupIds.has(id)) {
3283
+ throw new WorldOrbitError(`Duplicate group id "${id}"`, line, tokens[1].column);
3284
+ }
3285
+ const group = {
3286
+ id,
3287
+ label: humanizeIdentifier2(id),
3288
+ summary: "",
3289
+ color: null,
3290
+ tags: [],
3291
+ hidden: false
3292
+ };
3293
+ groups.push(group);
3294
+ groupIds.add(id);
3295
+ return {
3296
+ kind: "group",
3297
+ group,
3298
+ seenFields: /* @__PURE__ */ new Set()
3299
+ };
3300
+ }
3301
+ function startRelationSection(tokens, line, relations, relationIds) {
3302
+ if (tokens.length !== 2) {
3303
+ throw new WorldOrbitError("Invalid relation declaration", line, tokens[0]?.column ?? 1);
3304
+ }
3305
+ const id = normalizeIdentifier(tokens[1].value);
3306
+ if (!id) {
3307
+ throw new WorldOrbitError("Relation id must not be empty", line, tokens[1].column);
3308
+ }
3309
+ if (relationIds.has(id)) {
3310
+ throw new WorldOrbitError(`Duplicate relation id "${id}"`, line, tokens[1].column);
3311
+ }
3312
+ const relation = {
3313
+ id,
3314
+ from: "",
3315
+ to: "",
3316
+ kind: "",
3317
+ label: null,
3318
+ summary: null,
3319
+ tags: [],
3320
+ color: null,
3321
+ hidden: false
3322
+ };
3323
+ relations.push(relation);
3324
+ relationIds.add(id);
3325
+ return {
3326
+ kind: "relation",
3327
+ relation,
3328
+ seenFields: /* @__PURE__ */ new Set()
3329
+ };
3330
+ }
3331
+ function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
2634
3332
  if (tokens.length < 3) {
2635
3333
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
2636
3334
  }
@@ -2641,12 +3339,11 @@
2641
3339
  throw new WorldOrbitError(`Unknown object type "${objectTypeToken.value}"`, line, objectTypeToken.column);
2642
3340
  }
2643
3341
  const objectNode = {
2644
- type: "object",
2645
3342
  objectType,
2646
- name: idToken.value,
2647
- inlineFields: parseInlineFields2(tokens.slice(3), line),
2648
- blockFields: [],
3343
+ id: idToken.value,
3344
+ fields: parseInlineObjectFields(tokens.slice(3), line, objectType, sourceSchemaVersion, diagnostics),
2649
3345
  infoEntries: [],
3346
+ typedBlockEntries: {},
2650
3347
  location: {
2651
3348
  line,
2652
3349
  column: objectTypeToken.column
@@ -2656,8 +3353,12 @@
2656
3353
  return {
2657
3354
  kind: "object",
2658
3355
  objectNode,
2659
- inInfoBlock: false,
2660
- infoIndent: null
3356
+ sourceSchemaVersion,
3357
+ diagnostics,
3358
+ activeBlock: null,
3359
+ blockIndent: null,
3360
+ seenInfoKeys: /* @__PURE__ */ new Set(),
3361
+ seenTypedBlockKeys: {}
2661
3362
  };
2662
3363
  }
2663
3364
  function handleSectionLine(section, indent, tokens, line) {
@@ -2677,6 +3378,12 @@
2677
3378
  case "annotation":
2678
3379
  applyAnnotationField(section, tokens, line);
2679
3380
  return;
3381
+ case "group":
3382
+ applyGroupField(section, tokens, line);
3383
+ return;
3384
+ case "relation":
3385
+ applyRelationField(section, tokens, line);
3386
+ return;
2680
3387
  case "object":
2681
3388
  applyObjectField(section, indent, tokens, line);
2682
3389
  return;
@@ -2684,10 +3391,35 @@
2684
3391
  }
2685
3392
  function applySystemField(section, tokens, line) {
2686
3393
  const key = requireUniqueField(tokens, section.seenFields, line);
2687
- if (key !== "title") {
2688
- throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
3394
+ const value = joinFieldValue(tokens, line);
3395
+ switch (key) {
3396
+ case "title":
3397
+ section.system.title = value;
3398
+ return;
3399
+ case "description":
3400
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
3401
+ line,
3402
+ column: tokens[0].column
3403
+ });
3404
+ section.system.description = value;
3405
+ return;
3406
+ case "epoch":
3407
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
3408
+ line,
3409
+ column: tokens[0].column
3410
+ });
3411
+ section.system.epoch = value;
3412
+ return;
3413
+ case "referenceplane":
3414
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "referencePlane", {
3415
+ line,
3416
+ column: tokens[0].column
3417
+ });
3418
+ section.system.referencePlane = value;
3419
+ return;
3420
+ default:
3421
+ throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
2689
3422
  }
2690
- section.system.title = joinFieldValue(tokens, line);
2691
3423
  }
2692
3424
  function applyDefaultsField(section, tokens, line) {
2693
3425
  const key = requireUniqueField(tokens, section.seenFields, line);
@@ -2718,14 +3450,11 @@
2718
3450
  section.metadataIndent = null;
2719
3451
  }
2720
3452
  if (section.inMetadata) {
2721
- if (tokens.length < 2) {
2722
- throw new WorldOrbitError("Invalid atlas metadata entry", line, tokens[0]?.column ?? 1);
3453
+ const entry = parseInfoLikeEntry(tokens, line, "Invalid atlas metadata entry");
3454
+ if (entry.key in section.system.atlasMetadata) {
3455
+ throw new WorldOrbitError(`Duplicate atlas metadata key "${entry.key}"`, line, tokens[0].column);
2723
3456
  }
2724
- const key = tokens[0].value;
2725
- if (key in section.system.atlasMetadata) {
2726
- throw new WorldOrbitError(`Duplicate atlas metadata key "${key}"`, line, tokens[0].column);
2727
- }
2728
- section.system.atlasMetadata[key] = joinFieldValue(tokens, line);
3457
+ section.system.atlasMetadata[entry.key] = entry.value;
2729
3458
  return;
2730
3459
  }
2731
3460
  if (tokens.length === 1 && tokens[0].value.toLowerCase() === "metadata") {
@@ -2827,21 +3556,102 @@
2827
3556
  throw new WorldOrbitError(`Unknown annotation field "${tokens[0].value}"`, line, tokens[0].column);
2828
3557
  }
2829
3558
  }
2830
- function applyObjectField(section, indent, tokens, line) {
2831
- if (tokens.length === 1 && tokens[0].value === "info") {
2832
- section.inInfoBlock = true;
2833
- section.infoIndent = indent;
2834
- return;
3559
+ function applyGroupField(section, tokens, line) {
3560
+ const key = requireUniqueField(tokens, section.seenFields, line);
3561
+ switch (key) {
3562
+ case "label":
3563
+ section.group.label = joinFieldValue(tokens, line);
3564
+ return;
3565
+ case "summary":
3566
+ section.group.summary = joinFieldValue(tokens, line);
3567
+ return;
3568
+ case "color":
3569
+ section.group.color = joinFieldValue(tokens, line);
3570
+ return;
3571
+ case "tags":
3572
+ section.group.tags = parseTokenList(tokens.slice(1), line, "tags");
3573
+ return;
3574
+ case "hidden":
3575
+ section.group.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
3576
+ line,
3577
+ column: tokens[0].column
3578
+ });
3579
+ return;
3580
+ default:
3581
+ throw new WorldOrbitError(`Unknown group field "${tokens[0].value}"`, line, tokens[0].column);
3582
+ }
3583
+ }
3584
+ function applyRelationField(section, tokens, line) {
3585
+ const key = requireUniqueField(tokens, section.seenFields, line);
3586
+ switch (key) {
3587
+ case "from":
3588
+ section.relation.from = joinFieldValue(tokens, line);
3589
+ return;
3590
+ case "to":
3591
+ section.relation.to = joinFieldValue(tokens, line);
3592
+ return;
3593
+ case "kind":
3594
+ section.relation.kind = joinFieldValue(tokens, line);
3595
+ return;
3596
+ case "label":
3597
+ section.relation.label = joinFieldValue(tokens, line);
3598
+ return;
3599
+ case "summary":
3600
+ section.relation.summary = joinFieldValue(tokens, line);
3601
+ return;
3602
+ case "tags":
3603
+ section.relation.tags = parseTokenList(tokens.slice(1), line, "tags");
3604
+ return;
3605
+ case "color":
3606
+ section.relation.color = joinFieldValue(tokens, line);
3607
+ return;
3608
+ case "hidden":
3609
+ section.relation.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
3610
+ line,
3611
+ column: tokens[0].column
3612
+ });
3613
+ return;
3614
+ default:
3615
+ throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
2835
3616
  }
2836
- if (section.inInfoBlock && indent <= (section.infoIndent ?? 0)) {
2837
- section.inInfoBlock = false;
2838
- section.infoIndent = null;
3617
+ }
3618
+ function applyObjectField(section, indent, tokens, line) {
3619
+ if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
3620
+ section.activeBlock = null;
3621
+ section.blockIndent = null;
3622
+ }
3623
+ if (tokens.length === 1) {
3624
+ const blockName = tokens[0].value.toLowerCase();
3625
+ if (blockName === "info" || STRUCTURED_TYPED_BLOCKS.has(blockName)) {
3626
+ if (blockName !== "info") {
3627
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, blockName, { line, column: tokens[0].column });
3628
+ }
3629
+ section.activeBlock = blockName;
3630
+ section.blockIndent = indent;
3631
+ return;
3632
+ }
2839
3633
  }
2840
- if (section.inInfoBlock) {
2841
- section.objectNode.infoEntries.push(parseInfoEntry2(tokens, line));
3634
+ if (section.activeBlock) {
3635
+ const entry = parseInfoLikeEntry(tokens, line, `Invalid ${section.activeBlock} entry`);
3636
+ if (section.activeBlock === "info") {
3637
+ if (section.seenInfoKeys.has(entry.key)) {
3638
+ throw new WorldOrbitError(`Duplicate info key "${entry.key}"`, line, tokens[0].column);
3639
+ }
3640
+ section.seenInfoKeys.add(entry.key);
3641
+ section.objectNode.infoEntries.push(entry);
3642
+ return;
3643
+ }
3644
+ const typedBlock = section.activeBlock;
3645
+ const seenKeys = section.seenTypedBlockKeys[typedBlock] ?? (section.seenTypedBlockKeys[typedBlock] = /* @__PURE__ */ new Set());
3646
+ if (seenKeys.has(entry.key)) {
3647
+ throw new WorldOrbitError(`Duplicate ${typedBlock} key "${entry.key}"`, line, tokens[0].column);
3648
+ }
3649
+ seenKeys.add(entry.key);
3650
+ const entries = section.objectNode.typedBlockEntries[typedBlock] ?? (section.objectNode.typedBlockEntries[typedBlock] = []);
3651
+ entries.push(entry);
2842
3652
  return;
2843
3653
  }
2844
- section.objectNode.blockFields.push(parseField2(tokens, line));
3654
+ section.objectNode.fields.push(parseObjectField(tokens, line, section.objectNode.objectType, section.sourceSchemaVersion, section.diagnostics));
2845
3655
  }
2846
3656
  function requireUniqueField(tokens, seenFields, line) {
2847
3657
  if (tokens.length < 2) {
@@ -2861,50 +3671,40 @@
2861
3671
  return tokens.slice(1).map((token) => token.value).join(" ").trim();
2862
3672
  }
2863
3673
  function parseObjectTypeTokens(tokens, line) {
2864
- if (tokens.length === 0) {
2865
- throw new WorldOrbitError("Missing value for atlas field", line);
2866
- }
2867
- return tokens.map((token) => {
2868
- const value = token.value;
2869
- if (value !== "star" && value !== "planet" && value !== "moon" && value !== "belt" && value !== "asteroid" && value !== "comet" && value !== "ring" && value !== "structure" && value !== "phenomenon") {
2870
- throw new WorldOrbitError(`Unknown viewpoint object type "${token.value}"`, line, token.column);
2871
- }
2872
- return value;
2873
- });
2874
- }
2875
- function parseTokenList(tokens, line, field) {
2876
- if (tokens.length === 0) {
2877
- throw new WorldOrbitError(`Missing value for field "${field}"`, line);
2878
- }
2879
- return tokens.map((token) => token.value);
3674
+ 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");
2880
3675
  }
2881
3676
  function parseLayerTokens(tokens, line) {
2882
- if (tokens.length === 0) {
2883
- throw new WorldOrbitError('Missing value for field "layers"', line);
2884
- }
2885
- const next = {};
2886
- for (const token of tokens) {
2887
- const enabled = !token.value.startsWith("-") && !token.value.startsWith("!");
2888
- const rawLayer = token.value.replace(/^[-!]+/, "").toLowerCase();
2889
- if (rawLayer === "orbits") {
2890
- next["orbits-back"] = enabled;
2891
- next["orbits-front"] = enabled;
3677
+ const layers = {};
3678
+ for (const token of parseTokenList(tokens, line, "layers")) {
3679
+ const enabled = !token.startsWith("-") && !token.startsWith("!");
3680
+ const raw = token.replace(/^[-!]+/, "").toLowerCase();
3681
+ if (raw === "orbits") {
3682
+ layers["orbits-back"] = enabled;
3683
+ layers["orbits-front"] = enabled;
2892
3684
  continue;
2893
3685
  }
2894
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
2895
- next[rawLayer] = enabled;
2896
- continue;
3686
+ if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "objects" || raw === "labels" || raw === "metadata") {
3687
+ layers[raw] = enabled;
2897
3688
  }
2898
- throw new WorldOrbitError(`Unknown layer token "${token.value}"`, line, token.column);
2899
3689
  }
2900
- return next;
3690
+ return layers;
3691
+ }
3692
+ function parseTokenList(tokens, line, fieldName) {
3693
+ if (tokens.length === 0) {
3694
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, 1);
3695
+ }
3696
+ const values = tokens.map((token) => token.value).filter(Boolean);
3697
+ if (values.length === 0) {
3698
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, tokens[0]?.column ?? 1);
3699
+ }
3700
+ return values;
2901
3701
  }
2902
3702
  function parseProjectionValue(value, line, column) {
2903
3703
  const normalized = value.toLowerCase();
2904
- if (normalized === "topdown" || normalized === "isometric") {
2905
- return normalized;
3704
+ if (normalized !== "topdown" && normalized !== "isometric") {
3705
+ throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
2906
3706
  }
2907
- throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
3707
+ return normalized;
2908
3708
  }
2909
3709
  function parsePresetValue(value, line, column) {
2910
3710
  const normalized = value.toLowerCase();
@@ -2914,16 +3714,16 @@
2914
3714
  throw new WorldOrbitError(`Unknown render preset "${value}"`, line, column);
2915
3715
  }
2916
3716
  function parsePositiveNumber2(value, line, column, field) {
2917
- const parsed = Number(value);
2918
- if (!Number.isFinite(parsed) || parsed <= 0) {
2919
- throw new WorldOrbitError(`Field "${field}" expects a positive number`, line, column);
3717
+ const parsed = parseFiniteNumber2(value, line, column, field);
3718
+ if (parsed <= 0) {
3719
+ throw new WorldOrbitError(`Field "${field}" must be greater than zero`, line, column);
2920
3720
  }
2921
3721
  return parsed;
2922
3722
  }
2923
3723
  function parseFiniteNumber2(value, line, column, field) {
2924
3724
  const parsed = Number(value);
2925
3725
  if (!Number.isFinite(parsed)) {
2926
- throw new WorldOrbitError(`Field "${field}" expects a finite number`, line, column);
3726
+ throw new WorldOrbitError(`Invalid numeric value "${value}" for "${field}"`, line, column);
2927
3727
  }
2928
3728
  return parsed;
2929
3729
  }
@@ -2935,28 +3735,43 @@
2935
3735
  groupIds: []
2936
3736
  };
2937
3737
  }
2938
- function parseInlineFields2(tokens, line) {
3738
+ function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
2939
3739
  const fields = [];
2940
3740
  let index = 0;
2941
3741
  while (index < tokens.length) {
2942
3742
  const keyToken = tokens[index];
2943
- const schema = getFieldSchema(keyToken.value);
2944
- if (!schema) {
3743
+ const spec = getDraftObjectFieldSpec(keyToken.value);
3744
+ if (!spec) {
2945
3745
  throw new WorldOrbitError(`Unknown field "${keyToken.value}"`, line, keyToken.column);
2946
3746
  }
3747
+ if (spec.version === "2.1") {
3748
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, keyToken.value, {
3749
+ line,
3750
+ column: keyToken.column
3751
+ });
3752
+ }
2947
3753
  index++;
2948
3754
  const valueTokens = [];
2949
- if (schema.arity === "multiple") {
2950
- while (index < tokens.length && !isKnownFieldKey(tokens[index].value)) {
2951
- valueTokens.push(tokens[index]);
2952
- index++;
2953
- }
2954
- } else {
3755
+ if (spec.inlineMode === "single") {
2955
3756
  const nextToken = tokens[index];
2956
3757
  if (nextToken) {
2957
3758
  valueTokens.push(nextToken);
2958
3759
  index++;
2959
3760
  }
3761
+ } else if (spec.inlineMode === "pair") {
3762
+ for (let count = 0; count < 2; count++) {
3763
+ const nextToken = tokens[index];
3764
+ if (!nextToken) {
3765
+ break;
3766
+ }
3767
+ valueTokens.push(nextToken);
3768
+ index++;
3769
+ }
3770
+ } else {
3771
+ while (index < tokens.length && !DRAFT_OBJECT_FIELD_KEYS.has(tokens[index].value)) {
3772
+ valueTokens.push(tokens[index]);
3773
+ index++;
3774
+ }
2960
3775
  }
2961
3776
  if (valueTokens.length === 0) {
2962
3777
  throw new WorldOrbitError(`Missing value for field "${keyToken.value}"`, line, keyToken.column);
@@ -2968,25 +3783,35 @@
2968
3783
  location: { line, column: keyToken.column }
2969
3784
  });
2970
3785
  }
3786
+ validateDraftObjectFieldCompatibility(fields, objectType);
2971
3787
  return fields;
2972
3788
  }
2973
- function parseField2(tokens, line) {
3789
+ function parseObjectField(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
2974
3790
  if (tokens.length < 2) {
2975
3791
  throw new WorldOrbitError("Invalid field line", line, tokens[0]?.column ?? 1);
2976
3792
  }
2977
- if (!getFieldSchema(tokens[0].value)) {
3793
+ const spec = getDraftObjectFieldSpec(tokens[0].value);
3794
+ if (!spec) {
2978
3795
  throw new WorldOrbitError(`Unknown field "${tokens[0].value}"`, line, tokens[0].column);
2979
3796
  }
2980
- return {
3797
+ if (spec.version === "2.1") {
3798
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, tokens[0].value, {
3799
+ line,
3800
+ column: tokens[0].column
3801
+ });
3802
+ }
3803
+ const field = {
2981
3804
  type: "field",
2982
3805
  key: tokens[0].value,
2983
3806
  values: tokens.slice(1).map((token) => token.value),
2984
3807
  location: { line, column: tokens[0].column }
2985
3808
  };
3809
+ validateDraftObjectFieldCompatibility([field], objectType);
3810
+ return field;
2986
3811
  }
2987
- function parseInfoEntry2(tokens, line) {
3812
+ function parseInfoLikeEntry(tokens, line, errorMessage) {
2988
3813
  if (tokens.length < 2) {
2989
- throw new WorldOrbitError("Invalid info entry", line, tokens[0]?.column ?? 1);
3814
+ throw new WorldOrbitError(errorMessage, line, tokens[0]?.column ?? 1);
2990
3815
  }
2991
3816
  return {
2992
3817
  type: "info-entry",
@@ -2995,18 +3820,348 @@
2995
3820
  location: { line, column: tokens[0].column }
2996
3821
  };
2997
3822
  }
2998
- function normalizeIdentifier(value) {
2999
- return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
3823
+ function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
3824
+ const fieldMap = collectDraftFields(node.fields);
3825
+ const placement = extractDraftPlacement(node.objectType, fieldMap);
3826
+ const properties = normalizeDraftProperties(node.objectType, fieldMap);
3827
+ const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
3828
+ const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
3829
+ const referencePlane = parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0]);
3830
+ const tidalLock = fieldMap.has("tidalLock") ? parseAtlasBoolean(singleFieldValue2(fieldMap.get("tidalLock")[0]), "tidalLock", fieldMap.get("tidalLock")[0].location) : void 0;
3831
+ const resonance = fieldMap.has("resonance") ? parseResonanceField(fieldMap.get("resonance")[0]) : void 0;
3832
+ const renderHints = extractRenderHints(fieldMap);
3833
+ const deriveRules = fieldMap.get("derive")?.map((field) => parseDeriveField(field));
3834
+ const validationRules = fieldMap.get("validate")?.map((field) => ({
3835
+ rule: singleFieldValue2(field)
3836
+ }));
3837
+ const lockedFields = fieldMap.has("locked") ? [...new Set(fieldMap.get("locked").flatMap((field) => field.values))] : void 0;
3838
+ const tolerances = fieldMap.get("tolerance")?.map((field) => parseToleranceField(field));
3839
+ const typedBlocks = normalizeTypedBlocks(node.typedBlockEntries);
3840
+ const info2 = normalizeInfoEntries(node.infoEntries, "info");
3841
+ const object = {
3842
+ type: node.objectType,
3843
+ id: node.id,
3844
+ properties,
3845
+ placement,
3846
+ info: info2
3847
+ };
3848
+ if (groups.length > 0)
3849
+ object.groups = groups;
3850
+ if (epoch)
3851
+ object.epoch = epoch;
3852
+ if (referencePlane)
3853
+ object.referencePlane = referencePlane;
3854
+ if (tidalLock !== void 0)
3855
+ object.tidalLock = tidalLock;
3856
+ if (resonance)
3857
+ object.resonance = resonance;
3858
+ if (renderHints)
3859
+ object.renderHints = renderHints;
3860
+ if (deriveRules?.length)
3861
+ object.deriveRules = deriveRules;
3862
+ if (validationRules?.length)
3863
+ object.validationRules = validationRules;
3864
+ if (lockedFields?.length)
3865
+ object.lockedFields = lockedFields;
3866
+ if (tolerances?.length)
3867
+ object.tolerances = tolerances;
3868
+ if (typedBlocks && Object.keys(typedBlocks).length > 0)
3869
+ object.typedBlocks = typedBlocks;
3870
+ if (sourceSchemaVersion !== "2.1") {
3871
+ 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) {
3872
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
3873
+ }
3874
+ }
3875
+ return object;
3000
3876
  }
3001
- function humanizeIdentifier2(value) {
3002
- return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
3877
+ function collectDraftFields(fields) {
3878
+ const grouped = /* @__PURE__ */ new Map();
3879
+ for (const field of fields) {
3880
+ const spec = getDraftObjectFieldSpec(field.key);
3881
+ if (!spec) {
3882
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
3883
+ }
3884
+ if (!spec.allowRepeat && grouped.has(field.key)) {
3885
+ throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
3886
+ }
3887
+ const existing = grouped.get(field.key) ?? [];
3888
+ existing.push(field);
3889
+ grouped.set(field.key, existing);
3890
+ }
3891
+ return grouped;
3892
+ }
3893
+ function extractDraftPlacement(objectType, fieldMap) {
3894
+ const orbitField = fieldMap.get("orbit")?.[0];
3895
+ const atField = fieldMap.get("at")?.[0];
3896
+ const surfaceField = fieldMap.get("surface")?.[0];
3897
+ const freeField = fieldMap.get("free")?.[0];
3898
+ const count = [orbitField, atField, surfaceField, freeField].filter(Boolean).length;
3899
+ if (count > 1) {
3900
+ const conflictingField = orbitField ?? atField ?? surfaceField ?? freeField;
3901
+ throw WorldOrbitError.fromLocation("Object has multiple placement modes", conflictingField?.location);
3902
+ }
3903
+ if (orbitField) {
3904
+ return {
3905
+ mode: "orbit",
3906
+ target: singleFieldValue2(orbitField),
3907
+ distance: parseOptionalUnitField(fieldMap.get("distance")?.[0], "distance"),
3908
+ semiMajor: parseOptionalUnitField(fieldMap.get("semiMajor")?.[0], "semiMajor"),
3909
+ eccentricity: parseOptionalNumberField(fieldMap.get("eccentricity")?.[0], "eccentricity"),
3910
+ period: parseOptionalUnitField(fieldMap.get("period")?.[0], "period"),
3911
+ angle: parseOptionalUnitField(fieldMap.get("angle")?.[0], "angle"),
3912
+ inclination: parseOptionalUnitField(fieldMap.get("inclination")?.[0], "inclination"),
3913
+ phase: parseOptionalUnitField(fieldMap.get("phase")?.[0], "phase")
3914
+ };
3915
+ }
3916
+ if (atField) {
3917
+ const target = singleFieldValue2(atField);
3918
+ return {
3919
+ mode: "at",
3920
+ target,
3921
+ reference: parseAtlasAtReference(target, atField.location)
3922
+ };
3923
+ }
3924
+ if (surfaceField) {
3925
+ return {
3926
+ mode: "surface",
3927
+ target: singleFieldValue2(surfaceField)
3928
+ };
3929
+ }
3930
+ if (freeField) {
3931
+ const raw = singleFieldValue2(freeField);
3932
+ const distance = tryParseAtlasUnitValue(raw);
3933
+ return {
3934
+ mode: "free",
3935
+ distance: distance ?? void 0,
3936
+ descriptor: distance ? void 0 : raw
3937
+ };
3938
+ }
3939
+ return null;
3940
+ }
3941
+ function normalizeDraftProperties(objectType, fieldMap) {
3942
+ const properties = {};
3943
+ for (const [key, fields] of fieldMap.entries()) {
3944
+ const field = fields[0];
3945
+ const spec = getDraftObjectFieldSpec(key);
3946
+ if (!field || !spec?.legacySchema || spec.legacySchema.placement) {
3947
+ continue;
3948
+ }
3949
+ ensureAtlasFieldSupported(key, objectType, field.location);
3950
+ properties[key] = normalizeLegacyScalarValue(key, field.values, field.location);
3951
+ }
3952
+ return properties;
3953
+ }
3954
+ function normalizeInfoEntries(entries, label) {
3955
+ const normalized = {};
3956
+ for (const entry of entries) {
3957
+ if (entry.key in normalized) {
3958
+ throw WorldOrbitError.fromLocation(`Duplicate ${label} key "${entry.key}"`, entry.location);
3959
+ }
3960
+ normalized[entry.key] = entry.value;
3961
+ }
3962
+ return normalized;
3963
+ }
3964
+ function normalizeTypedBlocks(typedBlockEntries) {
3965
+ const typedBlocks = {};
3966
+ for (const blockName of Object.keys(typedBlockEntries)) {
3967
+ const entries = typedBlockEntries[blockName];
3968
+ if (entries?.length) {
3969
+ typedBlocks[blockName] = normalizeInfoEntries(entries, blockName);
3970
+ }
3971
+ }
3972
+ return typedBlocks;
3973
+ }
3974
+ function extractRenderHints(fieldMap) {
3975
+ const renderHints = {};
3976
+ const renderLabelField = fieldMap.get("renderLabel")?.[0];
3977
+ const renderOrbitField = fieldMap.get("renderOrbit")?.[0];
3978
+ const renderPriorityField = fieldMap.get("renderPriority")?.[0];
3979
+ if (renderLabelField) {
3980
+ renderHints.renderLabel = parseAtlasBoolean(singleFieldValue2(renderLabelField), "renderLabel", renderLabelField.location);
3981
+ }
3982
+ if (renderOrbitField) {
3983
+ renderHints.renderOrbit = parseAtlasBoolean(singleFieldValue2(renderOrbitField), "renderOrbit", renderOrbitField.location);
3984
+ }
3985
+ if (renderPriorityField) {
3986
+ renderHints.renderPriority = parseAtlasNumber(singleFieldValue2(renderPriorityField), "renderPriority", renderPriorityField.location);
3987
+ }
3988
+ return Object.keys(renderHints).length > 0 ? renderHints : void 0;
3989
+ }
3990
+ function parseResonanceField(field) {
3991
+ if (field.values.length !== 2) {
3992
+ throw WorldOrbitError.fromLocation('Field "resonance" expects "<targetObjectId> <ratio>"', field.location);
3993
+ }
3994
+ const ratio = field.values[1];
3995
+ if (!/^\d+:\d+$/.test(ratio)) {
3996
+ throw WorldOrbitError.fromLocation(`Invalid resonance ratio "${ratio}"`, field.location);
3997
+ }
3998
+ return {
3999
+ targetObjectId: field.values[0],
4000
+ ratio
4001
+ };
4002
+ }
4003
+ function parseDeriveField(field) {
4004
+ if (field.values.length !== 2) {
4005
+ throw WorldOrbitError.fromLocation('Field "derive" expects "<field> <strategy>"', field.location);
4006
+ }
4007
+ return {
4008
+ field: field.values[0],
4009
+ strategy: field.values[1]
4010
+ };
4011
+ }
4012
+ function parseToleranceField(field) {
4013
+ if (field.values.length !== 2) {
4014
+ throw WorldOrbitError.fromLocation('Field "tolerance" expects "<field> <value>"', field.location);
4015
+ }
4016
+ const rawValue = field.values[1];
4017
+ const unitValue = tryParseAtlasUnitValue(rawValue);
4018
+ const numericValue2 = Number(rawValue);
4019
+ return {
4020
+ field: field.values[0],
4021
+ value: unitValue ?? (Number.isFinite(numericValue2) ? numericValue2 : rawValue)
4022
+ };
4023
+ }
4024
+ function parseOptionalTokenList(field) {
4025
+ return field ? [...new Set(field.values)] : [];
4026
+ }
4027
+ function parseOptionalJoinedValue(field) {
4028
+ if (!field) {
4029
+ return null;
4030
+ }
4031
+ return field.values.join(" ").trim() || null;
4032
+ }
4033
+ function parseOptionalUnitField(field, key) {
4034
+ return field ? parseAtlasUnitValue(singleFieldValue2(field), field.location, key) : void 0;
4035
+ }
4036
+ function parseOptionalNumberField(field, key) {
4037
+ return field ? parseAtlasNumber(singleFieldValue2(field), key, field.location) : void 0;
4038
+ }
4039
+ function singleFieldValue2(field) {
4040
+ return singleAtlasValue(field.values, field.key, field.location);
4041
+ }
4042
+ function getDraftObjectFieldSpec(key) {
4043
+ return DRAFT_OBJECT_FIELD_SPECS.get(key);
4044
+ }
4045
+ function validateDraftObjectFieldCompatibility(fields, objectType) {
4046
+ for (const field of fields) {
4047
+ const spec = getDraftObjectFieldSpec(field.key);
4048
+ if (!spec) {
4049
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
4050
+ }
4051
+ if (spec.legacySchema) {
4052
+ ensureAtlasFieldSupported(field.key, objectType, field.location);
4053
+ continue;
4054
+ }
4055
+ if ((field.key === "renderLabel" || field.key === "renderOrbit" || field.key === "tidalLock") && field.values.length !== 1) {
4056
+ throw WorldOrbitError.fromLocation(`Field "${field.key}" expects exactly one value`, field.location);
4057
+ }
4058
+ }
4059
+ }
4060
+ function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
4061
+ if (sourceSchemaVersion === "2.1") {
4062
+ return;
4063
+ }
4064
+ diagnostics.push({
4065
+ code: "parse.schema21.featureCompatibility",
4066
+ severity: "warning",
4067
+ source: "parse",
4068
+ message: `Feature "${featureName}" requires schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
4069
+ line: location.line,
4070
+ column: location.column
4071
+ });
4072
+ }
4073
+ function preprocessAtlasSource(source) {
4074
+ const chars = [...source];
4075
+ const comments = [];
4076
+ let inString = false;
4077
+ let inBlockComment = false;
4078
+ let blockCommentStart = null;
4079
+ let line = 1;
4080
+ let column = 1;
4081
+ for (let index = 0; index < chars.length; index++) {
4082
+ const ch = chars[index];
4083
+ const next = chars[index + 1];
4084
+ if (inBlockComment) {
4085
+ if (ch === "*" && next === "/") {
4086
+ chars[index] = " ";
4087
+ chars[index + 1] = " ";
4088
+ inBlockComment = false;
4089
+ blockCommentStart = null;
4090
+ index++;
4091
+ column += 2;
4092
+ continue;
4093
+ }
4094
+ if (ch !== "\n" && ch !== "\r") {
4095
+ chars[index] = " ";
4096
+ }
4097
+ if (ch === "\n") {
4098
+ line++;
4099
+ column = 1;
4100
+ } else {
4101
+ column++;
4102
+ }
4103
+ continue;
4104
+ }
4105
+ if (!inString && ch === "/" && next === "*") {
4106
+ comments.push({ kind: "block", line, column });
4107
+ chars[index] = " ";
4108
+ chars[index + 1] = " ";
4109
+ inBlockComment = true;
4110
+ blockCommentStart = { line, column };
4111
+ index++;
4112
+ column += 2;
4113
+ continue;
4114
+ }
4115
+ if (!inString && ch === "#" && !isHexColorLiteral(chars, index)) {
4116
+ comments.push({ kind: "line", line, column });
4117
+ chars[index] = " ";
4118
+ let inner = index + 1;
4119
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
4120
+ chars[inner] = " ";
4121
+ inner++;
4122
+ }
4123
+ column += inner - index;
4124
+ index = inner - 1;
4125
+ continue;
4126
+ }
4127
+ if (ch === '"' && chars[index - 1] !== "\\") {
4128
+ inString = !inString;
4129
+ }
4130
+ if (ch === "\n") {
4131
+ line++;
4132
+ column = 1;
4133
+ } else {
4134
+ column++;
4135
+ }
4136
+ }
4137
+ if (inBlockComment) {
4138
+ throw WorldOrbitError.fromLocation("Unclosed block comment", blockCommentStart ?? void 0);
4139
+ }
4140
+ return {
4141
+ source: chars.join(""),
4142
+ comments
4143
+ };
4144
+ }
4145
+ function isHexColorLiteral(chars, start) {
4146
+ let index = start + 1;
4147
+ let length = 0;
4148
+ while (index < chars.length && /[0-9a-f]/i.test(chars[index] ?? "")) {
4149
+ index++;
4150
+ length++;
4151
+ }
4152
+ if (![3, 4, 6, 8].includes(length)) {
4153
+ return false;
4154
+ }
4155
+ const next = chars[index];
4156
+ return next === void 0 || next === " " || next === " " || next === "\r" || next === "\n";
3003
4157
  }
3004
4158
 
3005
4159
  // packages/core/dist/load.js
3006
- var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0)?$/i;
4160
+ var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1)?$/i;
4161
+ var ATLAS_SCHEMA_21_PATTERN = /^schema\s+2\.1$/i;
3007
4162
  var LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
3008
4163
  function detectWorldOrbitSchemaVersion(source) {
3009
- for (const line of source.split(/\r?\n/)) {
4164
+ for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
3010
4165
  const trimmed = line.trim();
3011
4166
  if (!trimmed) {
3012
4167
  continue;
@@ -3014,6 +4169,9 @@
3014
4169
  if (LEGACY_DRAFT_SCHEMA_PATTERN.test(trimmed)) {
3015
4170
  return "2.0-draft";
3016
4171
  }
4172
+ if (ATLAS_SCHEMA_21_PATTERN.test(trimmed)) {
4173
+ return "2.1";
4174
+ }
3017
4175
  if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
3018
4176
  return "2.0";
3019
4177
  }
@@ -3021,6 +4179,49 @@
3021
4179
  }
3022
4180
  return "1.0";
3023
4181
  }
4182
+ function stripCommentsForSchemaDetection(source) {
4183
+ const chars = [...source];
4184
+ let inString = false;
4185
+ let inBlockComment = false;
4186
+ for (let index = 0; index < chars.length; index++) {
4187
+ const ch = chars[index];
4188
+ const next = chars[index + 1];
4189
+ if (inBlockComment) {
4190
+ if (ch === "*" && next === "/") {
4191
+ chars[index] = " ";
4192
+ chars[index + 1] = " ";
4193
+ inBlockComment = false;
4194
+ index++;
4195
+ continue;
4196
+ }
4197
+ if (ch !== "\n" && ch !== "\r") {
4198
+ chars[index] = " ";
4199
+ }
4200
+ continue;
4201
+ }
4202
+ if (!inString && ch === "/" && next === "*") {
4203
+ chars[index] = " ";
4204
+ chars[index + 1] = " ";
4205
+ inBlockComment = true;
4206
+ index++;
4207
+ continue;
4208
+ }
4209
+ if (!inString && ch === "#") {
4210
+ chars[index] = " ";
4211
+ let inner = index + 1;
4212
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
4213
+ chars[inner] = " ";
4214
+ inner++;
4215
+ }
4216
+ index = inner - 1;
4217
+ continue;
4218
+ }
4219
+ if (ch === '"' && chars[index - 1] !== "\\") {
4220
+ inString = !inString;
4221
+ }
4222
+ }
4223
+ return chars.join("");
4224
+ }
3024
4225
  function loadWorldOrbitSource(source) {
3025
4226
  const result = loadWorldOrbitSourceWithDiagnostics(source);
3026
4227
  if (!result.ok || !result.value) {
@@ -3031,36 +4232,36 @@
3031
4232
  }
3032
4233
  function loadWorldOrbitSourceWithDiagnostics(source) {
3033
4234
  const schemaVersion = detectWorldOrbitSchemaVersion(source);
3034
- if (schemaVersion === "2.0" || schemaVersion === "2.0-draft") {
4235
+ if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1") {
3035
4236
  return loadAtlasSourceWithDiagnostics(source, schemaVersion);
3036
4237
  }
3037
4238
  let ast;
3038
4239
  try {
3039
4240
  ast = parseWorldOrbit(source);
3040
- } catch (error) {
4241
+ } catch (error2) {
3041
4242
  return {
3042
4243
  ok: false,
3043
4244
  value: null,
3044
- diagnostics: [diagnosticFromError(error, "parse")]
4245
+ diagnostics: [diagnosticFromError(error2, "parse")]
3045
4246
  };
3046
4247
  }
3047
4248
  let document2;
3048
4249
  try {
3049
4250
  document2 = normalizeDocument(ast);
3050
- } catch (error) {
4251
+ } catch (error2) {
3051
4252
  return {
3052
4253
  ok: false,
3053
4254
  value: null,
3054
- diagnostics: [diagnosticFromError(error, "normalize")]
4255
+ diagnostics: [diagnosticFromError(error2, "normalize")]
3055
4256
  };
3056
4257
  }
3057
4258
  try {
3058
4259
  validateDocument(document2);
3059
- } catch (error) {
4260
+ } catch (error2) {
3060
4261
  return {
3061
4262
  ok: false,
3062
4263
  value: null,
3063
- diagnostics: [diagnosticFromError(error, "validate")]
4264
+ diagnostics: [diagnosticFromError(error2, "validate")]
3064
4265
  };
3065
4266
  }
3066
4267
  return {
@@ -3080,30 +4281,29 @@
3080
4281
  let atlasDocument;
3081
4282
  try {
3082
4283
  atlasDocument = parseWorldOrbitAtlas(source);
3083
- } catch (error) {
4284
+ } catch (error2) {
3084
4285
  return {
3085
4286
  ok: false,
3086
4287
  value: null,
3087
- diagnostics: [diagnosticFromError(error, "parse", "load.atlas.failed")]
4288
+ diagnostics: [diagnosticFromError(error2, "parse", "load.atlas.failed")]
3088
4289
  };
3089
4290
  }
3090
- let document2;
3091
- try {
3092
- document2 = materializeAtlasDocument(atlasDocument);
3093
- } catch (error) {
4291
+ const atlasDiagnostics = [...atlasDocument.diagnostics];
4292
+ if (atlasDiagnostics.some((diagnostic) => diagnostic.severity === "error")) {
3094
4293
  return {
3095
4294
  ok: false,
3096
4295
  value: null,
3097
- diagnostics: [diagnosticFromError(error, "normalize", "load.atlas.materialize.failed")]
4296
+ diagnostics: atlasDiagnostics
3098
4297
  };
3099
4298
  }
4299
+ let document2;
3100
4300
  try {
3101
- validateDocument(document2);
3102
- } catch (error) {
4301
+ document2 = materializeAtlasDocument(atlasDocument);
4302
+ } catch (error2) {
3103
4303
  return {
3104
4304
  ok: false,
3105
4305
  value: null,
3106
- diagnostics: [diagnosticFromError(error, "validate", "load.atlas.validate.failed")]
4306
+ diagnostics: [diagnosticFromError(error2, "normalize", "load.atlas.materialize.failed")]
3107
4307
  };
3108
4308
  }
3109
4309
  const loaded = {
@@ -3112,12 +4312,12 @@
3112
4312
  document: document2,
3113
4313
  atlasDocument,
3114
4314
  draftDocument: atlasDocument,
3115
- diagnostics: [...atlasDocument.diagnostics]
4315
+ diagnostics: atlasDiagnostics
3116
4316
  };
3117
4317
  return {
3118
4318
  ok: true,
3119
4319
  value: loaded,
3120
- diagnostics: [...atlasDocument.diagnostics]
4320
+ diagnostics: atlasDiagnostics
3121
4321
  };
3122
4322
  }
3123
4323
 
@@ -3125,6 +4325,7 @@
3125
4325
  var DEFAULT_LAYERS = {
3126
4326
  background: true,
3127
4327
  guides: true,
4328
+ relations: true,
3128
4329
  orbits: true,
3129
4330
  objects: true,
3130
4331
  labels: true,
@@ -3139,6 +4340,7 @@
3139
4340
  backgroundGlow: "rgba(240, 180, 100, 0.18)",
3140
4341
  panel: "rgba(7, 17, 27, 0.9)",
3141
4342
  panelLine: "rgba(168, 207, 242, 0.18)",
4343
+ relation: "rgba(240, 180, 100, 0.42)",
3142
4344
  orbit: "rgba(163, 209, 255, 0.24)",
3143
4345
  orbitBand: "rgba(255, 190, 120, 0.28)",
3144
4346
  guide: "rgba(255, 255, 255, 0.04)",
@@ -3161,6 +4363,7 @@
3161
4363
  backgroundGlow: "rgba(120, 255, 215, 0.16)",
3162
4364
  panel: "rgba(7, 20, 30, 0.9)",
3163
4365
  panelLine: "rgba(120, 255, 215, 0.16)",
4366
+ relation: "rgba(156, 231, 255, 0.42)",
3164
4367
  orbit: "rgba(120, 255, 215, 0.2)",
3165
4368
  orbitBand: "rgba(137, 185, 255, 0.24)",
3166
4369
  guide: "rgba(255, 255, 255, 0.035)",
@@ -3183,6 +4386,7 @@
3183
4386
  backgroundGlow: "rgba(255, 127, 95, 0.18)",
3184
4387
  panel: "rgba(24, 9, 13, 0.9)",
3185
4388
  panelLine: "rgba(255, 166, 149, 0.16)",
4389
+ relation: "rgba(255, 178, 125, 0.42)",
3186
4390
  orbit: "rgba(255, 188, 164, 0.22)",
3187
4391
  orbitBand: "rgba(255, 214, 139, 0.24)",
3188
4392
  guide: "rgba(255, 255, 255, 0.03)",
@@ -3264,7 +4468,11 @@
3264
4468
  return false;
3265
4469
  }
3266
4470
  if (filter.groupIds?.length && (!object.groupId || !filter.groupIds.includes(object.groupId))) {
3267
- return false;
4471
+ const hasSemanticMatch = object.semanticGroupIds.length > 0 && filter.groupIds.some((groupId) => object.semanticGroupIds.includes(groupId));
4472
+ const hasLegacyMatch = Boolean(object.groupId && filter.groupIds.includes(object.groupId));
4473
+ if (!hasSemanticMatch && !hasLegacyMatch) {
4474
+ return false;
4475
+ }
3268
4476
  }
3269
4477
  if (filter.tags?.length) {
3270
4478
  const objectTags = Array.isArray(object.object.properties.tags) ? object.object.properties.tags.filter((entry) => typeof entry === "string") : [];
@@ -3320,6 +4528,7 @@
3320
4528
  const imageDefinitions = buildImageDefinitions(visibleObjects);
3321
4529
  const orbitMarkup = layers.orbits ? renderOrbitLayer(scene, visibleObjectIds, layers.structures) : { back: "", front: "" };
3322
4530
  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("") : "";
4531
+ 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("") : "";
3323
4532
  const objectMarkup = layers.objects ? visibleObjects.map((object) => renderSceneObject(object, options.selectedObjectId ?? null, theme)).join("") : "";
3324
4533
  const labelMarkup = layers.labels ? visibleLabels.map((label) => renderSceneLabel(scene, label, options.selectedObjectId ?? null)).join("") : "";
3325
4534
  const metadataMarkup = layers.metadata ? `<text class="wo-title" x="56" y="64">${escapeXml(scene.title)}</text>
@@ -3354,6 +4563,7 @@
3354
4563
  .wo-orbit-back { opacity: 0.38; stroke-dasharray: 8 6; }
3355
4564
  .wo-orbit-front { opacity: 0.9; }
3356
4565
  .wo-orbit-band { stroke: ${theme.orbitBand}; stroke-linecap: round; }
4566
+ .wo-relation { stroke: ${theme.relation}; stroke-width: 2; stroke-dasharray: 10 6; }
3357
4567
  .wo-leader { stroke: ${theme.leader}; stroke-width: 1.5; stroke-dasharray: 6 5; }
3358
4568
  .wo-label { fill: ${theme.ink}; font-family: ${theme.fontFamily}; font-weight: 600; letter-spacing: 0.02em; }
3359
4569
  .wo-label-secondary { fill: ${theme.muted}; font-family: ${theme.fontFamily}; font-weight: 500; }
@@ -3387,6 +4597,7 @@
3387
4597
  <g data-worldorbit-world-content="true">
3388
4598
  ${layers.orbits ? `<g data-layer-id="orbits-back">${orbitMarkup.back}</g>` : ""}
3389
4599
  ${layers.guides ? `<g data-layer-id="guides">${leaderMarkup}</g>` : ""}
4600
+ ${layers.relations ? `<g data-layer-id="relations">${relationMarkup}</g>` : ""}
3390
4601
  ${layers.objects ? `<g data-layer-id="objects">${objectMarkup}</g>` : ""}
3391
4602
  ${layers.orbits ? `<g data-layer-id="orbits-front">${orbitMarkup.front}</g>` : ""}
3392
4603
  ${layers.labels ? `<g data-layer-id="labels">${labelMarkup}</g>` : ""}
@@ -3430,10 +4641,11 @@
3430
4641
  function renderSceneObject(sceneObject, selectedObjectId, theme) {
3431
4642
  const { object, x, y, radius, visualRadius } = sceneObject;
3432
4643
  const selectionClass = selectedObjectId === sceneObject.objectId ? " wo-object-selected" : "";
4644
+ const kindClass = object.properties.kind ? ` wo-kind-${String(object.properties.kind).toLowerCase().replace(/[^a-z0-9-]/g, "-")}` : "";
3433
4645
  const palette = resolveObjectPalette(sceneObject, theme);
3434
4646
  const imageMarkup = renderObjectImage(sceneObject);
3435
4647
  const outlineMarkup = imageMarkup ? renderObjectBody(object, x, y, radius, palette, { outlineOnly: true }) : "";
3436
- 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}`)}">
4648
+ 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}`)}">
3437
4649
  <circle class="wo-selection-ring" cx="${x}" cy="${y}" r="${visualRadius + 8}" />
3438
4650
  ${renderAtmosphere(sceneObject, palette)}
3439
4651
  ${renderObjectBody(object, x, y, radius, palette)}
@@ -3467,8 +4679,33 @@
3467
4679
  <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
3468
4680
  case "structure":
3469
4681
  return `<polygon points="${diamondPoints(x, y, radius)}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
3470
- case "phenomenon":
4682
+ case "phenomenon": {
4683
+ const kind = String(object.properties.kind ?? "").toLowerCase().replace(/_/g, "-");
4684
+ if (options.outlineOnly) {
4685
+ if (kind === "black-hole" || kind === "nebula" || kind === "galaxy" || kind === "dwarf-galaxy") {
4686
+ return `<circle cx="${x}" cy="${y}" r="${radius}" fill="transparent" stroke="${palette.stroke}" stroke-width="1.4" />`;
4687
+ }
4688
+ return `<polygon points="${phenomenonPoints(x, y, radius)}" fill="transparent" stroke="${palette.stroke}" stroke-width="1.4" />`;
4689
+ }
4690
+ if (kind === "black-hole") {
4691
+ 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" />
4692
+ <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="2" />`;
4693
+ }
4694
+ if (kind === "galaxy") {
4695
+ return `<ellipse cx="${x}" cy="${y}" rx="${radius * 2.6}" ry="${radius}" fill="${palette.halo ?? "none"}" stroke="none" />
4696
+ <ellipse cx="${x}" cy="${y}" rx="${radius * 1.5}" ry="${radius * 0.42}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.2" />
4697
+ <circle cx="${x}" cy="${y}" r="${radius * 0.28}" fill="${palette.core ?? "#fff"}" stroke="none" />`;
4698
+ }
4699
+ if (kind === "dwarf-galaxy") {
4700
+ return `<ellipse cx="${x}" cy="${y}" rx="${radius * 1.6}" ry="${radius * 0.55}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1" />
4701
+ <circle cx="${x}" cy="${y}" r="${radius * 0.25}" fill="${palette.core ?? "#fff"}" stroke="none" />`;
4702
+ }
4703
+ if (kind === "nebula") {
4704
+ return `<circle cx="${x}" cy="${y}" r="${radius * 2.2}" fill="${palette.halo ?? "none"}" stroke="none" />
4705
+ <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1" />`;
4706
+ }
3471
4707
  return `<polygon points="${phenomenonPoints(x, y, radius)}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
4708
+ }
3472
4709
  }
3473
4710
  }
3474
4711
  function renderAtmosphere(sceneObject, palette) {
@@ -3537,7 +4774,8 @@
3537
4774
  }
3538
4775
  }
3539
4776
  function resolveObjectPalette(sceneObject, theme) {
3540
- const base = basePaletteForType(sceneObject.object.type, theme);
4777
+ const kind = String(sceneObject.object.properties.kind ?? "").toLowerCase().replace(/_/g, "-");
4778
+ const base = basePaletteForType(sceneObject.object.type, kind, theme);
3541
4779
  const customFill = sceneObject.fillColor && isColorLike(sceneObject.fillColor) ? sceneObject.fillColor : base.fill;
3542
4780
  const albedo = numericValue(sceneObject.object.properties.albedo);
3543
4781
  const temperature = numericValue(sceneObject.object.properties.temperature);
@@ -3553,7 +4791,7 @@
3553
4791
  tail: sceneObject.object.type === "comet" ? rgbaString(mixColors(fill, "#ffffff", 0.5) ?? fill, 0.72) : void 0
3554
4792
  };
3555
4793
  }
3556
- function basePaletteForType(type, theme) {
4794
+ function basePaletteForType(type, kind, theme) {
3557
4795
  switch (type) {
3558
4796
  case "star":
3559
4797
  return {
@@ -3575,8 +4813,26 @@
3575
4813
  case "structure":
3576
4814
  return { fill: theme.accentStrong, stroke: "#fff2ea" };
3577
4815
  case "phenomenon":
3578
- return { fill: "#78ffd7", stroke: "#e9fff7" };
4816
+ return kindPhenomenonPalette(kind);
4817
+ }
4818
+ }
4819
+ function kindPhenomenonPalette(kind) {
4820
+ if (kind === "galaxy") {
4821
+ return { fill: "rgba(165,125,255,0.55)", stroke: "rgba(210,185,255,0.75)", halo: "rgba(160,120,255,0.10)", core: "#ede0ff" };
4822
+ }
4823
+ if (kind === "dwarf-galaxy") {
4824
+ return { fill: "rgba(190,165,255,0.45)", stroke: "rgba(220,205,255,0.75)", core: "#ddd0ff" };
4825
+ }
4826
+ if (kind === "black-hole") {
4827
+ return { fill: "#040408", stroke: "#ff6a00", accentRing: "rgba(255,140,20,0.72)" };
4828
+ }
4829
+ if (kind === "nebula") {
4830
+ return { fill: "rgba(105,205,255,0.45)", stroke: "rgba(180,235,255,0.72)", halo: "rgba(100,200,255,0.08)" };
4831
+ }
4832
+ if (kind === "void") {
4833
+ return { fill: "#05080f", stroke: "rgba(130,160,255,0.4)" };
3579
4834
  }
4835
+ return { fill: "#78ffd7", stroke: "#e9fff7" };
3580
4836
  }
3581
4837
  function applyTemperatureAndAlbedo(baseColor, temperature, albedo, type) {
3582
4838
  let nextColor = baseColor;
@@ -3843,11 +5099,11 @@
3843
5099
  });
3844
5100
  }
3845
5101
  return `<figure class="${escapeAttribute3(options.className ?? "worldorbit-block worldorbit-static")}">${renderSceneToSvg(scene, options)}</figure>`;
3846
- } catch (error) {
5102
+ } catch (error2) {
3847
5103
  if (options.strict) {
3848
- throw error;
5104
+ throw error2;
3849
5105
  }
3850
- return renderWorldOrbitError(error instanceof Error ? error.message : String(error));
5106
+ return renderWorldOrbitError(error2 instanceof Error ? error2.message : String(error2));
3851
5107
  }
3852
5108
  }
3853
5109
  function renderWorldOrbitError(message) {