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
@@ -19,8 +19,8 @@ var WorldOrbit = (() => {
19
19
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
20
 
21
21
  // packages/markdown/dist/index.js
22
- var dist_exports = {};
23
- __export(dist_exports, {
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
24
  rehypeWorldOrbit: () => rehypeWorldOrbit,
25
25
  remarkWorldOrbit: () => remarkWorldOrbit,
26
26
  renderWorldOrbitBlock: () => renderWorldOrbitBlock,
@@ -329,13 +329,13 @@ var WorldOrbit = (() => {
329
329
  function unitFamilyAllowsUnit(family, unit) {
330
330
  switch (family) {
331
331
  case "distance":
332
- return unit === null || ["au", "km", "re", "sol"].includes(unit);
332
+ return unit === null || ["au", "km", "m", "ly", "pc", "kpc", "re", "sol"].includes(unit);
333
333
  case "radius":
334
- return unit === null || ["km", "re", "sol"].includes(unit);
334
+ return unit === null || ["km", "m", "re", "rj", "sol"].includes(unit);
335
335
  case "mass":
336
- return unit === null || ["me", "sol"].includes(unit);
336
+ return unit === null || ["me", "mj", "sol"].includes(unit);
337
337
  case "duration":
338
- return unit === null || ["h", "d", "y"].includes(unit);
338
+ return unit === null || ["s", "min", "h", "d", "y", "ky", "my", "gy"].includes(unit);
339
339
  case "angle":
340
340
  return unit === null || unit === "deg";
341
341
  case "generic":
@@ -539,7 +539,7 @@ var WorldOrbit = (() => {
539
539
  }
540
540
 
541
541
  // packages/core/dist/normalize.js
542
- var UNIT_PATTERN = /^(-?\d+(?:\.\d+)?)(au|km|re|sol|me|d|y|h|deg)?$/;
542
+ 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)?$/;
543
543
  var BOOLEAN_VALUES = /* @__PURE__ */ new Map([
544
544
  ["true", true],
545
545
  ["false", false],
@@ -564,7 +564,10 @@ var WorldOrbit = (() => {
564
564
  return {
565
565
  format: "worldorbit",
566
566
  version: "1.0",
567
+ schemaVersion: "1.0",
567
568
  system,
569
+ groups: [],
570
+ relations: [],
568
571
  objects
569
572
  };
570
573
  }
@@ -574,13 +577,17 @@ var WorldOrbit = (() => {
574
577
  const fieldMap = collectFields(mergedFields);
575
578
  const placement = extractPlacement(node.objectType, fieldMap);
576
579
  const properties = normalizeProperties(fieldMap);
577
- const info = normalizeInfo(node.infoEntries);
580
+ const info2 = normalizeInfo(node.infoEntries);
578
581
  if (node.objectType === "system") {
579
582
  return {
580
583
  type: "system",
581
584
  id: node.name,
585
+ title: typeof properties.title === "string" ? properties.title : null,
586
+ description: null,
587
+ epoch: null,
588
+ referencePlane: null,
582
589
  properties,
583
- info
590
+ info: info2
584
591
  };
585
592
  }
586
593
  return {
@@ -588,7 +595,7 @@ var WorldOrbit = (() => {
588
595
  id: node.name,
589
596
  properties,
590
597
  placement,
591
- info
598
+ info: info2
592
599
  };
593
600
  }
594
601
  function validateFieldCompatibility(objectType, fields) {
@@ -718,14 +725,14 @@ var WorldOrbit = (() => {
718
725
  }
719
726
  }
720
727
  function normalizeInfo(entries) {
721
- const info = {};
728
+ const info2 = {};
722
729
  for (const entry of entries) {
723
- if (entry.key in info) {
730
+ if (entry.key in info2) {
724
731
  throw WorldOrbitError.fromLocation(`Duplicate info key "${entry.key}"`, entry.location);
725
732
  }
726
- info[entry.key] = entry.value;
733
+ info2[entry.key] = entry.value;
727
734
  }
728
- return info;
735
+ return info2;
729
736
  }
730
737
  function parseAtReference(target, location) {
731
738
  if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
@@ -896,37 +903,41 @@ var WorldOrbit = (() => {
896
903
  }
897
904
 
898
905
  // packages/core/dist/diagnostics.js
899
- function diagnosticFromError(error, source, code = `${source}.failed`) {
900
- if (error instanceof WorldOrbitError) {
906
+ function diagnosticFromError(error2, source, code = `${source}.failed`) {
907
+ if (error2 instanceof WorldOrbitError) {
901
908
  return {
902
909
  code,
903
910
  severity: "error",
904
911
  source,
905
- message: error.message,
906
- line: error.line,
907
- column: error.column
912
+ message: error2.message,
913
+ line: error2.line,
914
+ column: error2.column
908
915
  };
909
916
  }
910
- if (error instanceof Error) {
917
+ if (error2 instanceof Error) {
911
918
  return {
912
919
  code,
913
920
  severity: "error",
914
921
  source,
915
- message: error.message
922
+ message: error2.message
916
923
  };
917
924
  }
918
925
  return {
919
926
  code,
920
927
  severity: "error",
921
928
  source,
922
- message: String(error)
929
+ message: String(error2)
923
930
  };
924
931
  }
925
932
 
926
933
  // packages/core/dist/scene.js
927
934
  var AU_IN_KM = 1495978707e-1;
928
935
  var EARTH_RADIUS_IN_KM = 6371;
936
+ var JUPITER_RADIUS_IN_KM = 71492;
929
937
  var SOLAR_RADIUS_IN_KM = 695700;
938
+ var LY_IN_AU = 63241.077;
939
+ var PC_IN_AU = 206264.806;
940
+ var KPC_IN_AU = 206264806;
930
941
  var ISO_FLATTENING = 0.68;
931
942
  var MIN_ISO_MINOR_SCALE = 0.2;
932
943
  var ARC_SAMPLE_COUNT = 28;
@@ -1046,8 +1057,10 @@ var WorldOrbit = (() => {
1046
1057
  const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
1047
1058
  const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
1048
1059
  const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
1049
- const layers = createSceneLayers(orbitVisuals, leaders, objects, labels);
1060
+ const relations = createSceneRelations(document2, objects);
1061
+ const layers = createSceneLayers(orbitVisuals, relations, leaders, objects, labels);
1050
1062
  const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
1063
+ const semanticGroups = createSceneSemanticGroups(document2, objects);
1051
1064
  const viewpoints = createSceneViewpoints(document2, projection, frame.preset, relationships, objectMap);
1052
1065
  const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
1053
1066
  return {
@@ -1057,7 +1070,7 @@ var WorldOrbit = (() => {
1057
1070
  renderPreset: frame.preset,
1058
1071
  projection,
1059
1072
  scaleModel,
1060
- title: String(document2.system?.properties.title ?? document2.system?.id ?? "WorldOrbit") || "WorldOrbit",
1073
+ title: String(document2.system?.title ?? document2.system?.properties.title ?? document2.system?.id ?? "WorldOrbit") || "WorldOrbit",
1061
1074
  subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
1062
1075
  systemId,
1063
1076
  viewMode: projection,
@@ -1073,9 +1086,11 @@ var WorldOrbit = (() => {
1073
1086
  contentBounds,
1074
1087
  layers,
1075
1088
  groups,
1089
+ semanticGroups,
1076
1090
  viewpoints,
1077
1091
  objects,
1078
1092
  orbitVisuals,
1093
+ relations,
1079
1094
  leaders,
1080
1095
  labels
1081
1096
  };
@@ -1174,6 +1189,7 @@ var WorldOrbit = (() => {
1174
1189
  }
1175
1190
  function createSceneObject(position, scaleModel, relationships) {
1176
1191
  const { object, x, y, radius, sortKey, anchorX, anchorY } = position;
1192
+ const renderPriority = object.renderHints?.renderPriority ?? 0;
1177
1193
  return {
1178
1194
  renderId: createRenderId(object.id),
1179
1195
  objectId: object.id,
@@ -1182,11 +1198,12 @@ var WorldOrbit = (() => {
1182
1198
  ancestorIds: relationships.ancestorIds.get(object.id) ?? [],
1183
1199
  childIds: relationships.childIds.get(object.id) ?? [],
1184
1200
  groupId: relationships.groupIds.get(object.id) ?? null,
1201
+ semanticGroupIds: [...object.groups ?? []],
1185
1202
  x,
1186
1203
  y,
1187
1204
  radius,
1188
1205
  visualRadius: visualExtentForObject(object, radius, scaleModel),
1189
- sortKey,
1206
+ sortKey: sortKey + renderPriority * 1e-3,
1190
1207
  anchorX,
1191
1208
  anchorY,
1192
1209
  label: object.id,
@@ -1203,6 +1220,7 @@ var WorldOrbit = (() => {
1203
1220
  object: draft.object,
1204
1221
  parentId: draft.parentId,
1205
1222
  groupId,
1223
+ semanticGroupIds: [...draft.object.groups ?? []],
1206
1224
  kind: draft.kind,
1207
1225
  cx: draft.cx,
1208
1226
  cy: draft.cy,
@@ -1214,7 +1232,7 @@ var WorldOrbit = (() => {
1214
1232
  bandThickness: draft.bandThickness,
1215
1233
  frontArcPath: draft.frontArcPath,
1216
1234
  backArcPath: draft.backArcPath,
1217
- hidden: draft.object.properties.hidden === true
1235
+ hidden: draft.object.properties.hidden === true || draft.object.renderHints?.renderOrbit === false
1218
1236
  };
1219
1237
  }
1220
1238
  function createLeaderLine(draft) {
@@ -1223,6 +1241,7 @@ var WorldOrbit = (() => {
1223
1241
  objectId: draft.object.id,
1224
1242
  object: draft.object,
1225
1243
  groupId: draft.groupId,
1244
+ semanticGroupIds: [...draft.object.groups ?? []],
1226
1245
  x1: draft.x1,
1227
1246
  y1: draft.y1,
1228
1247
  x2: draft.x2,
@@ -1234,7 +1253,7 @@ var WorldOrbit = (() => {
1234
1253
  function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1235
1254
  const labels = [];
1236
1255
  const occupied = [];
1237
- const visibleObjects = [...objects].filter((object) => !object.hidden).sort((left, right) => left.sortKey - right.sortKey);
1256
+ const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort((left, right) => left.sortKey - right.sortKey);
1238
1257
  for (const object of visibleObjects) {
1239
1258
  const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1240
1259
  const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
@@ -1254,6 +1273,7 @@ var WorldOrbit = (() => {
1254
1273
  objectId: object.objectId,
1255
1274
  object: object.object,
1256
1275
  groupId: object.groupId,
1276
+ semanticGroupIds: [...object.semanticGroupIds],
1257
1277
  label: object.label,
1258
1278
  secondaryLabel: object.secondaryLabel,
1259
1279
  x: object.x,
@@ -1266,7 +1286,7 @@ var WorldOrbit = (() => {
1266
1286
  }
1267
1287
  return labels;
1268
1288
  }
1269
- function createSceneLayers(orbitVisuals, leaders, objects, labels) {
1289
+ function createSceneLayers(orbitVisuals, relations, leaders, objects, labels) {
1270
1290
  const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1271
1291
  const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1272
1292
  return [
@@ -1277,6 +1297,10 @@ var WorldOrbit = (() => {
1277
1297
  },
1278
1298
  { id: "orbits-back", renderIds: backOrbitIds },
1279
1299
  { id: "orbits-front", renderIds: frontOrbitIds },
1300
+ {
1301
+ id: "relations",
1302
+ renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId)
1303
+ },
1280
1304
  {
1281
1305
  id: "objects",
1282
1306
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
@@ -1341,6 +1365,36 @@ var WorldOrbit = (() => {
1341
1365
  }
1342
1366
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1343
1367
  }
1368
+ function createSceneSemanticGroups(document2, objects) {
1369
+ return [...document2.groups].map((group) => ({
1370
+ id: group.id,
1371
+ label: group.label,
1372
+ summary: group.summary,
1373
+ color: group.color,
1374
+ tags: [...group.tags],
1375
+ hidden: group.hidden,
1376
+ objectIds: objects.filter((object) => !object.hidden && object.semanticGroupIds.includes(group.id)).map((object) => object.objectId)
1377
+ })).sort((left, right) => left.label.localeCompare(right.label));
1378
+ }
1379
+ function createSceneRelations(document2, objects) {
1380
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1381
+ return document2.relations.map((relation) => {
1382
+ const from = objectMap.get(relation.from);
1383
+ const to = objectMap.get(relation.to);
1384
+ return {
1385
+ renderId: `${createRenderId(relation.id)}-relation`,
1386
+ relationId: relation.id,
1387
+ relation,
1388
+ fromObjectId: relation.from,
1389
+ toObjectId: relation.to,
1390
+ x1: from?.x ?? 0,
1391
+ y1: from?.y ?? 0,
1392
+ x2: to?.x ?? 0,
1393
+ y2: to?.y ?? 0,
1394
+ hidden: relation.hidden || !from || !to || from.hidden || to.hidden
1395
+ };
1396
+ }).sort((left, right) => left.relation.id.localeCompare(right.relation.id));
1397
+ }
1344
1398
  function createSceneViewpoints(document2, projection, preset, relationships, objectMap) {
1345
1399
  const generatedOverview = createGeneratedOverviewViewpoint(document2, projection, preset);
1346
1400
  const drafts = /* @__PURE__ */ new Map();
@@ -1358,7 +1412,7 @@ var WorldOrbit = (() => {
1358
1412
  }
1359
1413
  const field = fieldParts.join(".").toLowerCase();
1360
1414
  const draft = drafts.get(id) ?? { id };
1361
- applyViewpointField(draft, field, value, projection, preset, relationships, objectMap);
1415
+ applyViewpointField(draft, field, value, document2, projection, preset, relationships, objectMap);
1362
1416
  drafts.set(id, draft);
1363
1417
  }
1364
1418
  const viewpoints = [...drafts.values()].map((draft) => finalizeViewpointDraft(draft, projection, preset, objectMap)).filter(Boolean);
@@ -1386,7 +1440,8 @@ var WorldOrbit = (() => {
1386
1440
  });
1387
1441
  }
1388
1442
  function createGeneratedOverviewViewpoint(document2, projection, preset) {
1389
- const label = document2.system?.properties.title ? `${String(document2.system.properties.title)} Overview` : "Overview";
1443
+ const title = document2.system?.title ?? document2.system?.properties.title;
1444
+ const label = title ? `${String(title)} Overview` : "Overview";
1390
1445
  return {
1391
1446
  id: "overview",
1392
1447
  label,
@@ -1402,7 +1457,7 @@ var WorldOrbit = (() => {
1402
1457
  generated: true
1403
1458
  };
1404
1459
  }
1405
- function applyViewpointField(draft, field, value, projection, preset, relationships, objectMap) {
1460
+ function applyViewpointField(draft, field, value, document2, projection, preset, relationships, objectMap) {
1406
1461
  const normalizedValue = value.trim();
1407
1462
  switch (field) {
1408
1463
  case "label":
@@ -1469,7 +1524,7 @@ var WorldOrbit = (() => {
1469
1524
  case "groups":
1470
1525
  draft.filter = {
1471
1526
  ...draft.filter ?? createEmptyViewpointFilter(),
1472
- groupIds: parseViewpointGroups(normalizedValue, relationships, objectMap)
1527
+ groupIds: parseViewpointGroups(normalizedValue, document2, relationships, objectMap)
1473
1528
  };
1474
1529
  return;
1475
1530
  }
@@ -1542,7 +1597,7 @@ var WorldOrbit = (() => {
1542
1597
  next["orbits-front"] = enabled;
1543
1598
  continue;
1544
1599
  }
1545
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1600
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1546
1601
  next[rawLayer] = enabled;
1547
1602
  }
1548
1603
  }
@@ -1551,8 +1606,11 @@ var WorldOrbit = (() => {
1551
1606
  function parseViewpointObjectTypes(value) {
1552
1607
  return splitListValue(value).filter((entry) => entry === "star" || entry === "planet" || entry === "moon" || entry === "belt" || entry === "asteroid" || entry === "comet" || entry === "ring" || entry === "structure" || entry === "phenomenon");
1553
1608
  }
1554
- function parseViewpointGroups(value, relationships, objectMap) {
1609
+ function parseViewpointGroups(value, document2, relationships, objectMap) {
1555
1610
  return splitListValue(value).map((entry) => {
1611
+ if (document2.schemaVersion === "2.1" || document2.groups.some((group) => group.id === entry)) {
1612
+ return entry;
1613
+ }
1556
1614
  if (entry.startsWith("wo-") && entry.endsWith("-group")) {
1557
1615
  return entry;
1558
1616
  }
@@ -1683,8 +1741,9 @@ var WorldOrbit = (() => {
1683
1741
  }
1684
1742
  const orbiting = [...context.orbitChildren.get(object.id) ?? []].sort(compareOrbiting);
1685
1743
  const orbitMetricContext = computeOrbitMetricContext(orbiting, parent.radius, context.spacingFactor, context.scaleModel);
1744
+ const orbitRadiiPx = resolveOrbitRadiiPx(orbiting, orbitMetricContext);
1686
1745
  orbiting.forEach((child, index) => {
1687
- const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, context);
1746
+ const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, orbitRadiiPx[index] ?? orbitMetricContext.innerPx, context);
1688
1747
  orbitDrafts.push({
1689
1748
  object: child,
1690
1749
  parentId: object.id,
@@ -1758,7 +1817,8 @@ var WorldOrbit = (() => {
1758
1817
  metricSpread: 0,
1759
1818
  innerPx,
1760
1819
  stepPx,
1761
- pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx)
1820
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
1821
+ minimumGapPx: stepPx * 0.42
1762
1822
  };
1763
1823
  }
1764
1824
  const minMetric = Math.min(...presentMetrics);
@@ -1771,10 +1831,11 @@ var WorldOrbit = (() => {
1771
1831
  metricSpread,
1772
1832
  innerPx,
1773
1833
  stepPx,
1774
- pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx)
1834
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
1835
+ minimumGapPx: stepPx * 0.42
1775
1836
  };
1776
1837
  }
1777
- function resolveOrbitGeometry(object, index, count, parent, metricContext, context) {
1838
+ function resolveOrbitGeometry(object, index, count, parent, metricContext, orbitRadiusPx, context) {
1778
1839
  const placement = object.placement;
1779
1840
  const band = object.type === "belt" || object.type === "ring";
1780
1841
  if (!placement || placement.mode !== "orbit") {
@@ -1792,7 +1853,7 @@ var WorldOrbit = (() => {
1792
1853
  };
1793
1854
  }
1794
1855
  const eccentricity = clampNumber(typeof placement.eccentricity === "number" ? placement.eccentricity : 0, 0, 0.92);
1795
- const semiMajor = resolveOrbitRadiusPx(object, index, metricContext);
1856
+ const semiMajor = orbitRadiusPx;
1796
1857
  const baseMinor = Math.max(semiMajor * Math.sqrt(1 - eccentricity * eccentricity), semiMajor * 0.18);
1797
1858
  const inclinationDeg = unitValueToDegrees(placement.inclination) ?? 0;
1798
1859
  const inclinationScale = context.projection === "isometric" ? Math.max(MIN_ISO_MINOR_SCALE, Math.cos(degreesToRadians(inclinationDeg))) * ISO_FLATTENING : 1;
@@ -1822,15 +1883,19 @@ var WorldOrbit = (() => {
1822
1883
  objectY: objectPoint.y
1823
1884
  };
1824
1885
  }
1825
- function resolveOrbitRadiusPx(object, index, metricContext) {
1826
- const metric = orbitMetric(object);
1827
- if (metric === null) {
1828
- return metricContext.innerPx + index * metricContext.stepPx;
1829
- }
1830
- if (metricContext.metricSpread > 0) {
1831
- return metricContext.innerPx + (metric - metricContext.minMetric) / metricContext.metricSpread * metricContext.pixelSpread;
1832
- }
1833
- return metricContext.innerPx + Math.log10(metric + 1) * metricContext.stepPx;
1886
+ function resolveOrbitRadiusPx(metric, metricContext) {
1887
+ return metricContext.innerPx + metricContext.stepPx * log2(Math.max(metric, 0) + 1);
1888
+ }
1889
+ function resolveOrbitRadiiPx(objects, metricContext) {
1890
+ const radii = [];
1891
+ objects.forEach((object, index) => {
1892
+ const metric = orbitMetric(object);
1893
+ const fallbackRadius = metricContext.innerPx + index * metricContext.stepPx;
1894
+ const baseRadius = metric === null ? fallbackRadius : resolveOrbitRadiusPx(metric, metricContext);
1895
+ const minimumRadius = index === 0 ? metricContext.innerPx : (radii[index - 1] ?? metricContext.innerPx) + metricContext.minimumGapPx;
1896
+ radii.push(Math.max(baseRadius, minimumRadius));
1897
+ });
1898
+ return radii;
1834
1899
  }
1835
1900
  function orbitMetric(object) {
1836
1901
  if (!object.placement || object.placement.mode !== "orbit") {
@@ -1838,6 +1903,9 @@ var WorldOrbit = (() => {
1838
1903
  }
1839
1904
  return toDistanceMetric(object.placement.semiMajor ?? object.placement.distance ?? null);
1840
1905
  }
1906
+ function log2(value) {
1907
+ return Math.log(value) / Math.log(2);
1908
+ }
1841
1909
  function resolveOrbitPhase(phase, index, count) {
1842
1910
  const degreeValue = phase ? unitValueToDegrees(phase) : null;
1843
1911
  if (degreeValue !== null) {
@@ -2161,8 +2229,18 @@ var WorldOrbit = (() => {
2161
2229
  return value.value;
2162
2230
  case "km":
2163
2231
  return value.value / AU_IN_KM;
2232
+ case "m":
2233
+ return value.value / 1e3 / AU_IN_KM;
2234
+ case "ly":
2235
+ return value.value * LY_IN_AU;
2236
+ case "pc":
2237
+ return value.value * PC_IN_AU;
2238
+ case "kpc":
2239
+ return value.value * KPC_IN_AU;
2164
2240
  case "re":
2165
2241
  return value.value * EARTH_RADIUS_IN_KM / AU_IN_KM;
2242
+ case "rj":
2243
+ return value.value * JUPITER_RADIUS_IN_KM / AU_IN_KM;
2166
2244
  case "sol":
2167
2245
  return value.value * SOLAR_RADIUS_IN_KM / AU_IN_KM;
2168
2246
  default:
@@ -2302,19 +2380,37 @@ var WorldOrbit = (() => {
2302
2380
  const system = document2.system ? {
2303
2381
  type: "system",
2304
2382
  id: document2.system.id,
2383
+ title: document2.system.title,
2384
+ description: document2.system.description,
2385
+ epoch: document2.system.epoch,
2386
+ referencePlane: document2.system.referencePlane,
2305
2387
  properties: materializeDraftSystemProperties(document2.system),
2306
2388
  info: materializeDraftSystemInfo(document2.system)
2307
2389
  } : null;
2308
2390
  return {
2309
2391
  format: "worldorbit",
2310
2392
  version: "1.0",
2393
+ schemaVersion: document2.version,
2311
2394
  system,
2395
+ groups: structuredClone(document2.groups ?? []),
2396
+ relations: structuredClone(document2.relations ?? []),
2312
2397
  objects: document2.objects.map(cloneWorldOrbitObject)
2313
2398
  };
2314
2399
  }
2315
2400
  function cloneWorldOrbitObject(object) {
2316
2401
  return {
2317
2402
  ...object,
2403
+ groups: object.groups ? [...object.groups] : void 0,
2404
+ resonance: object.resonance ? { ...object.resonance } : object.resonance,
2405
+ renderHints: object.renderHints ? { ...object.renderHints } : object.renderHints,
2406
+ deriveRules: object.deriveRules ? object.deriveRules.map((rule) => ({ ...rule })) : void 0,
2407
+ validationRules: object.validationRules ? object.validationRules.map((rule) => ({ ...rule })) : void 0,
2408
+ lockedFields: object.lockedFields ? [...object.lockedFields] : void 0,
2409
+ tolerances: object.tolerances ? object.tolerances.map((entry) => ({
2410
+ field: entry.field,
2411
+ 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
2412
+ })) : void 0,
2413
+ typedBlocks: object.typedBlocks ? Object.fromEntries(Object.entries(object.typedBlocks).map(([key, block]) => [key, { ...block ?? {} }])) : void 0,
2318
2414
  properties: cloneProperties(object.properties),
2319
2415
  placement: object.placement ? structuredClone(object.placement) : null,
2320
2416
  info: { ...object.info }
@@ -2350,71 +2446,80 @@ var WorldOrbit = (() => {
2350
2446
  if (system.defaults.units) {
2351
2447
  properties.units = system.defaults.units;
2352
2448
  }
2449
+ if (system.description) {
2450
+ properties.description = system.description;
2451
+ }
2452
+ if (system.epoch) {
2453
+ properties.epoch = system.epoch;
2454
+ }
2455
+ if (system.referencePlane) {
2456
+ properties.referencePlane = system.referencePlane;
2457
+ }
2353
2458
  return properties;
2354
2459
  }
2355
2460
  function materializeDraftSystemInfo(system) {
2356
- const info = {
2461
+ const info2 = {
2357
2462
  ...system.atlasMetadata
2358
2463
  };
2359
2464
  if (system.defaults.theme) {
2360
- info["atlas.theme"] = system.defaults.theme;
2465
+ info2["atlas.theme"] = system.defaults.theme;
2361
2466
  }
2362
2467
  for (const viewpoint of system.viewpoints) {
2363
2468
  const prefix = `viewpoint.${viewpoint.id}`;
2364
- info[`${prefix}.label`] = viewpoint.label;
2469
+ info2[`${prefix}.label`] = viewpoint.label;
2365
2470
  if (viewpoint.summary) {
2366
- info[`${prefix}.summary`] = viewpoint.summary;
2471
+ info2[`${prefix}.summary`] = viewpoint.summary;
2367
2472
  }
2368
2473
  if (viewpoint.focusObjectId) {
2369
- info[`${prefix}.focus`] = viewpoint.focusObjectId;
2474
+ info2[`${prefix}.focus`] = viewpoint.focusObjectId;
2370
2475
  }
2371
2476
  if (viewpoint.selectedObjectId) {
2372
- info[`${prefix}.select`] = viewpoint.selectedObjectId;
2477
+ info2[`${prefix}.select`] = viewpoint.selectedObjectId;
2373
2478
  }
2374
2479
  if (viewpoint.projection) {
2375
- info[`${prefix}.projection`] = viewpoint.projection;
2480
+ info2[`${prefix}.projection`] = viewpoint.projection;
2376
2481
  }
2377
2482
  if (viewpoint.preset) {
2378
- info[`${prefix}.preset`] = viewpoint.preset;
2483
+ info2[`${prefix}.preset`] = viewpoint.preset;
2379
2484
  }
2380
2485
  if (viewpoint.zoom !== null) {
2381
- info[`${prefix}.zoom`] = String(viewpoint.zoom);
2486
+ info2[`${prefix}.zoom`] = String(viewpoint.zoom);
2382
2487
  }
2383
2488
  if (viewpoint.rotationDeg !== 0) {
2384
- info[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2489
+ info2[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2385
2490
  }
2386
2491
  const serializedLayers = serializeViewpointLayers(viewpoint.layers);
2387
2492
  if (serializedLayers) {
2388
- info[`${prefix}.layers`] = serializedLayers;
2493
+ info2[`${prefix}.layers`] = serializedLayers;
2389
2494
  }
2390
2495
  if (viewpoint.filter?.query) {
2391
- info[`${prefix}.query`] = viewpoint.filter.query;
2496
+ info2[`${prefix}.query`] = viewpoint.filter.query;
2392
2497
  }
2393
2498
  if ((viewpoint.filter?.objectTypes.length ?? 0) > 0) {
2394
- info[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
2499
+ info2[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
2395
2500
  }
2396
2501
  if ((viewpoint.filter?.tags.length ?? 0) > 0) {
2397
- info[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
2502
+ info2[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
2398
2503
  }
2399
2504
  if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2400
- info[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2505
+ info2[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2401
2506
  }
2402
2507
  }
2403
2508
  for (const annotation of system.annotations) {
2404
2509
  const prefix = `annotation.${annotation.id}`;
2405
- info[`${prefix}.label`] = annotation.label;
2510
+ info2[`${prefix}.label`] = annotation.label;
2406
2511
  if (annotation.targetObjectId) {
2407
- info[`${prefix}.target`] = annotation.targetObjectId;
2512
+ info2[`${prefix}.target`] = annotation.targetObjectId;
2408
2513
  }
2409
- info[`${prefix}.body`] = annotation.body;
2514
+ info2[`${prefix}.body`] = annotation.body;
2410
2515
  if (annotation.tags.length > 0) {
2411
- info[`${prefix}.tags`] = annotation.tags.join(" ");
2516
+ info2[`${prefix}.tags`] = annotation.tags.join(" ");
2412
2517
  }
2413
2518
  if (annotation.sourceObjectId) {
2414
- info[`${prefix}.source`] = annotation.sourceObjectId;
2519
+ info2[`${prefix}.source`] = annotation.sourceObjectId;
2415
2520
  }
2416
2521
  }
2417
- return info;
2522
+ return info2;
2418
2523
  }
2419
2524
  function serializeViewpointLayers(layers) {
2420
2525
  const tokens = [];
@@ -2423,7 +2528,7 @@ var WorldOrbit = (() => {
2423
2528
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2424
2529
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2425
2530
  }
2426
- for (const key of ["background", "guides", "objects", "labels", "metadata"]) {
2531
+ for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
2427
2532
  if (layers[key] !== void 0) {
2428
2533
  tokens.push(layers[key] ? key : `-${key}`);
2429
2534
  }
@@ -2431,21 +2536,530 @@ var WorldOrbit = (() => {
2431
2536
  return tokens.join(" ");
2432
2537
  }
2433
2538
 
2539
+ // packages/core/dist/atlas-utils.js
2540
+ 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)?$/;
2541
+ var BOOLEAN_VALUES2 = /* @__PURE__ */ new Map([
2542
+ ["true", true],
2543
+ ["false", false],
2544
+ ["yes", true],
2545
+ ["no", false]
2546
+ ]);
2547
+ var URL_SCHEME_PATTERN2 = /^[A-Za-z][A-Za-z0-9+.-]*:/;
2548
+ function normalizeIdentifier(value) {
2549
+ return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
2550
+ }
2551
+ function humanizeIdentifier2(value) {
2552
+ return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
2553
+ }
2554
+ function parseAtlasUnitValue(input, location, fieldKey) {
2555
+ const match = input.match(UNIT_PATTERN2);
2556
+ if (!match) {
2557
+ throw WorldOrbitError.fromLocation(`Invalid unit value "${input}"`, location);
2558
+ }
2559
+ const unitValue = {
2560
+ value: Number(match[1]),
2561
+ unit: match[2] ?? null
2562
+ };
2563
+ if (fieldKey) {
2564
+ const schema = getFieldSchema(fieldKey);
2565
+ if (schema?.unitFamily && !unitFamilyAllowsUnit(schema.unitFamily, unitValue.unit)) {
2566
+ throw WorldOrbitError.fromLocation(`Unit "${unitValue.unit ?? "none"}" is not valid for "${fieldKey}"`, location);
2567
+ }
2568
+ }
2569
+ return unitValue;
2570
+ }
2571
+ function tryParseAtlasUnitValue(input) {
2572
+ const match = input.match(UNIT_PATTERN2);
2573
+ if (!match) {
2574
+ return null;
2575
+ }
2576
+ return {
2577
+ value: Number(match[1]),
2578
+ unit: match[2] ?? null
2579
+ };
2580
+ }
2581
+ function parseAtlasNumber(input, key, location) {
2582
+ const value = Number(input);
2583
+ if (!Number.isFinite(value)) {
2584
+ throw WorldOrbitError.fromLocation(`Invalid numeric value "${input}" for "${key}"`, location);
2585
+ }
2586
+ return value;
2587
+ }
2588
+ function parseAtlasBoolean(input, key, location) {
2589
+ const parsed = BOOLEAN_VALUES2.get(input.toLowerCase());
2590
+ if (parsed === void 0) {
2591
+ throw WorldOrbitError.fromLocation(`Invalid boolean value "${input}" for "${key}"`, location);
2592
+ }
2593
+ return parsed;
2594
+ }
2595
+ function parseAtlasAtReference(target, location) {
2596
+ if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
2597
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
2598
+ }
2599
+ const pairedMatch = target.match(/^([A-Za-z0-9._-]+)-([A-Za-z0-9._-]+):(L[1-5])$/);
2600
+ if (pairedMatch) {
2601
+ return {
2602
+ kind: "lagrange",
2603
+ primary: pairedMatch[1],
2604
+ secondary: pairedMatch[2],
2605
+ point: pairedMatch[3]
2606
+ };
2607
+ }
2608
+ const simpleMatch = target.match(/^([A-Za-z0-9._-]+):(L[1-5])$/);
2609
+ if (simpleMatch) {
2610
+ return {
2611
+ kind: "lagrange",
2612
+ primary: simpleMatch[1],
2613
+ secondary: null,
2614
+ point: simpleMatch[2]
2615
+ };
2616
+ }
2617
+ if (/^[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
2618
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
2619
+ }
2620
+ const anchorMatch = target.match(/^([A-Za-z0-9._-]+):([A-Za-z0-9._-]+)$/);
2621
+ if (anchorMatch) {
2622
+ return {
2623
+ kind: "anchor",
2624
+ objectId: anchorMatch[1],
2625
+ anchor: anchorMatch[2]
2626
+ };
2627
+ }
2628
+ return {
2629
+ kind: "named",
2630
+ name: target
2631
+ };
2632
+ }
2633
+ function validateAtlasImageSource(value, location) {
2634
+ if (!value) {
2635
+ throw WorldOrbitError.fromLocation('Field "image" must not be empty', location);
2636
+ }
2637
+ if (value.startsWith("//")) {
2638
+ throw WorldOrbitError.fromLocation('Field "image" must use a relative path, root-relative path, or an http/https URL', location);
2639
+ }
2640
+ const schemeMatch = value.match(URL_SCHEME_PATTERN2);
2641
+ if (!schemeMatch) {
2642
+ return;
2643
+ }
2644
+ const scheme = schemeMatch[0].slice(0, -1).toLowerCase();
2645
+ if (scheme !== "http" && scheme !== "https") {
2646
+ throw WorldOrbitError.fromLocation(`Field "image" does not support the "${scheme}" scheme`, location);
2647
+ }
2648
+ }
2649
+ function normalizeLegacyScalarValue(key, values, location) {
2650
+ const schema = getFieldSchema(key);
2651
+ if (!schema) {
2652
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
2653
+ }
2654
+ if (schema.arity === "single" && values.length !== 1) {
2655
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
2656
+ }
2657
+ switch (schema.kind) {
2658
+ case "list":
2659
+ return values;
2660
+ case "boolean":
2661
+ return parseAtlasBoolean(singleAtlasValue(values, key, location), key, location);
2662
+ case "number":
2663
+ return parseAtlasNumber(singleAtlasValue(values, key, location), key, location);
2664
+ case "unit":
2665
+ return parseAtlasUnitValue(singleAtlasValue(values, key, location), location, key);
2666
+ case "string": {
2667
+ const value = values.join(" ").trim();
2668
+ if (key === "image") {
2669
+ validateAtlasImageSource(value, location);
2670
+ }
2671
+ return value;
2672
+ }
2673
+ }
2674
+ }
2675
+ function ensureAtlasFieldSupported(key, objectType, location) {
2676
+ const schema = getFieldSchema(key);
2677
+ if (!schema) {
2678
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
2679
+ }
2680
+ if (!schema.objectTypes.includes(objectType)) {
2681
+ throw WorldOrbitError.fromLocation(`Field "${key}" is not valid on "${objectType}"`, location);
2682
+ }
2683
+ }
2684
+ function singleAtlasValue(values, key, location) {
2685
+ if (values.length !== 1) {
2686
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
2687
+ }
2688
+ return values[0];
2689
+ }
2690
+
2691
+ // packages/core/dist/atlas-validate.js
2692
+ var SURFACE_TARGET_TYPES2 = /* @__PURE__ */ new Set(["star", "planet", "moon", "asteroid", "comet"]);
2693
+ var EARTH_MASSES_PER_SOLAR = 332946.0487;
2694
+ var JUPITER_MASSES_PER_SOLAR = 1047.3486;
2695
+ var AU_IN_KM2 = 1495978707e-1;
2696
+ var EARTH_RADIUS_IN_KM2 = 6371;
2697
+ var SOLAR_RADIUS_IN_KM2 = 695700;
2698
+ var LY_IN_AU2 = 63241.077;
2699
+ var PC_IN_AU2 = 206264.806;
2700
+ var KPC_IN_AU2 = 206264806;
2701
+ function collectAtlasDiagnostics(document2, sourceSchemaVersion) {
2702
+ const diagnostics = [];
2703
+ const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
2704
+ const groupIds = new Set(document2.groups.map((group) => group.id));
2705
+ if (!document2.system) {
2706
+ diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
2707
+ }
2708
+ const knownIds = /* @__PURE__ */ new Map();
2709
+ for (const [kind, ids] of [
2710
+ ["group", document2.groups.map((group) => group.id)],
2711
+ ["viewpoint", document2.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
2712
+ ["annotation", document2.system?.annotations.map((annotation) => annotation.id) ?? []],
2713
+ ["relation", document2.relations.map((relation) => relation.id)],
2714
+ ["object", document2.objects.map((object) => object.id)]
2715
+ ]) {
2716
+ for (const id of ids) {
2717
+ const previous = knownIds.get(id);
2718
+ if (previous) {
2719
+ diagnostics.push(error("validate.id.duplicate", `Duplicate ${kind} id "${id}" already used by ${previous}.`));
2720
+ } else {
2721
+ knownIds.set(id, kind);
2722
+ }
2723
+ }
2724
+ }
2725
+ for (const relation of document2.relations) {
2726
+ validateRelation(relation, objectMap, diagnostics);
2727
+ }
2728
+ for (const viewpoint of document2.system?.viewpoints ?? []) {
2729
+ validateViewpointFilter(viewpoint.filter, groupIds, sourceSchemaVersion, diagnostics, viewpoint.id);
2730
+ }
2731
+ for (const object of document2.objects) {
2732
+ validateObject(object, document2.system, objectMap, groupIds, diagnostics);
2733
+ }
2734
+ return diagnostics;
2735
+ }
2736
+ function validateRelation(relation, objectMap, diagnostics) {
2737
+ if (!relation.from) {
2738
+ diagnostics.push(error("validate.relation.from.required", `Relation "${relation.id}" is missing a "from" target.`));
2739
+ } else if (!objectMap.has(relation.from)) {
2740
+ diagnostics.push(error("validate.relation.from.unknown", `Unknown relation source "${relation.from}" on "${relation.id}".`));
2741
+ }
2742
+ if (!relation.to) {
2743
+ diagnostics.push(error("validate.relation.to.required", `Relation "${relation.id}" is missing a "to" target.`));
2744
+ } else if (!objectMap.has(relation.to)) {
2745
+ diagnostics.push(error("validate.relation.to.unknown", `Unknown relation target "${relation.to}" on "${relation.id}".`));
2746
+ }
2747
+ if (!relation.kind) {
2748
+ diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
2749
+ }
2750
+ }
2751
+ function validateViewpointFilter(filter, groupIds, sourceSchemaVersion, diagnostics, viewpointId) {
2752
+ if (!filter || sourceSchemaVersion !== "2.1") {
2753
+ return;
2754
+ }
2755
+ for (const groupId of filter.groupIds) {
2756
+ if (!groupIds.has(groupId)) {
2757
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`));
2758
+ }
2759
+ }
2760
+ }
2761
+ function validateObject(object, system, objectMap, groupIds, diagnostics) {
2762
+ const placement = object.placement;
2763
+ const orbitPlacement = placement?.mode === "orbit" ? placement : null;
2764
+ const parentObject = placement?.mode === "orbit" ? objectMap.get(placement.target) ?? null : null;
2765
+ if (object.groups) {
2766
+ for (const groupId of object.groups) {
2767
+ if (!groupIds.has(groupId)) {
2768
+ diagnostics.push(warn("validate.group.unknown", `Unknown group "${groupId}" on "${object.id}".`, object.id, "groups"));
2769
+ }
2770
+ }
2771
+ }
2772
+ if (orbitPlacement) {
2773
+ if (!objectMap.has(orbitPlacement.target)) {
2774
+ diagnostics.push(error("validate.orbit.target.unknown", `Unknown placement target "${orbitPlacement.target}" on "${object.id}".`, object.id, "orbit"));
2775
+ }
2776
+ if (orbitPlacement.distance && orbitPlacement.semiMajor) {
2777
+ diagnostics.push(error("validate.orbit.distanceConflict", `Object "${object.id}" cannot declare both "distance" and "semiMajor".`, object.id, "distance"));
2778
+ }
2779
+ if (orbitPlacement.phase && !object.epoch && !system?.epoch) {
2780
+ diagnostics.push(warn("validate.phase.epochMissing", `Object "${object.id}" sets "phase" without an object or system epoch.`, object.id, "phase"));
2781
+ }
2782
+ if (orbitPlacement.inclination && !object.referencePlane && !system?.referencePlane) {
2783
+ diagnostics.push(warn("validate.inclination.referencePlaneMissing", `Object "${object.id}" sets "inclination" without an object or system reference plane.`, object.id, "inclination"));
2784
+ }
2785
+ if (orbitPlacement.period && !massInSolar(parentObject?.properties.mass)) {
2786
+ diagnostics.push(warn("validate.period.massMissing", `Object "${object.id}" sets "period" but its central mass cannot be derived.`, object.id, "period"));
2787
+ }
2788
+ }
2789
+ if (placement?.mode === "surface") {
2790
+ const target = objectMap.get(placement.target);
2791
+ if (!target) {
2792
+ diagnostics.push(error("validate.surface.target.unknown", `Unknown placement target "${placement.target}" on "${object.id}".`, object.id, "surface"));
2793
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
2794
+ diagnostics.push(error("validate.surface.target.invalid", `Surface target "${placement.target}" on "${object.id}" is not surface-capable.`, object.id, "surface"));
2795
+ }
2796
+ }
2797
+ if (placement?.mode === "at") {
2798
+ if (object.type !== "structure" && object.type !== "phenomenon") {
2799
+ diagnostics.push(error("validate.at.objectType", `Only structures and phenomena may use "at" placement; found "${object.type}" on "${object.id}".`, object.id, "at"));
2800
+ }
2801
+ if (!validateAtTarget(object, objectMap, diagnostics)) {
2802
+ diagnostics.push(error("validate.at.target.unknown", `Unknown at-reference target "${placement.target}" on "${object.id}".`, object.id, "at"));
2803
+ }
2804
+ }
2805
+ if (object.resonance) {
2806
+ const target = objectMap.get(object.resonance.targetObjectId);
2807
+ if (!target) {
2808
+ diagnostics.push(error("validate.resonance.target.unknown", `Unknown resonance target "${object.resonance.targetObjectId}" on "${object.id}".`, object.id, "resonance"));
2809
+ } else if (object.placement?.mode !== "orbit" || target.placement?.mode !== "orbit" || object.placement.target !== target.placement.target) {
2810
+ diagnostics.push(warn("validate.resonance.orbitMismatch", `Resonance target "${object.resonance.targetObjectId}" on "${object.id}" does not share a compatible orbital parent.`, object.id, "resonance"));
2811
+ }
2812
+ }
2813
+ for (const rule of object.deriveRules ?? []) {
2814
+ if (rule.field !== "period" || rule.strategy !== "kepler") {
2815
+ diagnostics.push(warn("validate.derive.unsupported", `Unsupported derive rule "${rule.field} ${rule.strategy}" on "${object.id}".`, object.id, "derive"));
2816
+ continue;
2817
+ }
2818
+ const derivedPeriodDays = keplerPeriodDays(object, parentObject);
2819
+ if (derivedPeriodDays === null) {
2820
+ diagnostics.push(warn("validate.derive.inputsMissing", `Object "${object.id}" requests "derive period kepler" but lacks enough input data.`, object.id, "derive"));
2821
+ continue;
2822
+ }
2823
+ if (!orbitPlacement?.period) {
2824
+ diagnostics.push(info("validate.derive.period.available", `Object "${object.id}" can derive a Kepler period of ${formatDays(derivedPeriodDays)}.`, object.id, "derive"));
2825
+ }
2826
+ }
2827
+ for (const rule of object.validationRules ?? []) {
2828
+ if (rule.rule !== "kepler") {
2829
+ diagnostics.push(warn("validate.rule.unsupported", `Unsupported validation rule "${rule.rule}" on "${object.id}".`, object.id, "validate"));
2830
+ continue;
2831
+ }
2832
+ const actualPeriodDays = durationInDays(orbitPlacement?.period);
2833
+ const derivedPeriodDays = keplerPeriodDays(object, parentObject);
2834
+ if (actualPeriodDays === null || derivedPeriodDays === null) {
2835
+ continue;
2836
+ }
2837
+ const toleranceDays = toleranceForField(object, "period");
2838
+ if (Math.abs(actualPeriodDays - derivedPeriodDays) > toleranceDays) {
2839
+ diagnostics.push(error("validate.kepler.mismatch", `Object "${object.id}" fails Kepler validation for "period".`, object.id, "validate"));
2840
+ }
2841
+ }
2842
+ }
2843
+ function validateAtTarget(object, objectMap, diagnostics) {
2844
+ const reference = object.placement?.mode === "at" ? object.placement.reference : null;
2845
+ if (!reference) {
2846
+ return true;
2847
+ }
2848
+ if (reference.kind === "named") {
2849
+ return objectMap.has(reference.name);
2850
+ }
2851
+ if (reference.kind === "anchor") {
2852
+ if (!objectMap.has(reference.objectId)) {
2853
+ diagnostics.push(error("validate.anchor.target.unknown", `Unknown anchor target "${reference.objectId}" on "${object.id}".`, object.id, "at"));
2854
+ return false;
2855
+ }
2856
+ return true;
2857
+ }
2858
+ if (!objectMap.has(reference.primary)) {
2859
+ diagnostics.push(error("validate.lagrange.primary.unknown", `Unknown Lagrange reference "${reference.primary}" on "${object.id}".`, object.id, "at"));
2860
+ return false;
2861
+ }
2862
+ if (reference.secondary && !objectMap.has(reference.secondary)) {
2863
+ diagnostics.push(error("validate.lagrange.secondary.unknown", `Unknown Lagrange reference "${reference.secondary}" on "${object.id}".`, object.id, "at"));
2864
+ return false;
2865
+ }
2866
+ return true;
2867
+ }
2868
+ function keplerPeriodDays(object, parentObject) {
2869
+ const placement = object.placement;
2870
+ if (!placement || placement.mode !== "orbit") {
2871
+ return null;
2872
+ }
2873
+ const semiMajorAu = distanceInAu(placement.semiMajor ?? placement.distance);
2874
+ const centralMassSolar = massInSolar(parentObject?.properties.mass);
2875
+ if (semiMajorAu === null || centralMassSolar === null || centralMassSolar <= 0) {
2876
+ return null;
2877
+ }
2878
+ const periodYears = Math.sqrt(semiMajorAu ** 3 / centralMassSolar);
2879
+ return periodYears * 365.25;
2880
+ }
2881
+ function distanceInAu(value) {
2882
+ if (!value)
2883
+ return null;
2884
+ switch (value.unit) {
2885
+ case null:
2886
+ case "au":
2887
+ return value.value;
2888
+ case "km":
2889
+ return value.value / AU_IN_KM2;
2890
+ case "m":
2891
+ return value.value / (AU_IN_KM2 * 1e3);
2892
+ case "ly":
2893
+ return value.value * LY_IN_AU2;
2894
+ case "pc":
2895
+ return value.value * PC_IN_AU2;
2896
+ case "kpc":
2897
+ return value.value * KPC_IN_AU2;
2898
+ case "re":
2899
+ return value.value * EARTH_RADIUS_IN_KM2 / AU_IN_KM2;
2900
+ case "sol":
2901
+ return value.value * SOLAR_RADIUS_IN_KM2 / AU_IN_KM2;
2902
+ default:
2903
+ return null;
2904
+ }
2905
+ }
2906
+ function massInSolar(value) {
2907
+ if (!value || typeof value !== "object" || !("value" in value)) {
2908
+ return null;
2909
+ }
2910
+ const unitValue = value;
2911
+ switch (unitValue.unit) {
2912
+ case null:
2913
+ case "sol":
2914
+ return unitValue.value;
2915
+ case "me":
2916
+ return unitValue.value / EARTH_MASSES_PER_SOLAR;
2917
+ case "mj":
2918
+ return unitValue.value / JUPITER_MASSES_PER_SOLAR;
2919
+ default:
2920
+ return null;
2921
+ }
2922
+ }
2923
+ function durationInDays(value) {
2924
+ if (!value)
2925
+ return null;
2926
+ switch (value.unit) {
2927
+ case null:
2928
+ case "d":
2929
+ return value.value;
2930
+ case "s":
2931
+ return value.value / 86400;
2932
+ case "min":
2933
+ return value.value / 1440;
2934
+ case "h":
2935
+ return value.value / 24;
2936
+ case "y":
2937
+ return value.value * 365.25;
2938
+ case "ky":
2939
+ return value.value * 365250;
2940
+ case "my":
2941
+ return value.value * 36525e4;
2942
+ case "gy":
2943
+ return value.value * 36525e7;
2944
+ default:
2945
+ return null;
2946
+ }
2947
+ }
2948
+ function toleranceForField(object, field) {
2949
+ const tolerance = object.tolerances?.find((entry) => entry.field === field)?.value;
2950
+ if (typeof tolerance === "number") {
2951
+ return tolerance;
2952
+ }
2953
+ if (tolerance && typeof tolerance === "object" && "value" in tolerance) {
2954
+ return durationInDays(tolerance) ?? 0;
2955
+ }
2956
+ return 0;
2957
+ }
2958
+ function formatDays(days) {
2959
+ return `${Math.round(days * 100) / 100}d`;
2960
+ }
2961
+ function error(code, message, objectId, field) {
2962
+ return { code, severity: "error", source: "validate", message, objectId, field };
2963
+ }
2964
+ function warn(code, message, objectId, field) {
2965
+ return { code, severity: "warning", source: "validate", message, objectId, field };
2966
+ }
2967
+ function info(code, message, objectId, field) {
2968
+ return { code, severity: "info", source: "validate", message, objectId, field };
2969
+ }
2970
+
2434
2971
  // packages/core/dist/draft-parse.js
2972
+ var STRUCTURED_TYPED_BLOCKS = /* @__PURE__ */ new Set([
2973
+ "climate",
2974
+ "habitability",
2975
+ "settlement"
2976
+ ]);
2977
+ var DRAFT_OBJECT_FIELD_SPECS = /* @__PURE__ */ new Map();
2978
+ for (const key of [
2979
+ "orbit",
2980
+ "distance",
2981
+ "semiMajor",
2982
+ "eccentricity",
2983
+ "period",
2984
+ "angle",
2985
+ "inclination",
2986
+ "phase",
2987
+ "at",
2988
+ "surface",
2989
+ "free",
2990
+ "kind",
2991
+ "class",
2992
+ "culture",
2993
+ "tags",
2994
+ "color",
2995
+ "image",
2996
+ "hidden",
2997
+ "radius",
2998
+ "mass",
2999
+ "density",
3000
+ "gravity",
3001
+ "temperature",
3002
+ "albedo",
3003
+ "atmosphere",
3004
+ "inner",
3005
+ "outer",
3006
+ "on",
3007
+ "source",
3008
+ "cycle"
3009
+ ]) {
3010
+ const schema = getFieldSchema(key);
3011
+ if (schema) {
3012
+ DRAFT_OBJECT_FIELD_SPECS.set(key, {
3013
+ key,
3014
+ version: "2.0",
3015
+ inlineMode: schema.arity === "multiple" ? "multiple" : "single",
3016
+ allowRepeat: false,
3017
+ legacySchema: schema
3018
+ });
3019
+ }
3020
+ }
3021
+ for (const spec of [
3022
+ { key: "groups", inlineMode: "multiple", allowRepeat: false },
3023
+ { key: "epoch", inlineMode: "single", allowRepeat: false },
3024
+ { key: "referencePlane", inlineMode: "single", allowRepeat: false },
3025
+ { key: "tidalLock", inlineMode: "single", allowRepeat: false },
3026
+ { key: "renderLabel", inlineMode: "single", allowRepeat: false },
3027
+ { key: "renderOrbit", inlineMode: "single", allowRepeat: false },
3028
+ { key: "renderPriority", inlineMode: "single", allowRepeat: false },
3029
+ { key: "resonance", inlineMode: "pair", allowRepeat: false },
3030
+ { key: "derive", inlineMode: "pair", allowRepeat: true },
3031
+ { key: "validate", inlineMode: "single", allowRepeat: true },
3032
+ { key: "locked", inlineMode: "multiple", allowRepeat: false },
3033
+ { key: "tolerance", inlineMode: "pair", allowRepeat: true }
3034
+ ]) {
3035
+ DRAFT_OBJECT_FIELD_SPECS.set(spec.key, {
3036
+ key: spec.key,
3037
+ version: "2.1",
3038
+ inlineMode: spec.inlineMode,
3039
+ allowRepeat: spec.allowRepeat
3040
+ });
3041
+ }
3042
+ var DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
2435
3043
  function parseWorldOrbitAtlas(source) {
2436
- return parseAtlasSource(source, "2.0");
3044
+ return parseAtlasSource(source);
2437
3045
  }
2438
- function parseAtlasSource(source, outputVersion) {
2439
- const lines = source.split(/\r?\n/);
3046
+ function parseAtlasSource(source, forcedOutputVersion) {
3047
+ const prepared = preprocessAtlasSource(source);
3048
+ const lines = prepared.source.split(/\r?\n/);
3049
+ const diagnostics = [];
2440
3050
  let sawSchemaHeader = false;
2441
- let schemaVersion = "2.0";
3051
+ let sourceSchemaVersion = "2.0";
2442
3052
  let system = null;
2443
3053
  let section = null;
2444
3054
  const objectNodes = [];
3055
+ const groups = [];
3056
+ const relations = [];
2445
3057
  let sawDefaults = false;
2446
3058
  let sawAtlas = false;
2447
3059
  const viewpointIds = /* @__PURE__ */ new Set();
2448
3060
  const annotationIds = /* @__PURE__ */ new Set();
3061
+ const groupIds = /* @__PURE__ */ new Set();
3062
+ const relationIds = /* @__PURE__ */ new Set();
2449
3063
  for (let index = 0; index < lines.length; index++) {
2450
3064
  const rawLine = lines[index];
2451
3065
  const lineNumber = index + 1;
@@ -2461,15 +3075,22 @@ var WorldOrbit = (() => {
2461
3075
  continue;
2462
3076
  }
2463
3077
  if (!sawSchemaHeader) {
2464
- schemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
3078
+ sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
2465
3079
  sawSchemaHeader = true;
3080
+ if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
3081
+ diagnostics.push({
3082
+ code: "parse.schema21.commentCompatibility",
3083
+ severity: "warning",
3084
+ source: "parse",
3085
+ message: `Comments require schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
3086
+ line: prepared.comments[0].line,
3087
+ column: prepared.comments[0].column
3088
+ });
3089
+ }
2466
3090
  continue;
2467
3091
  }
2468
3092
  if (indent === 0) {
2469
- section = startTopLevelSection(tokens, lineNumber, system, objectNodes, viewpointIds, annotationIds, {
2470
- sawDefaults,
2471
- sawAtlas
2472
- });
3093
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, { sawDefaults, sawAtlas });
2473
3094
  if (section.kind === "system") {
2474
3095
  system = section.system;
2475
3096
  } else if (section.kind === "defaults") {
@@ -2487,48 +3108,57 @@ var WorldOrbit = (() => {
2487
3108
  if (!sawSchemaHeader) {
2488
3109
  throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
2489
3110
  }
2490
- const ast = {
2491
- type: "document",
2492
- objects: objectNodes
2493
- };
2494
- const normalizedObjects = normalizeDocument(ast).objects;
2495
- validateDocument({
2496
- format: "worldorbit",
2497
- version: "1.0",
2498
- system: null,
2499
- objects: normalizedObjects
2500
- });
2501
- const diagnostics = schemaVersion === "2.0-draft" && outputVersion === "2.0" ? [
2502
- {
2503
- code: "load.schema.deprecatedDraft",
2504
- severity: "warning",
2505
- source: "upgrade",
2506
- message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".'
2507
- }
2508
- ] : [];
2509
- return {
3111
+ const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
3112
+ const outputVersion = forcedOutputVersion ?? (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
3113
+ const baseDocument = {
2510
3114
  format: "worldorbit",
2511
- version: outputVersion,
2512
3115
  sourceVersion: "1.0",
2513
3116
  system,
2514
- objects: normalizedObjects,
3117
+ groups,
3118
+ relations,
3119
+ objects,
2515
3120
  diagnostics
2516
3121
  };
2517
- }
3122
+ if (outputVersion === "2.0-draft") {
3123
+ const document3 = {
3124
+ ...baseDocument,
3125
+ version: "2.0-draft",
3126
+ schemaVersion: "2.0-draft"
3127
+ };
3128
+ document3.diagnostics.push(...collectAtlasDiagnostics(document3, sourceSchemaVersion));
3129
+ return document3;
3130
+ }
3131
+ const document2 = {
3132
+ ...baseDocument,
3133
+ version: outputVersion,
3134
+ schemaVersion: outputVersion
3135
+ };
3136
+ if (sourceSchemaVersion === "2.0-draft") {
3137
+ document2.diagnostics.push({
3138
+ code: "load.schema.deprecatedDraft",
3139
+ severity: "warning",
3140
+ source: "upgrade",
3141
+ message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".'
3142
+ });
3143
+ }
3144
+ document2.diagnostics.push(...collectAtlasDiagnostics(document2, sourceSchemaVersion));
3145
+ return document2;
3146
+ }
2518
3147
  function assertDraftSchemaHeader(tokens, line) {
2519
- if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || tokens[1].value.toLowerCase() !== "2.0-draft" && tokens[1].value.toLowerCase() !== "2.0") {
2520
- throw new WorldOrbitError('Expected atlas header "schema 2.0" or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
3148
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
3149
+ throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
2521
3150
  }
2522
- return tokens[1].value.toLowerCase() === "2.0-draft" ? "2.0-draft" : "2.0";
3151
+ const version = tokens[1].value.toLowerCase();
3152
+ return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
2523
3153
  }
2524
- function startTopLevelSection(tokens, line, system, objectNodes, viewpointIds, annotationIds, flags) {
3154
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, flags) {
2525
3155
  const keyword = tokens[0]?.value.toLowerCase();
2526
3156
  switch (keyword) {
2527
3157
  case "system":
2528
3158
  if (system) {
2529
3159
  throw new WorldOrbitError('Atlas section "system" may only appear once', line, tokens[0].column);
2530
3160
  }
2531
- return startSystemSection(tokens, line);
3161
+ return startSystemSection(tokens, line, sourceSchemaVersion, diagnostics);
2532
3162
  case "defaults":
2533
3163
  if (!system) {
2534
3164
  throw new WorldOrbitError('Atlas section "defaults" requires a preceding system declaration', line, tokens[0].column);
@@ -2564,13 +3194,19 @@ var WorldOrbit = (() => {
2564
3194
  throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
2565
3195
  }
2566
3196
  return startAnnotationSection(tokens, line, system, annotationIds);
3197
+ case "group":
3198
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "group", { line, column: tokens[0].column });
3199
+ return startGroupSection(tokens, line, groups, groupIds);
3200
+ case "relation":
3201
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
3202
+ return startRelationSection(tokens, line, relations, relationIds);
2567
3203
  case "object":
2568
- return startObjectSection(tokens, line, objectNodes);
3204
+ return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
2569
3205
  default:
2570
3206
  throw new WorldOrbitError(`Unknown atlas section "${tokens[0]?.value ?? ""}"`, line, tokens[0]?.column ?? 1);
2571
3207
  }
2572
3208
  }
2573
- function startSystemSection(tokens, line) {
3209
+ function startSystemSection(tokens, line, sourceSchemaVersion, diagnostics) {
2574
3210
  if (tokens.length !== 2) {
2575
3211
  throw new WorldOrbitError("Invalid atlas system declaration", line, tokens[0]?.column ?? 1);
2576
3212
  }
@@ -2578,6 +3214,9 @@ var WorldOrbit = (() => {
2578
3214
  type: "system",
2579
3215
  id: tokens[1].value,
2580
3216
  title: null,
3217
+ description: null,
3218
+ epoch: null,
3219
+ referencePlane: null,
2581
3220
  defaults: {
2582
3221
  view: "topdown",
2583
3222
  scale: null,
@@ -2592,6 +3231,8 @@ var WorldOrbit = (() => {
2592
3231
  return {
2593
3232
  kind: "system",
2594
3233
  system,
3234
+ sourceSchemaVersion,
3235
+ diagnostics,
2595
3236
  seenFields: /* @__PURE__ */ new Set()
2596
3237
  };
2597
3238
  }
@@ -2657,7 +3298,64 @@ var WorldOrbit = (() => {
2657
3298
  seenFields: /* @__PURE__ */ new Set()
2658
3299
  };
2659
3300
  }
2660
- function startObjectSection(tokens, line, objectNodes) {
3301
+ function startGroupSection(tokens, line, groups, groupIds) {
3302
+ if (tokens.length !== 2) {
3303
+ throw new WorldOrbitError("Invalid group declaration", line, tokens[0]?.column ?? 1);
3304
+ }
3305
+ const id = normalizeIdentifier(tokens[1].value);
3306
+ if (!id) {
3307
+ throw new WorldOrbitError("Group id must not be empty", line, tokens[1].column);
3308
+ }
3309
+ if (groupIds.has(id)) {
3310
+ throw new WorldOrbitError(`Duplicate group id "${id}"`, line, tokens[1].column);
3311
+ }
3312
+ const group = {
3313
+ id,
3314
+ label: humanizeIdentifier2(id),
3315
+ summary: "",
3316
+ color: null,
3317
+ tags: [],
3318
+ hidden: false
3319
+ };
3320
+ groups.push(group);
3321
+ groupIds.add(id);
3322
+ return {
3323
+ kind: "group",
3324
+ group,
3325
+ seenFields: /* @__PURE__ */ new Set()
3326
+ };
3327
+ }
3328
+ function startRelationSection(tokens, line, relations, relationIds) {
3329
+ if (tokens.length !== 2) {
3330
+ throw new WorldOrbitError("Invalid relation declaration", line, tokens[0]?.column ?? 1);
3331
+ }
3332
+ const id = normalizeIdentifier(tokens[1].value);
3333
+ if (!id) {
3334
+ throw new WorldOrbitError("Relation id must not be empty", line, tokens[1].column);
3335
+ }
3336
+ if (relationIds.has(id)) {
3337
+ throw new WorldOrbitError(`Duplicate relation id "${id}"`, line, tokens[1].column);
3338
+ }
3339
+ const relation = {
3340
+ id,
3341
+ from: "",
3342
+ to: "",
3343
+ kind: "",
3344
+ label: null,
3345
+ summary: null,
3346
+ tags: [],
3347
+ color: null,
3348
+ hidden: false
3349
+ };
3350
+ relations.push(relation);
3351
+ relationIds.add(id);
3352
+ return {
3353
+ kind: "relation",
3354
+ relation,
3355
+ seenFields: /* @__PURE__ */ new Set()
3356
+ };
3357
+ }
3358
+ function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
2661
3359
  if (tokens.length < 3) {
2662
3360
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
2663
3361
  }
@@ -2668,12 +3366,11 @@ var WorldOrbit = (() => {
2668
3366
  throw new WorldOrbitError(`Unknown object type "${objectTypeToken.value}"`, line, objectTypeToken.column);
2669
3367
  }
2670
3368
  const objectNode = {
2671
- type: "object",
2672
3369
  objectType,
2673
- name: idToken.value,
2674
- inlineFields: parseInlineFields2(tokens.slice(3), line),
2675
- blockFields: [],
3370
+ id: idToken.value,
3371
+ fields: parseInlineObjectFields(tokens.slice(3), line, objectType, sourceSchemaVersion, diagnostics),
2676
3372
  infoEntries: [],
3373
+ typedBlockEntries: {},
2677
3374
  location: {
2678
3375
  line,
2679
3376
  column: objectTypeToken.column
@@ -2683,8 +3380,12 @@ var WorldOrbit = (() => {
2683
3380
  return {
2684
3381
  kind: "object",
2685
3382
  objectNode,
2686
- inInfoBlock: false,
2687
- infoIndent: null
3383
+ sourceSchemaVersion,
3384
+ diagnostics,
3385
+ activeBlock: null,
3386
+ blockIndent: null,
3387
+ seenInfoKeys: /* @__PURE__ */ new Set(),
3388
+ seenTypedBlockKeys: {}
2688
3389
  };
2689
3390
  }
2690
3391
  function handleSectionLine(section, indent, tokens, line) {
@@ -2704,6 +3405,12 @@ var WorldOrbit = (() => {
2704
3405
  case "annotation":
2705
3406
  applyAnnotationField(section, tokens, line);
2706
3407
  return;
3408
+ case "group":
3409
+ applyGroupField(section, tokens, line);
3410
+ return;
3411
+ case "relation":
3412
+ applyRelationField(section, tokens, line);
3413
+ return;
2707
3414
  case "object":
2708
3415
  applyObjectField(section, indent, tokens, line);
2709
3416
  return;
@@ -2711,10 +3418,35 @@ var WorldOrbit = (() => {
2711
3418
  }
2712
3419
  function applySystemField(section, tokens, line) {
2713
3420
  const key = requireUniqueField(tokens, section.seenFields, line);
2714
- if (key !== "title") {
2715
- throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
3421
+ const value = joinFieldValue(tokens, line);
3422
+ switch (key) {
3423
+ case "title":
3424
+ section.system.title = value;
3425
+ return;
3426
+ case "description":
3427
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
3428
+ line,
3429
+ column: tokens[0].column
3430
+ });
3431
+ section.system.description = value;
3432
+ return;
3433
+ case "epoch":
3434
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
3435
+ line,
3436
+ column: tokens[0].column
3437
+ });
3438
+ section.system.epoch = value;
3439
+ return;
3440
+ case "referenceplane":
3441
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "referencePlane", {
3442
+ line,
3443
+ column: tokens[0].column
3444
+ });
3445
+ section.system.referencePlane = value;
3446
+ return;
3447
+ default:
3448
+ throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
2716
3449
  }
2717
- section.system.title = joinFieldValue(tokens, line);
2718
3450
  }
2719
3451
  function applyDefaultsField(section, tokens, line) {
2720
3452
  const key = requireUniqueField(tokens, section.seenFields, line);
@@ -2745,14 +3477,11 @@ var WorldOrbit = (() => {
2745
3477
  section.metadataIndent = null;
2746
3478
  }
2747
3479
  if (section.inMetadata) {
2748
- if (tokens.length < 2) {
2749
- throw new WorldOrbitError("Invalid atlas metadata entry", line, tokens[0]?.column ?? 1);
3480
+ const entry = parseInfoLikeEntry(tokens, line, "Invalid atlas metadata entry");
3481
+ if (entry.key in section.system.atlasMetadata) {
3482
+ throw new WorldOrbitError(`Duplicate atlas metadata key "${entry.key}"`, line, tokens[0].column);
2750
3483
  }
2751
- const key = tokens[0].value;
2752
- if (key in section.system.atlasMetadata) {
2753
- throw new WorldOrbitError(`Duplicate atlas metadata key "${key}"`, line, tokens[0].column);
2754
- }
2755
- section.system.atlasMetadata[key] = joinFieldValue(tokens, line);
3484
+ section.system.atlasMetadata[entry.key] = entry.value;
2756
3485
  return;
2757
3486
  }
2758
3487
  if (tokens.length === 1 && tokens[0].value.toLowerCase() === "metadata") {
@@ -2854,21 +3583,102 @@ var WorldOrbit = (() => {
2854
3583
  throw new WorldOrbitError(`Unknown annotation field "${tokens[0].value}"`, line, tokens[0].column);
2855
3584
  }
2856
3585
  }
2857
- function applyObjectField(section, indent, tokens, line) {
2858
- if (tokens.length === 1 && tokens[0].value === "info") {
2859
- section.inInfoBlock = true;
2860
- section.infoIndent = indent;
2861
- return;
3586
+ function applyGroupField(section, tokens, line) {
3587
+ const key = requireUniqueField(tokens, section.seenFields, line);
3588
+ switch (key) {
3589
+ case "label":
3590
+ section.group.label = joinFieldValue(tokens, line);
3591
+ return;
3592
+ case "summary":
3593
+ section.group.summary = joinFieldValue(tokens, line);
3594
+ return;
3595
+ case "color":
3596
+ section.group.color = joinFieldValue(tokens, line);
3597
+ return;
3598
+ case "tags":
3599
+ section.group.tags = parseTokenList(tokens.slice(1), line, "tags");
3600
+ return;
3601
+ case "hidden":
3602
+ section.group.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
3603
+ line,
3604
+ column: tokens[0].column
3605
+ });
3606
+ return;
3607
+ default:
3608
+ throw new WorldOrbitError(`Unknown group field "${tokens[0].value}"`, line, tokens[0].column);
3609
+ }
3610
+ }
3611
+ function applyRelationField(section, tokens, line) {
3612
+ const key = requireUniqueField(tokens, section.seenFields, line);
3613
+ switch (key) {
3614
+ case "from":
3615
+ section.relation.from = joinFieldValue(tokens, line);
3616
+ return;
3617
+ case "to":
3618
+ section.relation.to = joinFieldValue(tokens, line);
3619
+ return;
3620
+ case "kind":
3621
+ section.relation.kind = joinFieldValue(tokens, line);
3622
+ return;
3623
+ case "label":
3624
+ section.relation.label = joinFieldValue(tokens, line);
3625
+ return;
3626
+ case "summary":
3627
+ section.relation.summary = joinFieldValue(tokens, line);
3628
+ return;
3629
+ case "tags":
3630
+ section.relation.tags = parseTokenList(tokens.slice(1), line, "tags");
3631
+ return;
3632
+ case "color":
3633
+ section.relation.color = joinFieldValue(tokens, line);
3634
+ return;
3635
+ case "hidden":
3636
+ section.relation.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
3637
+ line,
3638
+ column: tokens[0].column
3639
+ });
3640
+ return;
3641
+ default:
3642
+ throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
2862
3643
  }
2863
- if (section.inInfoBlock && indent <= (section.infoIndent ?? 0)) {
2864
- section.inInfoBlock = false;
2865
- section.infoIndent = null;
3644
+ }
3645
+ function applyObjectField(section, indent, tokens, line) {
3646
+ if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
3647
+ section.activeBlock = null;
3648
+ section.blockIndent = null;
3649
+ }
3650
+ if (tokens.length === 1) {
3651
+ const blockName = tokens[0].value.toLowerCase();
3652
+ if (blockName === "info" || STRUCTURED_TYPED_BLOCKS.has(blockName)) {
3653
+ if (blockName !== "info") {
3654
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, blockName, { line, column: tokens[0].column });
3655
+ }
3656
+ section.activeBlock = blockName;
3657
+ section.blockIndent = indent;
3658
+ return;
3659
+ }
2866
3660
  }
2867
- if (section.inInfoBlock) {
2868
- section.objectNode.infoEntries.push(parseInfoEntry2(tokens, line));
3661
+ if (section.activeBlock) {
3662
+ const entry = parseInfoLikeEntry(tokens, line, `Invalid ${section.activeBlock} entry`);
3663
+ if (section.activeBlock === "info") {
3664
+ if (section.seenInfoKeys.has(entry.key)) {
3665
+ throw new WorldOrbitError(`Duplicate info key "${entry.key}"`, line, tokens[0].column);
3666
+ }
3667
+ section.seenInfoKeys.add(entry.key);
3668
+ section.objectNode.infoEntries.push(entry);
3669
+ return;
3670
+ }
3671
+ const typedBlock = section.activeBlock;
3672
+ const seenKeys = section.seenTypedBlockKeys[typedBlock] ?? (section.seenTypedBlockKeys[typedBlock] = /* @__PURE__ */ new Set());
3673
+ if (seenKeys.has(entry.key)) {
3674
+ throw new WorldOrbitError(`Duplicate ${typedBlock} key "${entry.key}"`, line, tokens[0].column);
3675
+ }
3676
+ seenKeys.add(entry.key);
3677
+ const entries = section.objectNode.typedBlockEntries[typedBlock] ?? (section.objectNode.typedBlockEntries[typedBlock] = []);
3678
+ entries.push(entry);
2869
3679
  return;
2870
3680
  }
2871
- section.objectNode.blockFields.push(parseField2(tokens, line));
3681
+ section.objectNode.fields.push(parseObjectField(tokens, line, section.objectNode.objectType, section.sourceSchemaVersion, section.diagnostics));
2872
3682
  }
2873
3683
  function requireUniqueField(tokens, seenFields, line) {
2874
3684
  if (tokens.length < 2) {
@@ -2888,50 +3698,40 @@ var WorldOrbit = (() => {
2888
3698
  return tokens.slice(1).map((token) => token.value).join(" ").trim();
2889
3699
  }
2890
3700
  function parseObjectTypeTokens(tokens, line) {
2891
- if (tokens.length === 0) {
2892
- throw new WorldOrbitError("Missing value for atlas field", line);
2893
- }
2894
- return tokens.map((token) => {
2895
- const value = token.value;
2896
- if (value !== "star" && value !== "planet" && value !== "moon" && value !== "belt" && value !== "asteroid" && value !== "comet" && value !== "ring" && value !== "structure" && value !== "phenomenon") {
2897
- throw new WorldOrbitError(`Unknown viewpoint object type "${token.value}"`, line, token.column);
2898
- }
2899
- return value;
2900
- });
2901
- }
2902
- function parseTokenList(tokens, line, field) {
2903
- if (tokens.length === 0) {
2904
- throw new WorldOrbitError(`Missing value for field "${field}"`, line);
2905
- }
2906
- return tokens.map((token) => token.value);
3701
+ 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");
2907
3702
  }
2908
3703
  function parseLayerTokens(tokens, line) {
2909
- if (tokens.length === 0) {
2910
- throw new WorldOrbitError('Missing value for field "layers"', line);
2911
- }
2912
- const next = {};
2913
- for (const token of tokens) {
2914
- const enabled = !token.value.startsWith("-") && !token.value.startsWith("!");
2915
- const rawLayer = token.value.replace(/^[-!]+/, "").toLowerCase();
2916
- if (rawLayer === "orbits") {
2917
- next["orbits-back"] = enabled;
2918
- next["orbits-front"] = enabled;
3704
+ const layers = {};
3705
+ for (const token of parseTokenList(tokens, line, "layers")) {
3706
+ const enabled = !token.startsWith("-") && !token.startsWith("!");
3707
+ const raw = token.replace(/^[-!]+/, "").toLowerCase();
3708
+ if (raw === "orbits") {
3709
+ layers["orbits-back"] = enabled;
3710
+ layers["orbits-front"] = enabled;
2919
3711
  continue;
2920
3712
  }
2921
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
2922
- next[rawLayer] = enabled;
2923
- continue;
3713
+ if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "objects" || raw === "labels" || raw === "metadata") {
3714
+ layers[raw] = enabled;
2924
3715
  }
2925
- throw new WorldOrbitError(`Unknown layer token "${token.value}"`, line, token.column);
2926
3716
  }
2927
- return next;
3717
+ return layers;
3718
+ }
3719
+ function parseTokenList(tokens, line, fieldName) {
3720
+ if (tokens.length === 0) {
3721
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, 1);
3722
+ }
3723
+ const values = tokens.map((token) => token.value).filter(Boolean);
3724
+ if (values.length === 0) {
3725
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, tokens[0]?.column ?? 1);
3726
+ }
3727
+ return values;
2928
3728
  }
2929
3729
  function parseProjectionValue(value, line, column) {
2930
3730
  const normalized = value.toLowerCase();
2931
- if (normalized === "topdown" || normalized === "isometric") {
2932
- return normalized;
3731
+ if (normalized !== "topdown" && normalized !== "isometric") {
3732
+ throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
2933
3733
  }
2934
- throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
3734
+ return normalized;
2935
3735
  }
2936
3736
  function parsePresetValue(value, line, column) {
2937
3737
  const normalized = value.toLowerCase();
@@ -2941,16 +3741,16 @@ var WorldOrbit = (() => {
2941
3741
  throw new WorldOrbitError(`Unknown render preset "${value}"`, line, column);
2942
3742
  }
2943
3743
  function parsePositiveNumber2(value, line, column, field) {
2944
- const parsed = Number(value);
2945
- if (!Number.isFinite(parsed) || parsed <= 0) {
2946
- throw new WorldOrbitError(`Field "${field}" expects a positive number`, line, column);
3744
+ const parsed = parseFiniteNumber2(value, line, column, field);
3745
+ if (parsed <= 0) {
3746
+ throw new WorldOrbitError(`Field "${field}" must be greater than zero`, line, column);
2947
3747
  }
2948
3748
  return parsed;
2949
3749
  }
2950
3750
  function parseFiniteNumber2(value, line, column, field) {
2951
3751
  const parsed = Number(value);
2952
3752
  if (!Number.isFinite(parsed)) {
2953
- throw new WorldOrbitError(`Field "${field}" expects a finite number`, line, column);
3753
+ throw new WorldOrbitError(`Invalid numeric value "${value}" for "${field}"`, line, column);
2954
3754
  }
2955
3755
  return parsed;
2956
3756
  }
@@ -2962,28 +3762,43 @@ var WorldOrbit = (() => {
2962
3762
  groupIds: []
2963
3763
  };
2964
3764
  }
2965
- function parseInlineFields2(tokens, line) {
3765
+ function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
2966
3766
  const fields = [];
2967
3767
  let index = 0;
2968
3768
  while (index < tokens.length) {
2969
3769
  const keyToken = tokens[index];
2970
- const schema = getFieldSchema(keyToken.value);
2971
- if (!schema) {
3770
+ const spec = getDraftObjectFieldSpec(keyToken.value);
3771
+ if (!spec) {
2972
3772
  throw new WorldOrbitError(`Unknown field "${keyToken.value}"`, line, keyToken.column);
2973
3773
  }
3774
+ if (spec.version === "2.1") {
3775
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, keyToken.value, {
3776
+ line,
3777
+ column: keyToken.column
3778
+ });
3779
+ }
2974
3780
  index++;
2975
3781
  const valueTokens = [];
2976
- if (schema.arity === "multiple") {
2977
- while (index < tokens.length && !isKnownFieldKey(tokens[index].value)) {
2978
- valueTokens.push(tokens[index]);
2979
- index++;
2980
- }
2981
- } else {
3782
+ if (spec.inlineMode === "single") {
2982
3783
  const nextToken = tokens[index];
2983
3784
  if (nextToken) {
2984
3785
  valueTokens.push(nextToken);
2985
3786
  index++;
2986
3787
  }
3788
+ } else if (spec.inlineMode === "pair") {
3789
+ for (let count = 0; count < 2; count++) {
3790
+ const nextToken = tokens[index];
3791
+ if (!nextToken) {
3792
+ break;
3793
+ }
3794
+ valueTokens.push(nextToken);
3795
+ index++;
3796
+ }
3797
+ } else {
3798
+ while (index < tokens.length && !DRAFT_OBJECT_FIELD_KEYS.has(tokens[index].value)) {
3799
+ valueTokens.push(tokens[index]);
3800
+ index++;
3801
+ }
2987
3802
  }
2988
3803
  if (valueTokens.length === 0) {
2989
3804
  throw new WorldOrbitError(`Missing value for field "${keyToken.value}"`, line, keyToken.column);
@@ -2995,25 +3810,35 @@ var WorldOrbit = (() => {
2995
3810
  location: { line, column: keyToken.column }
2996
3811
  });
2997
3812
  }
3813
+ validateDraftObjectFieldCompatibility(fields, objectType);
2998
3814
  return fields;
2999
3815
  }
3000
- function parseField2(tokens, line) {
3816
+ function parseObjectField(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
3001
3817
  if (tokens.length < 2) {
3002
3818
  throw new WorldOrbitError("Invalid field line", line, tokens[0]?.column ?? 1);
3003
3819
  }
3004
- if (!getFieldSchema(tokens[0].value)) {
3820
+ const spec = getDraftObjectFieldSpec(tokens[0].value);
3821
+ if (!spec) {
3005
3822
  throw new WorldOrbitError(`Unknown field "${tokens[0].value}"`, line, tokens[0].column);
3006
3823
  }
3007
- return {
3824
+ if (spec.version === "2.1") {
3825
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, tokens[0].value, {
3826
+ line,
3827
+ column: tokens[0].column
3828
+ });
3829
+ }
3830
+ const field = {
3008
3831
  type: "field",
3009
3832
  key: tokens[0].value,
3010
3833
  values: tokens.slice(1).map((token) => token.value),
3011
3834
  location: { line, column: tokens[0].column }
3012
3835
  };
3836
+ validateDraftObjectFieldCompatibility([field], objectType);
3837
+ return field;
3013
3838
  }
3014
- function parseInfoEntry2(tokens, line) {
3839
+ function parseInfoLikeEntry(tokens, line, errorMessage) {
3015
3840
  if (tokens.length < 2) {
3016
- throw new WorldOrbitError("Invalid info entry", line, tokens[0]?.column ?? 1);
3841
+ throw new WorldOrbitError(errorMessage, line, tokens[0]?.column ?? 1);
3017
3842
  }
3018
3843
  return {
3019
3844
  type: "info-entry",
@@ -3022,18 +3847,348 @@ var WorldOrbit = (() => {
3022
3847
  location: { line, column: tokens[0].column }
3023
3848
  };
3024
3849
  }
3025
- function normalizeIdentifier(value) {
3026
- return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
3850
+ function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
3851
+ const fieldMap = collectDraftFields(node.fields);
3852
+ const placement = extractDraftPlacement(node.objectType, fieldMap);
3853
+ const properties = normalizeDraftProperties(node.objectType, fieldMap);
3854
+ const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
3855
+ const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
3856
+ const referencePlane = parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0]);
3857
+ const tidalLock = fieldMap.has("tidalLock") ? parseAtlasBoolean(singleFieldValue2(fieldMap.get("tidalLock")[0]), "tidalLock", fieldMap.get("tidalLock")[0].location) : void 0;
3858
+ const resonance = fieldMap.has("resonance") ? parseResonanceField(fieldMap.get("resonance")[0]) : void 0;
3859
+ const renderHints = extractRenderHints(fieldMap);
3860
+ const deriveRules = fieldMap.get("derive")?.map((field) => parseDeriveField(field));
3861
+ const validationRules = fieldMap.get("validate")?.map((field) => ({
3862
+ rule: singleFieldValue2(field)
3863
+ }));
3864
+ const lockedFields = fieldMap.has("locked") ? [...new Set(fieldMap.get("locked").flatMap((field) => field.values))] : void 0;
3865
+ const tolerances = fieldMap.get("tolerance")?.map((field) => parseToleranceField(field));
3866
+ const typedBlocks = normalizeTypedBlocks(node.typedBlockEntries);
3867
+ const info2 = normalizeInfoEntries(node.infoEntries, "info");
3868
+ const object = {
3869
+ type: node.objectType,
3870
+ id: node.id,
3871
+ properties,
3872
+ placement,
3873
+ info: info2
3874
+ };
3875
+ if (groups.length > 0)
3876
+ object.groups = groups;
3877
+ if (epoch)
3878
+ object.epoch = epoch;
3879
+ if (referencePlane)
3880
+ object.referencePlane = referencePlane;
3881
+ if (tidalLock !== void 0)
3882
+ object.tidalLock = tidalLock;
3883
+ if (resonance)
3884
+ object.resonance = resonance;
3885
+ if (renderHints)
3886
+ object.renderHints = renderHints;
3887
+ if (deriveRules?.length)
3888
+ object.deriveRules = deriveRules;
3889
+ if (validationRules?.length)
3890
+ object.validationRules = validationRules;
3891
+ if (lockedFields?.length)
3892
+ object.lockedFields = lockedFields;
3893
+ if (tolerances?.length)
3894
+ object.tolerances = tolerances;
3895
+ if (typedBlocks && Object.keys(typedBlocks).length > 0)
3896
+ object.typedBlocks = typedBlocks;
3897
+ if (sourceSchemaVersion !== "2.1") {
3898
+ 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) {
3899
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
3900
+ }
3901
+ }
3902
+ return object;
3027
3903
  }
3028
- function humanizeIdentifier2(value) {
3029
- return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
3904
+ function collectDraftFields(fields) {
3905
+ const grouped = /* @__PURE__ */ new Map();
3906
+ for (const field of fields) {
3907
+ const spec = getDraftObjectFieldSpec(field.key);
3908
+ if (!spec) {
3909
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
3910
+ }
3911
+ if (!spec.allowRepeat && grouped.has(field.key)) {
3912
+ throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
3913
+ }
3914
+ const existing = grouped.get(field.key) ?? [];
3915
+ existing.push(field);
3916
+ grouped.set(field.key, existing);
3917
+ }
3918
+ return grouped;
3919
+ }
3920
+ function extractDraftPlacement(objectType, fieldMap) {
3921
+ const orbitField = fieldMap.get("orbit")?.[0];
3922
+ const atField = fieldMap.get("at")?.[0];
3923
+ const surfaceField = fieldMap.get("surface")?.[0];
3924
+ const freeField = fieldMap.get("free")?.[0];
3925
+ const count = [orbitField, atField, surfaceField, freeField].filter(Boolean).length;
3926
+ if (count > 1) {
3927
+ const conflictingField = orbitField ?? atField ?? surfaceField ?? freeField;
3928
+ throw WorldOrbitError.fromLocation("Object has multiple placement modes", conflictingField?.location);
3929
+ }
3930
+ if (orbitField) {
3931
+ return {
3932
+ mode: "orbit",
3933
+ target: singleFieldValue2(orbitField),
3934
+ distance: parseOptionalUnitField(fieldMap.get("distance")?.[0], "distance"),
3935
+ semiMajor: parseOptionalUnitField(fieldMap.get("semiMajor")?.[0], "semiMajor"),
3936
+ eccentricity: parseOptionalNumberField(fieldMap.get("eccentricity")?.[0], "eccentricity"),
3937
+ period: parseOptionalUnitField(fieldMap.get("period")?.[0], "period"),
3938
+ angle: parseOptionalUnitField(fieldMap.get("angle")?.[0], "angle"),
3939
+ inclination: parseOptionalUnitField(fieldMap.get("inclination")?.[0], "inclination"),
3940
+ phase: parseOptionalUnitField(fieldMap.get("phase")?.[0], "phase")
3941
+ };
3942
+ }
3943
+ if (atField) {
3944
+ const target = singleFieldValue2(atField);
3945
+ return {
3946
+ mode: "at",
3947
+ target,
3948
+ reference: parseAtlasAtReference(target, atField.location)
3949
+ };
3950
+ }
3951
+ if (surfaceField) {
3952
+ return {
3953
+ mode: "surface",
3954
+ target: singleFieldValue2(surfaceField)
3955
+ };
3956
+ }
3957
+ if (freeField) {
3958
+ const raw = singleFieldValue2(freeField);
3959
+ const distance = tryParseAtlasUnitValue(raw);
3960
+ return {
3961
+ mode: "free",
3962
+ distance: distance ?? void 0,
3963
+ descriptor: distance ? void 0 : raw
3964
+ };
3965
+ }
3966
+ return null;
3967
+ }
3968
+ function normalizeDraftProperties(objectType, fieldMap) {
3969
+ const properties = {};
3970
+ for (const [key, fields] of fieldMap.entries()) {
3971
+ const field = fields[0];
3972
+ const spec = getDraftObjectFieldSpec(key);
3973
+ if (!field || !spec?.legacySchema || spec.legacySchema.placement) {
3974
+ continue;
3975
+ }
3976
+ ensureAtlasFieldSupported(key, objectType, field.location);
3977
+ properties[key] = normalizeLegacyScalarValue(key, field.values, field.location);
3978
+ }
3979
+ return properties;
3980
+ }
3981
+ function normalizeInfoEntries(entries, label) {
3982
+ const normalized = {};
3983
+ for (const entry of entries) {
3984
+ if (entry.key in normalized) {
3985
+ throw WorldOrbitError.fromLocation(`Duplicate ${label} key "${entry.key}"`, entry.location);
3986
+ }
3987
+ normalized[entry.key] = entry.value;
3988
+ }
3989
+ return normalized;
3990
+ }
3991
+ function normalizeTypedBlocks(typedBlockEntries) {
3992
+ const typedBlocks = {};
3993
+ for (const blockName of Object.keys(typedBlockEntries)) {
3994
+ const entries = typedBlockEntries[blockName];
3995
+ if (entries?.length) {
3996
+ typedBlocks[blockName] = normalizeInfoEntries(entries, blockName);
3997
+ }
3998
+ }
3999
+ return typedBlocks;
4000
+ }
4001
+ function extractRenderHints(fieldMap) {
4002
+ const renderHints = {};
4003
+ const renderLabelField = fieldMap.get("renderLabel")?.[0];
4004
+ const renderOrbitField = fieldMap.get("renderOrbit")?.[0];
4005
+ const renderPriorityField = fieldMap.get("renderPriority")?.[0];
4006
+ if (renderLabelField) {
4007
+ renderHints.renderLabel = parseAtlasBoolean(singleFieldValue2(renderLabelField), "renderLabel", renderLabelField.location);
4008
+ }
4009
+ if (renderOrbitField) {
4010
+ renderHints.renderOrbit = parseAtlasBoolean(singleFieldValue2(renderOrbitField), "renderOrbit", renderOrbitField.location);
4011
+ }
4012
+ if (renderPriorityField) {
4013
+ renderHints.renderPriority = parseAtlasNumber(singleFieldValue2(renderPriorityField), "renderPriority", renderPriorityField.location);
4014
+ }
4015
+ return Object.keys(renderHints).length > 0 ? renderHints : void 0;
4016
+ }
4017
+ function parseResonanceField(field) {
4018
+ if (field.values.length !== 2) {
4019
+ throw WorldOrbitError.fromLocation('Field "resonance" expects "<targetObjectId> <ratio>"', field.location);
4020
+ }
4021
+ const ratio = field.values[1];
4022
+ if (!/^\d+:\d+$/.test(ratio)) {
4023
+ throw WorldOrbitError.fromLocation(`Invalid resonance ratio "${ratio}"`, field.location);
4024
+ }
4025
+ return {
4026
+ targetObjectId: field.values[0],
4027
+ ratio
4028
+ };
4029
+ }
4030
+ function parseDeriveField(field) {
4031
+ if (field.values.length !== 2) {
4032
+ throw WorldOrbitError.fromLocation('Field "derive" expects "<field> <strategy>"', field.location);
4033
+ }
4034
+ return {
4035
+ field: field.values[0],
4036
+ strategy: field.values[1]
4037
+ };
4038
+ }
4039
+ function parseToleranceField(field) {
4040
+ if (field.values.length !== 2) {
4041
+ throw WorldOrbitError.fromLocation('Field "tolerance" expects "<field> <value>"', field.location);
4042
+ }
4043
+ const rawValue = field.values[1];
4044
+ const unitValue = tryParseAtlasUnitValue(rawValue);
4045
+ const numericValue2 = Number(rawValue);
4046
+ return {
4047
+ field: field.values[0],
4048
+ value: unitValue ?? (Number.isFinite(numericValue2) ? numericValue2 : rawValue)
4049
+ };
4050
+ }
4051
+ function parseOptionalTokenList(field) {
4052
+ return field ? [...new Set(field.values)] : [];
4053
+ }
4054
+ function parseOptionalJoinedValue(field) {
4055
+ if (!field) {
4056
+ return null;
4057
+ }
4058
+ return field.values.join(" ").trim() || null;
4059
+ }
4060
+ function parseOptionalUnitField(field, key) {
4061
+ return field ? parseAtlasUnitValue(singleFieldValue2(field), field.location, key) : void 0;
4062
+ }
4063
+ function parseOptionalNumberField(field, key) {
4064
+ return field ? parseAtlasNumber(singleFieldValue2(field), key, field.location) : void 0;
4065
+ }
4066
+ function singleFieldValue2(field) {
4067
+ return singleAtlasValue(field.values, field.key, field.location);
4068
+ }
4069
+ function getDraftObjectFieldSpec(key) {
4070
+ return DRAFT_OBJECT_FIELD_SPECS.get(key);
4071
+ }
4072
+ function validateDraftObjectFieldCompatibility(fields, objectType) {
4073
+ for (const field of fields) {
4074
+ const spec = getDraftObjectFieldSpec(field.key);
4075
+ if (!spec) {
4076
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
4077
+ }
4078
+ if (spec.legacySchema) {
4079
+ ensureAtlasFieldSupported(field.key, objectType, field.location);
4080
+ continue;
4081
+ }
4082
+ if ((field.key === "renderLabel" || field.key === "renderOrbit" || field.key === "tidalLock") && field.values.length !== 1) {
4083
+ throw WorldOrbitError.fromLocation(`Field "${field.key}" expects exactly one value`, field.location);
4084
+ }
4085
+ }
4086
+ }
4087
+ function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
4088
+ if (sourceSchemaVersion === "2.1") {
4089
+ return;
4090
+ }
4091
+ diagnostics.push({
4092
+ code: "parse.schema21.featureCompatibility",
4093
+ severity: "warning",
4094
+ source: "parse",
4095
+ message: `Feature "${featureName}" requires schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
4096
+ line: location.line,
4097
+ column: location.column
4098
+ });
4099
+ }
4100
+ function preprocessAtlasSource(source) {
4101
+ const chars = [...source];
4102
+ const comments = [];
4103
+ let inString = false;
4104
+ let inBlockComment = false;
4105
+ let blockCommentStart = null;
4106
+ let line = 1;
4107
+ let column = 1;
4108
+ for (let index = 0; index < chars.length; index++) {
4109
+ const ch = chars[index];
4110
+ const next = chars[index + 1];
4111
+ if (inBlockComment) {
4112
+ if (ch === "*" && next === "/") {
4113
+ chars[index] = " ";
4114
+ chars[index + 1] = " ";
4115
+ inBlockComment = false;
4116
+ blockCommentStart = null;
4117
+ index++;
4118
+ column += 2;
4119
+ continue;
4120
+ }
4121
+ if (ch !== "\n" && ch !== "\r") {
4122
+ chars[index] = " ";
4123
+ }
4124
+ if (ch === "\n") {
4125
+ line++;
4126
+ column = 1;
4127
+ } else {
4128
+ column++;
4129
+ }
4130
+ continue;
4131
+ }
4132
+ if (!inString && ch === "/" && next === "*") {
4133
+ comments.push({ kind: "block", line, column });
4134
+ chars[index] = " ";
4135
+ chars[index + 1] = " ";
4136
+ inBlockComment = true;
4137
+ blockCommentStart = { line, column };
4138
+ index++;
4139
+ column += 2;
4140
+ continue;
4141
+ }
4142
+ if (!inString && ch === "#" && !isHexColorLiteral(chars, index)) {
4143
+ comments.push({ kind: "line", line, column });
4144
+ chars[index] = " ";
4145
+ let inner = index + 1;
4146
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
4147
+ chars[inner] = " ";
4148
+ inner++;
4149
+ }
4150
+ column += inner - index;
4151
+ index = inner - 1;
4152
+ continue;
4153
+ }
4154
+ if (ch === '"' && chars[index - 1] !== "\\") {
4155
+ inString = !inString;
4156
+ }
4157
+ if (ch === "\n") {
4158
+ line++;
4159
+ column = 1;
4160
+ } else {
4161
+ column++;
4162
+ }
4163
+ }
4164
+ if (inBlockComment) {
4165
+ throw WorldOrbitError.fromLocation("Unclosed block comment", blockCommentStart ?? void 0);
4166
+ }
4167
+ return {
4168
+ source: chars.join(""),
4169
+ comments
4170
+ };
4171
+ }
4172
+ function isHexColorLiteral(chars, start) {
4173
+ let index = start + 1;
4174
+ let length = 0;
4175
+ while (index < chars.length && /[0-9a-f]/i.test(chars[index] ?? "")) {
4176
+ index++;
4177
+ length++;
4178
+ }
4179
+ if (![3, 4, 6, 8].includes(length)) {
4180
+ return false;
4181
+ }
4182
+ const next = chars[index];
4183
+ return next === void 0 || next === " " || next === " " || next === "\r" || next === "\n";
3030
4184
  }
3031
4185
 
3032
4186
  // packages/core/dist/load.js
3033
- var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0)?$/i;
4187
+ var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1)?$/i;
4188
+ var ATLAS_SCHEMA_21_PATTERN = /^schema\s+2\.1$/i;
3034
4189
  var LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
3035
4190
  function detectWorldOrbitSchemaVersion(source) {
3036
- for (const line of source.split(/\r?\n/)) {
4191
+ for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
3037
4192
  const trimmed = line.trim();
3038
4193
  if (!trimmed) {
3039
4194
  continue;
@@ -3041,6 +4196,9 @@ var WorldOrbit = (() => {
3041
4196
  if (LEGACY_DRAFT_SCHEMA_PATTERN.test(trimmed)) {
3042
4197
  return "2.0-draft";
3043
4198
  }
4199
+ if (ATLAS_SCHEMA_21_PATTERN.test(trimmed)) {
4200
+ return "2.1";
4201
+ }
3044
4202
  if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
3045
4203
  return "2.0";
3046
4204
  }
@@ -3048,6 +4206,49 @@ var WorldOrbit = (() => {
3048
4206
  }
3049
4207
  return "1.0";
3050
4208
  }
4209
+ function stripCommentsForSchemaDetection(source) {
4210
+ const chars = [...source];
4211
+ let inString = false;
4212
+ let inBlockComment = false;
4213
+ for (let index = 0; index < chars.length; index++) {
4214
+ const ch = chars[index];
4215
+ const next = chars[index + 1];
4216
+ if (inBlockComment) {
4217
+ if (ch === "*" && next === "/") {
4218
+ chars[index] = " ";
4219
+ chars[index + 1] = " ";
4220
+ inBlockComment = false;
4221
+ index++;
4222
+ continue;
4223
+ }
4224
+ if (ch !== "\n" && ch !== "\r") {
4225
+ chars[index] = " ";
4226
+ }
4227
+ continue;
4228
+ }
4229
+ if (!inString && ch === "/" && next === "*") {
4230
+ chars[index] = " ";
4231
+ chars[index + 1] = " ";
4232
+ inBlockComment = true;
4233
+ index++;
4234
+ continue;
4235
+ }
4236
+ if (!inString && ch === "#") {
4237
+ chars[index] = " ";
4238
+ let inner = index + 1;
4239
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
4240
+ chars[inner] = " ";
4241
+ inner++;
4242
+ }
4243
+ index = inner - 1;
4244
+ continue;
4245
+ }
4246
+ if (ch === '"' && chars[index - 1] !== "\\") {
4247
+ inString = !inString;
4248
+ }
4249
+ }
4250
+ return chars.join("");
4251
+ }
3051
4252
  function loadWorldOrbitSource(source) {
3052
4253
  const result = loadWorldOrbitSourceWithDiagnostics(source);
3053
4254
  if (!result.ok || !result.value) {
@@ -3058,36 +4259,36 @@ var WorldOrbit = (() => {
3058
4259
  }
3059
4260
  function loadWorldOrbitSourceWithDiagnostics(source) {
3060
4261
  const schemaVersion = detectWorldOrbitSchemaVersion(source);
3061
- if (schemaVersion === "2.0" || schemaVersion === "2.0-draft") {
4262
+ if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1") {
3062
4263
  return loadAtlasSourceWithDiagnostics(source, schemaVersion);
3063
4264
  }
3064
4265
  let ast;
3065
4266
  try {
3066
4267
  ast = parseWorldOrbit(source);
3067
- } catch (error) {
4268
+ } catch (error2) {
3068
4269
  return {
3069
4270
  ok: false,
3070
4271
  value: null,
3071
- diagnostics: [diagnosticFromError(error, "parse")]
4272
+ diagnostics: [diagnosticFromError(error2, "parse")]
3072
4273
  };
3073
4274
  }
3074
4275
  let document2;
3075
4276
  try {
3076
4277
  document2 = normalizeDocument(ast);
3077
- } catch (error) {
4278
+ } catch (error2) {
3078
4279
  return {
3079
4280
  ok: false,
3080
4281
  value: null,
3081
- diagnostics: [diagnosticFromError(error, "normalize")]
4282
+ diagnostics: [diagnosticFromError(error2, "normalize")]
3082
4283
  };
3083
4284
  }
3084
4285
  try {
3085
4286
  validateDocument(document2);
3086
- } catch (error) {
4287
+ } catch (error2) {
3087
4288
  return {
3088
4289
  ok: false,
3089
4290
  value: null,
3090
- diagnostics: [diagnosticFromError(error, "validate")]
4291
+ diagnostics: [diagnosticFromError(error2, "validate")]
3091
4292
  };
3092
4293
  }
3093
4294
  return {
@@ -3107,30 +4308,29 @@ var WorldOrbit = (() => {
3107
4308
  let atlasDocument;
3108
4309
  try {
3109
4310
  atlasDocument = parseWorldOrbitAtlas(source);
3110
- } catch (error) {
4311
+ } catch (error2) {
3111
4312
  return {
3112
4313
  ok: false,
3113
4314
  value: null,
3114
- diagnostics: [diagnosticFromError(error, "parse", "load.atlas.failed")]
4315
+ diagnostics: [diagnosticFromError(error2, "parse", "load.atlas.failed")]
3115
4316
  };
3116
4317
  }
3117
- let document2;
3118
- try {
3119
- document2 = materializeAtlasDocument(atlasDocument);
3120
- } catch (error) {
4318
+ const atlasDiagnostics = [...atlasDocument.diagnostics];
4319
+ if (atlasDiagnostics.some((diagnostic) => diagnostic.severity === "error")) {
3121
4320
  return {
3122
4321
  ok: false,
3123
4322
  value: null,
3124
- diagnostics: [diagnosticFromError(error, "normalize", "load.atlas.materialize.failed")]
4323
+ diagnostics: atlasDiagnostics
3125
4324
  };
3126
4325
  }
4326
+ let document2;
3127
4327
  try {
3128
- validateDocument(document2);
3129
- } catch (error) {
4328
+ document2 = materializeAtlasDocument(atlasDocument);
4329
+ } catch (error2) {
3130
4330
  return {
3131
4331
  ok: false,
3132
4332
  value: null,
3133
- diagnostics: [diagnosticFromError(error, "validate", "load.atlas.validate.failed")]
4333
+ diagnostics: [diagnosticFromError(error2, "normalize", "load.atlas.materialize.failed")]
3134
4334
  };
3135
4335
  }
3136
4336
  const loaded = {
@@ -3139,12 +4339,12 @@ var WorldOrbit = (() => {
3139
4339
  document: document2,
3140
4340
  atlasDocument,
3141
4341
  draftDocument: atlasDocument,
3142
- diagnostics: [...atlasDocument.diagnostics]
4342
+ diagnostics: atlasDiagnostics
3143
4343
  };
3144
4344
  return {
3145
4345
  ok: true,
3146
4346
  value: loaded,
3147
- diagnostics: [...atlasDocument.diagnostics]
4347
+ diagnostics: atlasDiagnostics
3148
4348
  };
3149
4349
  }
3150
4350
 
@@ -3152,6 +4352,7 @@ var WorldOrbit = (() => {
3152
4352
  var DEFAULT_LAYERS = {
3153
4353
  background: true,
3154
4354
  guides: true,
4355
+ relations: true,
3155
4356
  orbits: true,
3156
4357
  objects: true,
3157
4358
  labels: true,
@@ -3166,6 +4367,7 @@ var WorldOrbit = (() => {
3166
4367
  backgroundGlow: "rgba(240, 180, 100, 0.18)",
3167
4368
  panel: "rgba(7, 17, 27, 0.9)",
3168
4369
  panelLine: "rgba(168, 207, 242, 0.18)",
4370
+ relation: "rgba(240, 180, 100, 0.42)",
3169
4371
  orbit: "rgba(163, 209, 255, 0.24)",
3170
4372
  orbitBand: "rgba(255, 190, 120, 0.28)",
3171
4373
  guide: "rgba(255, 255, 255, 0.04)",
@@ -3188,6 +4390,7 @@ var WorldOrbit = (() => {
3188
4390
  backgroundGlow: "rgba(120, 255, 215, 0.16)",
3189
4391
  panel: "rgba(7, 20, 30, 0.9)",
3190
4392
  panelLine: "rgba(120, 255, 215, 0.16)",
4393
+ relation: "rgba(156, 231, 255, 0.42)",
3191
4394
  orbit: "rgba(120, 255, 215, 0.2)",
3192
4395
  orbitBand: "rgba(137, 185, 255, 0.24)",
3193
4396
  guide: "rgba(255, 255, 255, 0.035)",
@@ -3210,6 +4413,7 @@ var WorldOrbit = (() => {
3210
4413
  backgroundGlow: "rgba(255, 127, 95, 0.18)",
3211
4414
  panel: "rgba(24, 9, 13, 0.9)",
3212
4415
  panelLine: "rgba(255, 166, 149, 0.16)",
4416
+ relation: "rgba(255, 178, 125, 0.42)",
3213
4417
  orbit: "rgba(255, 188, 164, 0.22)",
3214
4418
  orbitBand: "rgba(255, 214, 139, 0.24)",
3215
4419
  guide: "rgba(255, 255, 255, 0.03)",
@@ -3291,7 +4495,11 @@ var WorldOrbit = (() => {
3291
4495
  return false;
3292
4496
  }
3293
4497
  if (filter.groupIds?.length && (!object.groupId || !filter.groupIds.includes(object.groupId))) {
3294
- return false;
4498
+ const hasSemanticMatch = object.semanticGroupIds.length > 0 && filter.groupIds.some((groupId) => object.semanticGroupIds.includes(groupId));
4499
+ const hasLegacyMatch = Boolean(object.groupId && filter.groupIds.includes(object.groupId));
4500
+ if (!hasSemanticMatch && !hasLegacyMatch) {
4501
+ return false;
4502
+ }
3295
4503
  }
3296
4504
  if (filter.tags?.length) {
3297
4505
  const objectTags = Array.isArray(object.object.properties.tags) ? object.object.properties.tags.filter((entry) => typeof entry === "string") : [];
@@ -3347,6 +4555,7 @@ var WorldOrbit = (() => {
3347
4555
  const imageDefinitions = buildImageDefinitions(visibleObjects);
3348
4556
  const orbitMarkup = layers.orbits ? renderOrbitLayer(scene, visibleObjectIds, layers.structures) : { back: "", front: "" };
3349
4557
  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("") : "";
4558
+ 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("") : "";
3350
4559
  const objectMarkup = layers.objects ? visibleObjects.map((object) => renderSceneObject(object, options.selectedObjectId ?? null, theme)).join("") : "";
3351
4560
  const labelMarkup = layers.labels ? visibleLabels.map((label) => renderSceneLabel(scene, label, options.selectedObjectId ?? null)).join("") : "";
3352
4561
  const metadataMarkup = layers.metadata ? `<text class="wo-title" x="56" y="64">${escapeXml(scene.title)}</text>
@@ -3381,6 +4590,7 @@ var WorldOrbit = (() => {
3381
4590
  .wo-orbit-back { opacity: 0.38; stroke-dasharray: 8 6; }
3382
4591
  .wo-orbit-front { opacity: 0.9; }
3383
4592
  .wo-orbit-band { stroke: ${theme.orbitBand}; stroke-linecap: round; }
4593
+ .wo-relation { stroke: ${theme.relation}; stroke-width: 2; stroke-dasharray: 10 6; }
3384
4594
  .wo-leader { stroke: ${theme.leader}; stroke-width: 1.5; stroke-dasharray: 6 5; }
3385
4595
  .wo-label { fill: ${theme.ink}; font-family: ${theme.fontFamily}; font-weight: 600; letter-spacing: 0.02em; }
3386
4596
  .wo-label-secondary { fill: ${theme.muted}; font-family: ${theme.fontFamily}; font-weight: 500; }
@@ -3414,6 +4624,7 @@ var WorldOrbit = (() => {
3414
4624
  <g data-worldorbit-world-content="true">
3415
4625
  ${layers.orbits ? `<g data-layer-id="orbits-back">${orbitMarkup.back}</g>` : ""}
3416
4626
  ${layers.guides ? `<g data-layer-id="guides">${leaderMarkup}</g>` : ""}
4627
+ ${layers.relations ? `<g data-layer-id="relations">${relationMarkup}</g>` : ""}
3417
4628
  ${layers.objects ? `<g data-layer-id="objects">${objectMarkup}</g>` : ""}
3418
4629
  ${layers.orbits ? `<g data-layer-id="orbits-front">${orbitMarkup.front}</g>` : ""}
3419
4630
  ${layers.labels ? `<g data-layer-id="labels">${labelMarkup}</g>` : ""}
@@ -3457,10 +4668,11 @@ var WorldOrbit = (() => {
3457
4668
  function renderSceneObject(sceneObject, selectedObjectId, theme) {
3458
4669
  const { object, x, y, radius, visualRadius } = sceneObject;
3459
4670
  const selectionClass = selectedObjectId === sceneObject.objectId ? " wo-object-selected" : "";
4671
+ const kindClass = object.properties.kind ? ` wo-kind-${String(object.properties.kind).toLowerCase().replace(/[^a-z0-9-]/g, "-")}` : "";
3460
4672
  const palette = resolveObjectPalette(sceneObject, theme);
3461
4673
  const imageMarkup = renderObjectImage(sceneObject);
3462
4674
  const outlineMarkup = imageMarkup ? renderObjectBody(object, x, y, radius, palette, { outlineOnly: true }) : "";
3463
- 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}`)}">
4675
+ 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}`)}">
3464
4676
  <circle class="wo-selection-ring" cx="${x}" cy="${y}" r="${visualRadius + 8}" />
3465
4677
  ${renderAtmosphere(sceneObject, palette)}
3466
4678
  ${renderObjectBody(object, x, y, radius, palette)}
@@ -3494,8 +4706,33 @@ var WorldOrbit = (() => {
3494
4706
  <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
3495
4707
  case "structure":
3496
4708
  return `<polygon points="${diamondPoints(x, y, radius)}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
3497
- case "phenomenon":
4709
+ case "phenomenon": {
4710
+ const kind = String(object.properties.kind ?? "").toLowerCase().replace(/_/g, "-");
4711
+ if (options.outlineOnly) {
4712
+ if (kind === "black-hole" || kind === "nebula" || kind === "galaxy" || kind === "dwarf-galaxy") {
4713
+ return `<circle cx="${x}" cy="${y}" r="${radius}" fill="transparent" stroke="${palette.stroke}" stroke-width="1.4" />`;
4714
+ }
4715
+ return `<polygon points="${phenomenonPoints(x, y, radius)}" fill="transparent" stroke="${palette.stroke}" stroke-width="1.4" />`;
4716
+ }
4717
+ if (kind === "black-hole") {
4718
+ 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" />
4719
+ <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="2" />`;
4720
+ }
4721
+ if (kind === "galaxy") {
4722
+ return `<ellipse cx="${x}" cy="${y}" rx="${radius * 2.6}" ry="${radius}" fill="${palette.halo ?? "none"}" stroke="none" />
4723
+ <ellipse cx="${x}" cy="${y}" rx="${radius * 1.5}" ry="${radius * 0.42}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.2" />
4724
+ <circle cx="${x}" cy="${y}" r="${radius * 0.28}" fill="${palette.core ?? "#fff"}" stroke="none" />`;
4725
+ }
4726
+ if (kind === "dwarf-galaxy") {
4727
+ return `<ellipse cx="${x}" cy="${y}" rx="${radius * 1.6}" ry="${radius * 0.55}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1" />
4728
+ <circle cx="${x}" cy="${y}" r="${radius * 0.25}" fill="${palette.core ?? "#fff"}" stroke="none" />`;
4729
+ }
4730
+ if (kind === "nebula") {
4731
+ return `<circle cx="${x}" cy="${y}" r="${radius * 2.2}" fill="${palette.halo ?? "none"}" stroke="none" />
4732
+ <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1" />`;
4733
+ }
3498
4734
  return `<polygon points="${phenomenonPoints(x, y, radius)}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
4735
+ }
3499
4736
  }
3500
4737
  }
3501
4738
  function renderAtmosphere(sceneObject, palette) {
@@ -3564,7 +4801,8 @@ var WorldOrbit = (() => {
3564
4801
  }
3565
4802
  }
3566
4803
  function resolveObjectPalette(sceneObject, theme) {
3567
- const base = basePaletteForType(sceneObject.object.type, theme);
4804
+ const kind = String(sceneObject.object.properties.kind ?? "").toLowerCase().replace(/_/g, "-");
4805
+ const base = basePaletteForType(sceneObject.object.type, kind, theme);
3568
4806
  const customFill = sceneObject.fillColor && isColorLike(sceneObject.fillColor) ? sceneObject.fillColor : base.fill;
3569
4807
  const albedo = numericValue(sceneObject.object.properties.albedo);
3570
4808
  const temperature = numericValue(sceneObject.object.properties.temperature);
@@ -3580,7 +4818,7 @@ var WorldOrbit = (() => {
3580
4818
  tail: sceneObject.object.type === "comet" ? rgbaString(mixColors(fill, "#ffffff", 0.5) ?? fill, 0.72) : void 0
3581
4819
  };
3582
4820
  }
3583
- function basePaletteForType(type, theme) {
4821
+ function basePaletteForType(type, kind, theme) {
3584
4822
  switch (type) {
3585
4823
  case "star":
3586
4824
  return {
@@ -3602,8 +4840,26 @@ var WorldOrbit = (() => {
3602
4840
  case "structure":
3603
4841
  return { fill: theme.accentStrong, stroke: "#fff2ea" };
3604
4842
  case "phenomenon":
3605
- return { fill: "#78ffd7", stroke: "#e9fff7" };
4843
+ return kindPhenomenonPalette(kind);
4844
+ }
4845
+ }
4846
+ function kindPhenomenonPalette(kind) {
4847
+ if (kind === "galaxy") {
4848
+ return { fill: "rgba(165,125,255,0.55)", stroke: "rgba(210,185,255,0.75)", halo: "rgba(160,120,255,0.10)", core: "#ede0ff" };
4849
+ }
4850
+ if (kind === "dwarf-galaxy") {
4851
+ return { fill: "rgba(190,165,255,0.45)", stroke: "rgba(220,205,255,0.75)", core: "#ddd0ff" };
4852
+ }
4853
+ if (kind === "black-hole") {
4854
+ return { fill: "#040408", stroke: "#ff6a00", accentRing: "rgba(255,140,20,0.72)" };
4855
+ }
4856
+ if (kind === "nebula") {
4857
+ return { fill: "rgba(105,205,255,0.45)", stroke: "rgba(180,235,255,0.72)", halo: "rgba(100,200,255,0.08)" };
4858
+ }
4859
+ if (kind === "void") {
4860
+ return { fill: "#05080f", stroke: "rgba(130,160,255,0.4)" };
3606
4861
  }
4862
+ return { fill: "#78ffd7", stroke: "#e9fff7" };
3607
4863
  }
3608
4864
  function applyTemperatureAndAlbedo(baseColor, temperature, albedo, type) {
3609
4865
  let nextColor = baseColor;
@@ -3870,11 +5126,11 @@ var WorldOrbit = (() => {
3870
5126
  });
3871
5127
  }
3872
5128
  return `<figure class="${escapeAttribute3(options.className ?? "worldorbit-block worldorbit-static")}">${renderSceneToSvg(scene, options)}</figure>`;
3873
- } catch (error) {
5129
+ } catch (error2) {
3874
5130
  if (options.strict) {
3875
- throw error;
5131
+ throw error2;
3876
5132
  }
3877
- return renderWorldOrbitError(error instanceof Error ? error.message : String(error));
5133
+ return renderWorldOrbitError(error2 instanceof Error ? error2.message : String(error2));
3878
5134
  }
3879
5135
  }
3880
5136
  function renderWorldOrbitError(message) {
@@ -3975,5 +5231,5 @@ var WorldOrbit = (() => {
3975
5231
  }
3976
5232
  return (node.children ?? []).map((child) => collectText(child)).join("");
3977
5233
  }
3978
- return __toCommonJS(dist_exports);
5234
+ return __toCommonJS(index_exports);
3979
5235
  })();