worldorbit 2.5.15 → 2.5.17

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 (48) hide show
  1. package/README.md +1 -1
  2. package/dist/browser/core/dist/index.js +2444 -342
  3. package/dist/browser/editor/dist/index.js +11702 -0
  4. package/dist/browser/markdown/dist/index.js +2207 -392
  5. package/dist/browser/viewer/dist/index.js +2302 -382
  6. package/dist/unpkg/core/dist/index.js +2447 -345
  7. package/dist/unpkg/editor/dist/index.js +11727 -0
  8. package/dist/unpkg/markdown/dist/index.js +2210 -395
  9. package/dist/unpkg/viewer/dist/index.js +2305 -385
  10. package/dist/unpkg/worldorbit-core.min.js +12 -12
  11. package/dist/unpkg/worldorbit-editor.min.js +894 -0
  12. package/dist/unpkg/worldorbit-markdown.min.js +66 -58
  13. package/dist/unpkg/worldorbit-viewer.min.js +76 -68
  14. package/dist/unpkg/worldorbit.js +797 -78
  15. package/dist/unpkg/worldorbit.min.js +80 -72
  16. package/package.json +3 -2
  17. package/packages/core/dist/atlas-edit.js +74 -0
  18. package/packages/core/dist/atlas-validate.js +122 -8
  19. package/packages/core/dist/draft-parse.js +212 -8
  20. package/packages/core/dist/draft.d.ts +5 -2
  21. package/packages/core/dist/draft.js +59 -3
  22. package/packages/core/dist/format.js +63 -1
  23. package/packages/core/dist/normalize.js +1 -0
  24. package/packages/core/dist/scene.js +248 -46
  25. package/packages/core/dist/types.d.ts +41 -2
  26. package/packages/editor/dist/editor.d.ts +2 -0
  27. package/packages/editor/dist/editor.js +3578 -0
  28. package/packages/editor/dist/index.d.ts +2 -0
  29. package/packages/editor/dist/index.js +1 -0
  30. package/packages/editor/dist/types.d.ts +55 -0
  31. package/packages/editor/dist/types.js +1 -0
  32. package/packages/markdown/dist/html.d.ts +3 -0
  33. package/packages/markdown/dist/html.js +57 -0
  34. package/packages/markdown/dist/index.d.ts +4 -0
  35. package/packages/markdown/dist/index.js +3 -0
  36. package/packages/markdown/dist/rehype.d.ts +10 -0
  37. package/packages/markdown/dist/rehype.js +49 -0
  38. package/packages/markdown/dist/remark.d.ts +9 -0
  39. package/packages/markdown/dist/remark.js +28 -0
  40. package/packages/markdown/dist/types.d.ts +11 -0
  41. package/packages/markdown/dist/types.js +1 -0
  42. package/packages/viewer/dist/atlas-state.js +6 -0
  43. package/packages/viewer/dist/atlas-viewer.js +1 -0
  44. package/packages/viewer/dist/render.js +31 -2
  45. package/packages/viewer/dist/theme.js +1 -0
  46. package/packages/viewer/dist/tooltip.js +9 -0
  47. package/packages/viewer/dist/types.d.ts +8 -1
  48. package/packages/viewer/dist/viewer.js +12 -1
@@ -19,8 +19,8 @@ var WorldOrbit = (() => {
19
19
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
20
 
21
21
  // packages/core/dist/index.js
22
- var dist_exports = {};
23
- __export(dist_exports, {
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
24
  WORLDORBIT_FIELD_KEYS: () => WORLDORBIT_FIELD_KEYS,
25
25
  WORLDORBIT_FIELD_SCHEMAS: () => WORLDORBIT_FIELD_SCHEMAS,
26
26
  WORLDORBIT_OBJECT_TYPES: () => WORLDORBIT_OBJECT_TYPES,
@@ -373,13 +373,13 @@ var WorldOrbit = (() => {
373
373
  function unitFamilyAllowsUnit(family, unit) {
374
374
  switch (family) {
375
375
  case "distance":
376
- return unit === null || ["au", "km", "re", "sol"].includes(unit);
376
+ return unit === null || ["au", "km", "m", "ly", "pc", "kpc", "re", "sol"].includes(unit);
377
377
  case "radius":
378
- return unit === null || ["km", "re", "sol"].includes(unit);
378
+ return unit === null || ["km", "m", "re", "rj", "sol"].includes(unit);
379
379
  case "mass":
380
- return unit === null || ["me", "sol"].includes(unit);
380
+ return unit === null || ["me", "mj", "sol"].includes(unit);
381
381
  case "duration":
382
- return unit === null || ["h", "d", "y"].includes(unit);
382
+ return unit === null || ["s", "min", "h", "d", "y", "ky", "my", "gy"].includes(unit);
383
383
  case "angle":
384
384
  return unit === null || unit === "deg";
385
385
  case "generic":
@@ -586,7 +586,7 @@ var WorldOrbit = (() => {
586
586
  }
587
587
 
588
588
  // packages/core/dist/normalize.js
589
- var UNIT_PATTERN = /^(-?\d+(?:\.\d+)?)(au|km|re|sol|me|d|y|h|deg)?$/;
589
+ var UNIT_PATTERN = /^(-?\d+(?:\.\d+)?)(kpc|min|mj|rj|ky|my|gy|au|km|me|re|pc|ly|deg|sol|K|m|s|h|d|y)?$/;
590
590
  var BOOLEAN_VALUES = /* @__PURE__ */ new Map([
591
591
  ["true", true],
592
592
  ["false", false],
@@ -611,7 +611,11 @@ var WorldOrbit = (() => {
611
611
  return {
612
612
  format: "worldorbit",
613
613
  version: "1.0",
614
+ schemaVersion: "1.0",
614
615
  system,
616
+ groups: [],
617
+ relations: [],
618
+ events: [],
615
619
  objects
616
620
  };
617
621
  }
@@ -621,13 +625,17 @@ var WorldOrbit = (() => {
621
625
  const fieldMap = collectFields(mergedFields);
622
626
  const placement = extractPlacement(node.objectType, fieldMap);
623
627
  const properties = normalizeProperties(fieldMap);
624
- const info = normalizeInfo(node.infoEntries);
628
+ const info2 = normalizeInfo(node.infoEntries);
625
629
  if (node.objectType === "system") {
626
630
  return {
627
631
  type: "system",
628
632
  id: node.name,
633
+ title: typeof properties.title === "string" ? properties.title : null,
634
+ description: null,
635
+ epoch: null,
636
+ referencePlane: null,
629
637
  properties,
630
- info
638
+ info: info2
631
639
  };
632
640
  }
633
641
  return {
@@ -635,7 +643,7 @@ var WorldOrbit = (() => {
635
643
  id: node.name,
636
644
  properties,
637
645
  placement,
638
- info
646
+ info: info2
639
647
  };
640
648
  }
641
649
  function validateFieldCompatibility(objectType, fields) {
@@ -765,14 +773,14 @@ var WorldOrbit = (() => {
765
773
  }
766
774
  }
767
775
  function normalizeInfo(entries) {
768
- const info = {};
776
+ const info2 = {};
769
777
  for (const entry of entries) {
770
- if (entry.key in info) {
778
+ if (entry.key in info2) {
771
779
  throw WorldOrbitError.fromLocation(`Duplicate info key "${entry.key}"`, entry.location);
772
780
  }
773
- info[entry.key] = entry.value;
781
+ info2[entry.key] = entry.value;
774
782
  }
775
- return info;
783
+ return info2;
776
784
  }
777
785
  function parseAtReference(target, location) {
778
786
  if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
@@ -946,38 +954,38 @@ var WorldOrbit = (() => {
946
954
  function createDiagnostic(diagnostic) {
947
955
  return { ...diagnostic };
948
956
  }
949
- function diagnosticFromError(error, source, code = `${source}.failed`) {
950
- if (error instanceof WorldOrbitError) {
957
+ function diagnosticFromError(error2, source, code = `${source}.failed`) {
958
+ if (error2 instanceof WorldOrbitError) {
951
959
  return {
952
960
  code,
953
961
  severity: "error",
954
962
  source,
955
- message: error.message,
956
- line: error.line,
957
- column: error.column
963
+ message: error2.message,
964
+ line: error2.line,
965
+ column: error2.column
958
966
  };
959
967
  }
960
- if (error instanceof Error) {
968
+ if (error2 instanceof Error) {
961
969
  return {
962
970
  code,
963
971
  severity: "error",
964
972
  source,
965
- message: error.message
973
+ message: error2.message
966
974
  };
967
975
  }
968
976
  return {
969
977
  code,
970
978
  severity: "error",
971
979
  source,
972
- message: String(error)
980
+ message: String(error2)
973
981
  };
974
982
  }
975
983
  function parseWithDiagnostics(source) {
976
984
  let ast;
977
985
  try {
978
986
  ast = parseWorldOrbit(source);
979
- } catch (error) {
980
- const diagnostic = diagnosticFromError(error, "parse");
987
+ } catch (error2) {
988
+ const diagnostic = diagnosticFromError(error2, "parse");
981
989
  return {
982
990
  ok: false,
983
991
  value: null,
@@ -987,20 +995,20 @@ var WorldOrbit = (() => {
987
995
  let document;
988
996
  try {
989
997
  document = normalizeDocument(ast);
990
- } catch (error) {
998
+ } catch (error2) {
991
999
  return {
992
1000
  ok: false,
993
1001
  value: null,
994
- diagnostics: [diagnosticFromError(error, "normalize")]
1002
+ diagnostics: [diagnosticFromError(error2, "normalize")]
995
1003
  };
996
1004
  }
997
1005
  try {
998
1006
  validateDocument(document);
999
- } catch (error) {
1007
+ } catch (error2) {
1000
1008
  return {
1001
1009
  ok: false,
1002
1010
  value: null,
1003
- diagnostics: [diagnosticFromError(error, "validate")]
1011
+ diagnostics: [diagnosticFromError(error2, "validate")]
1004
1012
  };
1005
1013
  }
1006
1014
  return {
@@ -1019,11 +1027,11 @@ var WorldOrbit = (() => {
1019
1027
  value: normalizeDocument(ast),
1020
1028
  diagnostics: []
1021
1029
  };
1022
- } catch (error) {
1030
+ } catch (error2) {
1023
1031
  return {
1024
1032
  ok: false,
1025
1033
  value: null,
1026
- diagnostics: [diagnosticFromError(error, "normalize")]
1034
+ diagnostics: [diagnosticFromError(error2, "normalize")]
1027
1035
  };
1028
1036
  }
1029
1037
  }
@@ -1035,11 +1043,11 @@ var WorldOrbit = (() => {
1035
1043
  value: document,
1036
1044
  diagnostics: []
1037
1045
  };
1038
- } catch (error) {
1046
+ } catch (error2) {
1039
1047
  return {
1040
1048
  ok: false,
1041
1049
  value: null,
1042
- diagnostics: [diagnosticFromError(error, "validate")]
1050
+ diagnostics: [diagnosticFromError(error2, "validate")]
1043
1051
  };
1044
1052
  }
1045
1053
  }
@@ -1047,7 +1055,11 @@ var WorldOrbit = (() => {
1047
1055
  // packages/core/dist/scene.js
1048
1056
  var AU_IN_KM = 1495978707e-1;
1049
1057
  var EARTH_RADIUS_IN_KM = 6371;
1058
+ var JUPITER_RADIUS_IN_KM = 71492;
1050
1059
  var SOLAR_RADIUS_IN_KM = 695700;
1060
+ var LY_IN_AU = 63241.077;
1061
+ var PC_IN_AU = 206264.806;
1062
+ var KPC_IN_AU = 206264806;
1051
1063
  var ISO_FLATTENING = 0.68;
1052
1064
  var MIN_ISO_MINOR_SCALE = 0.2;
1053
1065
  var ARC_SAMPLE_COUNT = 28;
@@ -1061,8 +1073,10 @@ var WorldOrbit = (() => {
1061
1073
  const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
1062
1074
  const spacingFactor = layoutPresetSpacing(layoutPreset);
1063
1075
  const systemId = document.system?.id ?? null;
1064
- const objectMap = new Map(document.objects.map((object) => [object.id, object]));
1065
- const relationships = buildSceneRelationships(document.objects, objectMap);
1076
+ const activeEventId = options.activeEventId ?? null;
1077
+ const effectiveObjects = createEffectiveObjects(document.objects, document.events ?? [], activeEventId);
1078
+ const objectMap = new Map(effectiveObjects.map((object) => [object.id, object]));
1079
+ const relationships = buildSceneRelationships(effectiveObjects, objectMap);
1066
1080
  const positions = /* @__PURE__ */ new Map();
1067
1081
  const orbitDrafts = [];
1068
1082
  const leaderDrafts = [];
@@ -1071,7 +1085,7 @@ var WorldOrbit = (() => {
1071
1085
  const atObjects = [];
1072
1086
  const surfaceChildren = /* @__PURE__ */ new Map();
1073
1087
  const orbitChildren = /* @__PURE__ */ new Map();
1074
- for (const object of document.objects) {
1088
+ for (const object of effectiveObjects) {
1075
1089
  const placement = object.placement;
1076
1090
  if (!placement) {
1077
1091
  rootObjects.push(object);
@@ -1166,11 +1180,14 @@ var WorldOrbit = (() => {
1166
1180
  const objects = [...positions.values()].map((position) => createSceneObject(position, scaleModel, relationships));
1167
1181
  const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
1168
1182
  const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
1169
- const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
1170
- const layers = createSceneLayers(orbitVisuals, leaders, objects, labels);
1171
- const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
1183
+ const labels = createSceneLabels(objects, width, height, scaleModel.labelMultiplier);
1184
+ const relations = createSceneRelations(document, objects);
1185
+ const events = createSceneEvents(document.events ?? [], objects, activeEventId);
1186
+ const layers = createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels);
1187
+ const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, scaleModel.labelMultiplier);
1188
+ const semanticGroups = createSceneSemanticGroups(document, objects);
1172
1189
  const viewpoints = createSceneViewpoints(document, projection, frame.preset, relationships, objectMap);
1173
- const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
1190
+ const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, scaleModel.labelMultiplier);
1174
1191
  return {
1175
1192
  width,
1176
1193
  height,
@@ -1178,7 +1195,7 @@ var WorldOrbit = (() => {
1178
1195
  renderPreset: frame.preset,
1179
1196
  projection,
1180
1197
  scaleModel,
1181
- title: String(document.system?.properties.title ?? document.system?.id ?? "WorldOrbit") || "WorldOrbit",
1198
+ title: String(document.system?.title ?? document.system?.properties.title ?? document.system?.id ?? "WorldOrbit") || "WorldOrbit",
1182
1199
  subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
1183
1200
  systemId,
1184
1201
  viewMode: projection,
@@ -1194,9 +1211,13 @@ var WorldOrbit = (() => {
1194
1211
  contentBounds,
1195
1212
  layers,
1196
1213
  groups,
1214
+ semanticGroups,
1197
1215
  viewpoints,
1216
+ events,
1217
+ activeEventId,
1198
1218
  objects,
1199
1219
  orbitVisuals,
1220
+ relations,
1200
1221
  leaders,
1201
1222
  labels
1202
1223
  };
@@ -1212,6 +1233,35 @@ var WorldOrbit = (() => {
1212
1233
  y: center.y + dx * sin + dy * cos
1213
1234
  };
1214
1235
  }
1236
+ function createEffectiveObjects(objects, events, activeEventId) {
1237
+ const cloned = objects.map((object) => structuredClone(object));
1238
+ if (!activeEventId) {
1239
+ return cloned;
1240
+ }
1241
+ const activeEvent = events.find((event) => event.id === activeEventId);
1242
+ if (!activeEvent) {
1243
+ return cloned;
1244
+ }
1245
+ const objectMap = new Map(cloned.map((object) => [object.id, object]));
1246
+ for (const pose of activeEvent.positions) {
1247
+ const object = objectMap.get(pose.objectId);
1248
+ if (!object) {
1249
+ continue;
1250
+ }
1251
+ object.placement = pose.placement ? structuredClone(pose.placement) : null;
1252
+ if (pose.inner) {
1253
+ object.properties.inner = { ...pose.inner };
1254
+ } else {
1255
+ delete object.properties.inner;
1256
+ }
1257
+ if (pose.outer) {
1258
+ object.properties.outer = { ...pose.outer };
1259
+ } else {
1260
+ delete object.properties.outer;
1261
+ }
1262
+ }
1263
+ return cloned;
1264
+ }
1215
1265
  function resolveLayoutPreset(document) {
1216
1266
  const rawScale = String(document.system?.properties.scale ?? "balanced").toLowerCase();
1217
1267
  switch (rawScale) {
@@ -1306,6 +1356,7 @@ var WorldOrbit = (() => {
1306
1356
  }
1307
1357
  function createSceneObject(position, scaleModel, relationships) {
1308
1358
  const { object, x, y, radius, sortKey, anchorX, anchorY } = position;
1359
+ const renderPriority = object.renderHints?.renderPriority ?? 0;
1309
1360
  return {
1310
1361
  renderId: createRenderId(object.id),
1311
1362
  objectId: object.id,
@@ -1314,11 +1365,12 @@ var WorldOrbit = (() => {
1314
1365
  ancestorIds: relationships.ancestorIds.get(object.id) ?? [],
1315
1366
  childIds: relationships.childIds.get(object.id) ?? [],
1316
1367
  groupId: relationships.groupIds.get(object.id) ?? null,
1368
+ semanticGroupIds: [...object.groups ?? []],
1317
1369
  x,
1318
1370
  y,
1319
1371
  radius,
1320
1372
  visualRadius: visualExtentForObject(object, radius, scaleModel),
1321
- sortKey,
1373
+ sortKey: sortKey + renderPriority * 1e-3,
1322
1374
  anchorX,
1323
1375
  anchorY,
1324
1376
  label: object.id,
@@ -1335,6 +1387,7 @@ var WorldOrbit = (() => {
1335
1387
  object: draft.object,
1336
1388
  parentId: draft.parentId,
1337
1389
  groupId,
1390
+ semanticGroupIds: [...draft.object.groups ?? []],
1338
1391
  kind: draft.kind,
1339
1392
  cx: draft.cx,
1340
1393
  cy: draft.cy,
@@ -1346,7 +1399,7 @@ var WorldOrbit = (() => {
1346
1399
  bandThickness: draft.bandThickness,
1347
1400
  frontArcPath: draft.frontArcPath,
1348
1401
  backArcPath: draft.backArcPath,
1349
- hidden: draft.object.properties.hidden === true
1402
+ hidden: draft.object.properties.hidden === true || draft.object.renderHints?.renderOrbit === false
1350
1403
  };
1351
1404
  }
1352
1405
  function createLeaderLine(draft) {
@@ -1355,6 +1408,7 @@ var WorldOrbit = (() => {
1355
1408
  objectId: draft.object.id,
1356
1409
  object: draft.object,
1357
1410
  groupId: draft.groupId,
1411
+ semanticGroupIds: [...draft.object.groups ?? []],
1358
1412
  x1: draft.x1,
1359
1413
  y1: draft.y1,
1360
1414
  x2: draft.x2,
@@ -1363,42 +1417,144 @@ var WorldOrbit = (() => {
1363
1417
  hidden: draft.object.properties.hidden === true
1364
1418
  };
1365
1419
  }
1366
- function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1420
+ function createSceneLabels(objects, sceneWidth, sceneHeight, labelMultiplier) {
1367
1421
  const labels = [];
1368
1422
  const occupied = [];
1369
- const visibleObjects = [...objects].filter((object) => !object.hidden).sort((left, right) => left.sortKey - right.sortKey);
1423
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1424
+ const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort(compareLabelPlacementOrder);
1370
1425
  for (const object of visibleObjects) {
1371
- const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1372
- const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
1373
- let labelY = object.y + direction * (object.radius + 18 * labelMultiplier);
1374
- let secondaryY = labelY + direction * (16 * labelMultiplier);
1375
- let bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1376
- let attempts = 0;
1377
- while (occupied.some((entry) => rectsOverlap(entry, bounds)) && attempts < 10) {
1378
- labelY += direction * 14 * labelMultiplier;
1379
- secondaryY += direction * 14 * labelMultiplier;
1380
- bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1381
- attempts += 1;
1382
- }
1383
- occupied.push(bounds);
1426
+ const placement = selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) ?? createLabelPlacement(object, defaultVerticalDirection(object, objectMap.get(object.parentId ?? "") ?? null, sceneHeight), 0, labelMultiplier);
1427
+ occupied.push(createLabelRect(object, placement, labelMultiplier));
1384
1428
  labels.push({
1385
1429
  renderId: `${object.renderId}-label`,
1386
1430
  objectId: object.objectId,
1387
1431
  object: object.object,
1388
1432
  groupId: object.groupId,
1433
+ semanticGroupIds: [...object.semanticGroupIds],
1389
1434
  label: object.label,
1390
1435
  secondaryLabel: object.secondaryLabel,
1391
- x: object.x,
1392
- y: labelY,
1393
- secondaryY,
1394
- textAnchor: "middle",
1395
- direction: direction < 0 ? "above" : "below",
1436
+ x: placement.x,
1437
+ y: placement.labelY,
1438
+ secondaryY: placement.secondaryY,
1439
+ textAnchor: placement.textAnchor,
1440
+ direction: placement.direction,
1396
1441
  hidden: object.hidden
1397
1442
  });
1398
1443
  }
1399
1444
  return labels;
1400
1445
  }
1401
- function createSceneLayers(orbitVisuals, leaders, objects, labels) {
1446
+ function compareLabelPlacementOrder(left, right) {
1447
+ const priorityDiff = labelPlacementPriority(left) - labelPlacementPriority(right);
1448
+ if (priorityDiff !== 0) {
1449
+ return priorityDiff;
1450
+ }
1451
+ const renderPriorityDiff = (right.object.renderHints?.renderPriority ?? 0) - (left.object.renderHints?.renderPriority ?? 0);
1452
+ if (renderPriorityDiff !== 0) {
1453
+ return renderPriorityDiff;
1454
+ }
1455
+ return left.sortKey - right.sortKey;
1456
+ }
1457
+ function labelPlacementPriority(object) {
1458
+ switch (object.object.type) {
1459
+ case "star":
1460
+ return 0;
1461
+ case "planet":
1462
+ return 1;
1463
+ case "moon":
1464
+ return 2;
1465
+ case "belt":
1466
+ case "ring":
1467
+ return 3;
1468
+ case "asteroid":
1469
+ case "comet":
1470
+ return 4;
1471
+ case "structure":
1472
+ case "phenomenon":
1473
+ return 5;
1474
+ }
1475
+ }
1476
+ function selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) {
1477
+ for (const direction of preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight)) {
1478
+ const maxAttempts = direction === "left" || direction === "right" ? 4 : 6;
1479
+ for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
1480
+ const placement = createLabelPlacement(object, direction, attempt, labelMultiplier);
1481
+ const rect = createLabelRect(object, placement, labelMultiplier);
1482
+ if (!occupied.some((entry) => rectsOverlap(entry, rect))) {
1483
+ return placement;
1484
+ }
1485
+ }
1486
+ }
1487
+ return null;
1488
+ }
1489
+ function preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight) {
1490
+ const parent = object.parentId ? objectMap.get(object.parentId) ?? null : null;
1491
+ const vertical = defaultVerticalDirection(object, parent, sceneHeight);
1492
+ const oppositeVertical = vertical === "below" ? "above" : "below";
1493
+ const horizontal = defaultHorizontalDirection(object, parent, sceneWidth);
1494
+ const oppositeHorizontal = horizontal === "right" ? "left" : "right";
1495
+ const preferHorizontal = object.object.type === "structure" || object.object.type === "phenomenon" || object.object.placement?.mode === "at" || object.object.placement?.mode === "surface" || object.object.placement?.mode === "free";
1496
+ return preferHorizontal ? [horizontal, vertical, oppositeHorizontal, oppositeVertical] : [vertical, horizontal, oppositeVertical, oppositeHorizontal];
1497
+ }
1498
+ function defaultVerticalDirection(object, parent, sceneHeight) {
1499
+ if (parent && Math.abs(object.y - parent.y) > 6) {
1500
+ return object.y >= parent.y ? "below" : "above";
1501
+ }
1502
+ return object.y > sceneHeight * 0.62 ? "above" : "below";
1503
+ }
1504
+ function defaultHorizontalDirection(object, parent, sceneWidth) {
1505
+ if (parent && Math.abs(object.x - parent.x) > 6) {
1506
+ return object.x >= parent.x ? "right" : "left";
1507
+ }
1508
+ return object.x >= sceneWidth / 2 ? "right" : "left";
1509
+ }
1510
+ function createLabelPlacement(object, direction, attempt, labelMultiplier) {
1511
+ const step = 14 * labelMultiplier;
1512
+ switch (direction) {
1513
+ case "above": {
1514
+ const labelY = object.y - (object.radius + 18 * labelMultiplier + attempt * step);
1515
+ return {
1516
+ x: object.x,
1517
+ labelY,
1518
+ secondaryY: labelY - 16 * labelMultiplier,
1519
+ textAnchor: "middle",
1520
+ direction
1521
+ };
1522
+ }
1523
+ case "below": {
1524
+ const labelY = object.y + object.radius + 18 * labelMultiplier + attempt * step;
1525
+ return {
1526
+ x: object.x,
1527
+ labelY,
1528
+ secondaryY: labelY + 16 * labelMultiplier,
1529
+ textAnchor: "middle",
1530
+ direction
1531
+ };
1532
+ }
1533
+ case "left": {
1534
+ const x = object.x - (object.visualRadius + 16 * labelMultiplier + attempt * step);
1535
+ const labelY = object.y - 4 * labelMultiplier;
1536
+ return {
1537
+ x,
1538
+ labelY,
1539
+ secondaryY: labelY + 16 * labelMultiplier,
1540
+ textAnchor: "end",
1541
+ direction
1542
+ };
1543
+ }
1544
+ case "right": {
1545
+ const x = object.x + object.visualRadius + 16 * labelMultiplier + attempt * step;
1546
+ const labelY = object.y - 4 * labelMultiplier;
1547
+ return {
1548
+ x,
1549
+ labelY,
1550
+ secondaryY: labelY + 16 * labelMultiplier,
1551
+ textAnchor: "start",
1552
+ direction
1553
+ };
1554
+ }
1555
+ }
1556
+ }
1557
+ function createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels) {
1402
1558
  const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1403
1559
  const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1404
1560
  return [
@@ -1409,6 +1565,14 @@ var WorldOrbit = (() => {
1409
1565
  },
1410
1566
  { id: "orbits-back", renderIds: backOrbitIds },
1411
1567
  { id: "orbits-front", renderIds: frontOrbitIds },
1568
+ {
1569
+ id: "relations",
1570
+ renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId)
1571
+ },
1572
+ {
1573
+ id: "events",
1574
+ renderIds: events.filter((event) => !event.hidden).map((event) => event.renderId)
1575
+ },
1412
1576
  {
1413
1577
  id: "objects",
1414
1578
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
@@ -1420,7 +1584,7 @@ var WorldOrbit = (() => {
1420
1584
  { id: "metadata", renderIds: ["wo-title", "wo-subtitle", "wo-meta"] }
1421
1585
  ];
1422
1586
  }
1423
- function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships) {
1587
+ function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, labelMultiplier) {
1424
1588
  const groups = /* @__PURE__ */ new Map();
1425
1589
  const ensureGroup = (groupId) => {
1426
1590
  if (!groupId) {
@@ -1469,10 +1633,63 @@ var WorldOrbit = (() => {
1469
1633
  }
1470
1634
  }
1471
1635
  for (const group of groups.values()) {
1472
- group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels);
1636
+ group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier);
1473
1637
  }
1474
1638
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1475
1639
  }
1640
+ function createSceneSemanticGroups(document, objects) {
1641
+ return [...document.groups].map((group) => ({
1642
+ id: group.id,
1643
+ label: group.label,
1644
+ summary: group.summary,
1645
+ color: group.color,
1646
+ tags: [...group.tags],
1647
+ hidden: group.hidden,
1648
+ objectIds: objects.filter((object) => !object.hidden && object.semanticGroupIds.includes(group.id)).map((object) => object.objectId)
1649
+ })).sort((left, right) => left.label.localeCompare(right.label));
1650
+ }
1651
+ function createSceneRelations(document, objects) {
1652
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1653
+ return document.relations.map((relation) => {
1654
+ const from = objectMap.get(relation.from);
1655
+ const to = objectMap.get(relation.to);
1656
+ return {
1657
+ renderId: `${createRenderId(relation.id)}-relation`,
1658
+ relationId: relation.id,
1659
+ relation,
1660
+ fromObjectId: relation.from,
1661
+ toObjectId: relation.to,
1662
+ x1: from?.x ?? 0,
1663
+ y1: from?.y ?? 0,
1664
+ x2: to?.x ?? 0,
1665
+ y2: to?.y ?? 0,
1666
+ hidden: relation.hidden || !from || !to || from.hidden || to.hidden
1667
+ };
1668
+ }).sort((left, right) => left.relation.id.localeCompare(right.relation.id));
1669
+ }
1670
+ function createSceneEvents(events, objects, activeEventId) {
1671
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1672
+ return events.map((event) => {
1673
+ const objectIds = [.../* @__PURE__ */ new Set([
1674
+ ...event.targetObjectId ? [event.targetObjectId] : [],
1675
+ ...event.participantObjectIds
1676
+ ])];
1677
+ const positions = objectIds.map((objectId) => objectMap.get(objectId)).filter(Boolean);
1678
+ const centroidX = positions.length > 0 ? positions.reduce((sum, object) => sum + object.x, 0) / positions.length : 0;
1679
+ const centroidY = positions.length > 0 ? positions.reduce((sum, object) => sum + object.y, 0) / positions.length : 0;
1680
+ return {
1681
+ renderId: `${createRenderId(event.id)}-event`,
1682
+ eventId: event.id,
1683
+ event,
1684
+ objectIds,
1685
+ participantIds: [...event.participantObjectIds],
1686
+ targetObjectId: event.targetObjectId,
1687
+ x: centroidX,
1688
+ y: centroidY,
1689
+ hidden: event.hidden || positions.length === 0 || positions.every((object) => object.hidden) || activeEventId !== null && event.id !== activeEventId
1690
+ };
1691
+ }).sort((left, right) => left.event.id.localeCompare(right.event.id));
1692
+ }
1476
1693
  function createSceneViewpoints(document, projection, preset, relationships, objectMap) {
1477
1694
  const generatedOverview = createGeneratedOverviewViewpoint(document, projection, preset);
1478
1695
  const drafts = /* @__PURE__ */ new Map();
@@ -1490,7 +1707,7 @@ var WorldOrbit = (() => {
1490
1707
  }
1491
1708
  const field = fieldParts.join(".").toLowerCase();
1492
1709
  const draft = drafts.get(id) ?? { id };
1493
- applyViewpointField(draft, field, value, projection, preset, relationships, objectMap);
1710
+ applyViewpointField(draft, field, value, document, projection, preset, relationships, objectMap);
1494
1711
  drafts.set(id, draft);
1495
1712
  }
1496
1713
  const viewpoints = [...drafts.values()].map((draft) => finalizeViewpointDraft(draft, projection, preset, objectMap)).filter(Boolean);
@@ -1518,13 +1735,15 @@ var WorldOrbit = (() => {
1518
1735
  });
1519
1736
  }
1520
1737
  function createGeneratedOverviewViewpoint(document, projection, preset) {
1521
- const label = document.system?.properties.title ? `${String(document.system.properties.title)} Overview` : "Overview";
1738
+ const title = document.system?.title ?? document.system?.properties.title;
1739
+ const label = title ? `${String(title)} Overview` : "Overview";
1522
1740
  return {
1523
1741
  id: "overview",
1524
1742
  label,
1525
1743
  summary: "Fit the whole system with the current atlas defaults.",
1526
1744
  objectId: null,
1527
1745
  selectedObjectId: null,
1746
+ eventIds: [],
1528
1747
  projection,
1529
1748
  preset,
1530
1749
  rotationDeg: 0,
@@ -1534,7 +1753,7 @@ var WorldOrbit = (() => {
1534
1753
  generated: true
1535
1754
  };
1536
1755
  }
1537
- function applyViewpointField(draft, field, value, projection, preset, relationships, objectMap) {
1756
+ function applyViewpointField(draft, field, value, document, projection, preset, relationships, objectMap) {
1538
1757
  const normalizedValue = value.trim();
1539
1758
  switch (field) {
1540
1759
  case "label":
@@ -1561,6 +1780,9 @@ var WorldOrbit = (() => {
1561
1780
  draft.select = normalizedValue;
1562
1781
  }
1563
1782
  return;
1783
+ case "events":
1784
+ draft.eventIds = splitListValue(normalizedValue);
1785
+ return;
1564
1786
  case "projection":
1565
1787
  case "view":
1566
1788
  draft.projection = parseViewProjection(normalizedValue) ?? projection;
@@ -1601,7 +1823,7 @@ var WorldOrbit = (() => {
1601
1823
  case "groups":
1602
1824
  draft.filter = {
1603
1825
  ...draft.filter ?? createEmptyViewpointFilter(),
1604
- groupIds: parseViewpointGroups(normalizedValue, relationships, objectMap)
1826
+ groupIds: parseViewpointGroups(normalizedValue, document, relationships, objectMap)
1605
1827
  };
1606
1828
  return;
1607
1829
  }
@@ -1617,6 +1839,7 @@ var WorldOrbit = (() => {
1617
1839
  summary: draft.summary?.trim() || createViewpointSummary(label, objectId, filter),
1618
1840
  objectId,
1619
1841
  selectedObjectId,
1842
+ eventIds: [...new Set(draft.eventIds ?? [])],
1620
1843
  projection: draft.projection ?? projection,
1621
1844
  preset: draft.preset ?? preset,
1622
1845
  rotationDeg: draft.rotationDeg ?? 0,
@@ -1674,7 +1897,7 @@ var WorldOrbit = (() => {
1674
1897
  next["orbits-front"] = enabled;
1675
1898
  continue;
1676
1899
  }
1677
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1900
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "events" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1678
1901
  next[rawLayer] = enabled;
1679
1902
  }
1680
1903
  }
@@ -1683,8 +1906,11 @@ var WorldOrbit = (() => {
1683
1906
  function parseViewpointObjectTypes(value) {
1684
1907
  return splitListValue(value).filter((entry) => entry === "star" || entry === "planet" || entry === "moon" || entry === "belt" || entry === "asteroid" || entry === "comet" || entry === "ring" || entry === "structure" || entry === "phenomenon");
1685
1908
  }
1686
- function parseViewpointGroups(value, relationships, objectMap) {
1909
+ function parseViewpointGroups(value, document, relationships, objectMap) {
1687
1910
  return splitListValue(value).map((entry) => {
1911
+ if (document.schemaVersion === "2.1" || document.groups.some((group) => group.id === entry)) {
1912
+ return entry;
1913
+ }
1688
1914
  if (entry.startsWith("wo-") && entry.endsWith("-group")) {
1689
1915
  return entry;
1690
1916
  }
@@ -1719,7 +1945,7 @@ var WorldOrbit = (() => {
1719
1945
  }
1720
1946
  return parts.join(" - ");
1721
1947
  }
1722
- function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels) {
1948
+ function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, labelMultiplier) {
1723
1949
  let minX = Number.POSITIVE_INFINITY;
1724
1950
  let minY = Number.POSITIVE_INFINITY;
1725
1951
  let maxX = Number.NEGATIVE_INFINITY;
@@ -1749,7 +1975,7 @@ var WorldOrbit = (() => {
1749
1975
  for (const label of labels) {
1750
1976
  if (label.hidden)
1751
1977
  continue;
1752
- includeLabelBounds(label, include);
1978
+ includeLabelBounds(label, include, labelMultiplier);
1753
1979
  }
1754
1980
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
1755
1981
  return createBounds(0, 0, width, height);
@@ -1787,13 +2013,10 @@ var WorldOrbit = (() => {
1787
2013
  include(object.x - object.visualRadius - 24, object.y - object.visualRadius - 16);
1788
2014
  include(object.x + object.visualRadius + 24, object.y + object.visualRadius + 36);
1789
2015
  }
1790
- function includeLabelBounds(label, include) {
1791
- const labelScale = 1;
1792
- const labelHalfWidth = estimateLabelHalfWidthFromText(label.label, label.secondaryLabel, labelScale);
1793
- include(label.x - labelHalfWidth, label.y - 18);
1794
- include(label.x + labelHalfWidth, label.y + 8);
1795
- include(label.x - labelHalfWidth, label.secondaryY - 14);
1796
- include(label.x + labelHalfWidth, label.secondaryY + 8);
2016
+ function includeLabelBounds(label, include, labelMultiplier) {
2017
+ const bounds = createLabelRectFromText(label.x, label.y, label.secondaryY, label.textAnchor, label.direction, label.label, label.secondaryLabel, labelMultiplier);
2018
+ include(bounds.left, bounds.top);
2019
+ include(bounds.right, bounds.bottom);
1797
2020
  }
1798
2021
  function placeObject(object, x, y, depth, positions, orbitDrafts, leaderDrafts, context) {
1799
2022
  if (positions.has(object.id)) {
@@ -1815,8 +2038,9 @@ var WorldOrbit = (() => {
1815
2038
  }
1816
2039
  const orbiting = [...context.orbitChildren.get(object.id) ?? []].sort(compareOrbiting);
1817
2040
  const orbitMetricContext = computeOrbitMetricContext(orbiting, parent.radius, context.spacingFactor, context.scaleModel);
2041
+ const orbitRadiiPx = resolveOrbitRadiiPx(orbiting, orbitMetricContext);
1818
2042
  orbiting.forEach((child, index) => {
1819
- const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, context);
2043
+ const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, orbitRadiiPx[index] ?? orbitMetricContext.innerPx, context);
1820
2044
  orbitDrafts.push({
1821
2045
  object: child,
1822
2046
  parentId: object.id,
@@ -1890,7 +2114,8 @@ var WorldOrbit = (() => {
1890
2114
  metricSpread: 0,
1891
2115
  innerPx,
1892
2116
  stepPx,
1893
- pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx)
2117
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
2118
+ minimumGapPx: stepPx * 0.42
1894
2119
  };
1895
2120
  }
1896
2121
  const minMetric = Math.min(...presentMetrics);
@@ -1903,10 +2128,11 @@ var WorldOrbit = (() => {
1903
2128
  metricSpread,
1904
2129
  innerPx,
1905
2130
  stepPx,
1906
- pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx)
2131
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
2132
+ minimumGapPx: stepPx * 0.42
1907
2133
  };
1908
2134
  }
1909
- function resolveOrbitGeometry(object, index, count, parent, metricContext, context) {
2135
+ function resolveOrbitGeometry(object, index, count, parent, metricContext, orbitRadiusPx, context) {
1910
2136
  const placement = object.placement;
1911
2137
  const band = object.type === "belt" || object.type === "ring";
1912
2138
  if (!placement || placement.mode !== "orbit") {
@@ -1924,7 +2150,7 @@ var WorldOrbit = (() => {
1924
2150
  };
1925
2151
  }
1926
2152
  const eccentricity = clampNumber(typeof placement.eccentricity === "number" ? placement.eccentricity : 0, 0, 0.92);
1927
- const semiMajor = resolveOrbitRadiusPx(object, index, metricContext);
2153
+ const semiMajor = orbitRadiusPx;
1928
2154
  const baseMinor = Math.max(semiMajor * Math.sqrt(1 - eccentricity * eccentricity), semiMajor * 0.18);
1929
2155
  const inclinationDeg = unitValueToDegrees(placement.inclination) ?? 0;
1930
2156
  const inclinationScale = context.projection === "isometric" ? Math.max(MIN_ISO_MINOR_SCALE, Math.cos(degreesToRadians(inclinationDeg))) * ISO_FLATTENING : 1;
@@ -1954,15 +2180,19 @@ var WorldOrbit = (() => {
1954
2180
  objectY: objectPoint.y
1955
2181
  };
1956
2182
  }
1957
- function resolveOrbitRadiusPx(object, index, metricContext) {
1958
- const metric = orbitMetric(object);
1959
- if (metric === null) {
1960
- return metricContext.innerPx + index * metricContext.stepPx;
1961
- }
1962
- if (metricContext.metricSpread > 0) {
1963
- return metricContext.innerPx + (metric - metricContext.minMetric) / metricContext.metricSpread * metricContext.pixelSpread;
1964
- }
1965
- return metricContext.innerPx + Math.log10(metric + 1) * metricContext.stepPx;
2183
+ function resolveOrbitRadiusPx(metric, metricContext) {
2184
+ return metricContext.innerPx + metricContext.stepPx * log2(Math.max(metric, 0) + 1);
2185
+ }
2186
+ function resolveOrbitRadiiPx(objects, metricContext) {
2187
+ const radii = [];
2188
+ objects.forEach((object, index) => {
2189
+ const metric = orbitMetric(object);
2190
+ const fallbackRadius = metricContext.innerPx + index * metricContext.stepPx;
2191
+ const baseRadius = metric === null ? fallbackRadius : resolveOrbitRadiusPx(metric, metricContext);
2192
+ const minimumRadius = index === 0 ? metricContext.innerPx : (radii[index - 1] ?? metricContext.innerPx) + metricContext.minimumGapPx;
2193
+ radii.push(Math.max(baseRadius, minimumRadius));
2194
+ });
2195
+ return radii;
1966
2196
  }
1967
2197
  function orbitMetric(object) {
1968
2198
  if (!object.placement || object.placement.mode !== "orbit") {
@@ -1970,6 +2200,9 @@ var WorldOrbit = (() => {
1970
2200
  }
1971
2201
  return toDistanceMetric(object.placement.semiMajor ?? object.placement.distance ?? null);
1972
2202
  }
2203
+ function log2(value) {
2204
+ return Math.log(value) / Math.log(2);
2205
+ }
1973
2206
  function resolveOrbitPhase(phase, index, count) {
1974
2207
  const degreeValue = phase ? unitValueToDegrees(phase) : null;
1975
2208
  if (degreeValue !== null) {
@@ -2173,7 +2406,7 @@ var WorldOrbit = (() => {
2173
2406
  return null;
2174
2407
  }
2175
2408
  }
2176
- function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels) {
2409
+ function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier) {
2177
2410
  let minX = Number.POSITIVE_INFINITY;
2178
2411
  let minY = Number.POSITIVE_INFINITY;
2179
2412
  let maxX = Number.NEGATIVE_INFINITY;
@@ -2202,7 +2435,7 @@ var WorldOrbit = (() => {
2202
2435
  }
2203
2436
  for (const label of labels) {
2204
2437
  if (!label.hidden && group.labelIds.includes(label.objectId)) {
2205
- includeLabelBounds(label, include);
2438
+ includeLabelBounds(label, include, labelMultiplier);
2206
2439
  }
2207
2440
  }
2208
2441
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
@@ -2227,12 +2460,28 @@ var WorldOrbit = (() => {
2227
2460
  }
2228
2461
  return current.id;
2229
2462
  }
2230
- function createLabelRect(x, labelY, secondaryY, labelHalfWidth, direction) {
2463
+ function createLabelRect(object, placement, labelMultiplier) {
2464
+ return createLabelRectFromText(placement.x, placement.labelY, placement.secondaryY, placement.textAnchor, placement.direction, object.label, object.secondaryLabel, labelMultiplier);
2465
+ }
2466
+ function createLabelRectFromText(x, labelY, secondaryY, textAnchor, direction, label, secondaryLabel, labelMultiplier) {
2467
+ const labelHalfWidth = estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier);
2468
+ const labelWidth = labelHalfWidth * 2;
2469
+ const topPadding = direction === "above" ? 18 : 12;
2470
+ const bottomPadding = direction === "above" ? 8 : 12;
2471
+ let left = x - labelHalfWidth;
2472
+ let right = x + labelHalfWidth;
2473
+ if (textAnchor === "start") {
2474
+ left = x;
2475
+ right = x + labelWidth;
2476
+ } else if (textAnchor === "end") {
2477
+ left = x - labelWidth;
2478
+ right = x;
2479
+ }
2231
2480
  return {
2232
- left: x - labelHalfWidth,
2233
- right: x + labelHalfWidth,
2234
- top: Math.min(labelY, secondaryY) - (direction < 0 ? 18 : 12),
2235
- bottom: Math.max(labelY, secondaryY) + (direction < 0 ? 8 : 12)
2481
+ left,
2482
+ right,
2483
+ top: Math.min(labelY, secondaryY) - topPadding,
2484
+ bottom: Math.max(labelY, secondaryY) + bottomPadding
2236
2485
  };
2237
2486
  }
2238
2487
  function rectsOverlap(left, right) {
@@ -2293,8 +2542,18 @@ var WorldOrbit = (() => {
2293
2542
  return value.value;
2294
2543
  case "km":
2295
2544
  return value.value / AU_IN_KM;
2545
+ case "m":
2546
+ return value.value / 1e3 / AU_IN_KM;
2547
+ case "ly":
2548
+ return value.value * LY_IN_AU;
2549
+ case "pc":
2550
+ return value.value * PC_IN_AU;
2551
+ case "kpc":
2552
+ return value.value * KPC_IN_AU;
2296
2553
  case "re":
2297
2554
  return value.value * EARTH_RADIUS_IN_KM / AU_IN_KM;
2555
+ case "rj":
2556
+ return value.value * JUPITER_RADIUS_IN_KM / AU_IN_KM;
2298
2557
  case "sol":
2299
2558
  return value.value * SOLAR_RADIUS_IN_KM / AU_IN_KM;
2300
2559
  default:
@@ -2409,11 +2668,6 @@ var WorldOrbit = (() => {
2409
2668
  function customColorFor(value) {
2410
2669
  return typeof value === "string" && value.trim() ? value : void 0;
2411
2670
  }
2412
- function estimateLabelHalfWidth(object, labelMultiplier) {
2413
- const primaryWidth = object.label.length * 4.6 * labelMultiplier + 18;
2414
- const secondaryWidth = object.secondaryLabel.length * 3.9 * labelMultiplier + 18;
2415
- return Math.max(primaryWidth, secondaryWidth, object.visualRadius + 18);
2416
- }
2417
2671
  function estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier) {
2418
2672
  const primaryWidth = label.length * 4.6 * labelMultiplier + 18;
2419
2673
  const secondaryWidth = secondaryLabel.length * 3.9 * labelMultiplier + 18;
@@ -2448,8 +2702,12 @@ var WorldOrbit = (() => {
2448
2702
  return {
2449
2703
  format: "worldorbit",
2450
2704
  version: "2.0",
2705
+ schemaVersion: "2.0",
2451
2706
  sourceVersion: document.version,
2452
2707
  system,
2708
+ groups: structuredClone(document.groups ?? []),
2709
+ relations: structuredClone(document.relations ?? []),
2710
+ events: structuredClone(document.events ?? []),
2453
2711
  objects: document.objects.map(cloneWorldOrbitObject),
2454
2712
  diagnostics
2455
2713
  };
@@ -2457,18 +2715,28 @@ var WorldOrbit = (() => {
2457
2715
  function upgradeDocumentToDraftV2(document, options = {}) {
2458
2716
  return convertAtlasDocumentToLegacyDraft(upgradeDocumentToV2(document, options));
2459
2717
  }
2460
- function materializeAtlasDocument(document) {
2718
+ function materializeAtlasDocument(document, options = {}) {
2461
2719
  const system = document.system ? {
2462
2720
  type: "system",
2463
2721
  id: document.system.id,
2722
+ title: document.system.title,
2723
+ description: document.system.description,
2724
+ epoch: document.system.epoch,
2725
+ referencePlane: document.system.referencePlane,
2464
2726
  properties: materializeDraftSystemProperties(document.system),
2465
2727
  info: materializeDraftSystemInfo(document.system)
2466
2728
  } : null;
2729
+ const objects = document.objects.map(cloneWorldOrbitObject);
2730
+ applyEventPoseOverrides(objects, document.events ?? [], options.activeEventId ?? null);
2467
2731
  return {
2468
2732
  format: "worldorbit",
2469
2733
  version: "1.0",
2734
+ schemaVersion: document.version,
2470
2735
  system,
2471
- objects: document.objects.map(cloneWorldOrbitObject)
2736
+ groups: structuredClone(document.groups ?? []),
2737
+ relations: structuredClone(document.relations ?? []),
2738
+ events: document.events.map(cloneWorldOrbitEvent),
2739
+ objects
2472
2740
  };
2473
2741
  }
2474
2742
  function materializeDraftDocument(document) {
@@ -2482,7 +2750,10 @@ var WorldOrbit = (() => {
2482
2750
  return {
2483
2751
  type: "system",
2484
2752
  id: document.system?.id ?? "WorldOrbit",
2485
- title: typeof document.system?.properties.title === "string" ? document.system.properties.title : null,
2753
+ title: document.system?.title ?? (typeof document.system?.properties.title === "string" ? document.system.properties.title : null),
2754
+ description: document.system?.description ?? null,
2755
+ epoch: document.system?.epoch ?? null,
2756
+ referencePlane: document.system?.referencePlane ?? null,
2486
2757
  defaults,
2487
2758
  atlasMetadata,
2488
2759
  viewpoints: scene.viewpoints.map(mapSceneViewpointToDraftViewpoint),
@@ -2593,6 +2864,7 @@ var WorldOrbit = (() => {
2593
2864
  summary: viewpoint.summary,
2594
2865
  focusObjectId: viewpoint.objectId,
2595
2866
  selectedObjectId: viewpoint.selectedObjectId,
2867
+ events: [...viewpoint.eventIds],
2596
2868
  projection: viewpoint.projection,
2597
2869
  preset: viewpoint.preset,
2598
2870
  zoom: viewpoint.scale,
@@ -2609,11 +2881,68 @@ var WorldOrbit = (() => {
2609
2881
  function cloneWorldOrbitObject(object) {
2610
2882
  return {
2611
2883
  ...object,
2884
+ groups: object.groups ? [...object.groups] : void 0,
2885
+ resonance: object.resonance ? { ...object.resonance } : object.resonance,
2886
+ renderHints: object.renderHints ? { ...object.renderHints } : object.renderHints,
2887
+ deriveRules: object.deriveRules ? object.deriveRules.map((rule) => ({ ...rule })) : void 0,
2888
+ validationRules: object.validationRules ? object.validationRules.map((rule) => ({ ...rule })) : void 0,
2889
+ lockedFields: object.lockedFields ? [...object.lockedFields] : void 0,
2890
+ tolerances: object.tolerances ? object.tolerances.map((entry) => ({
2891
+ field: entry.field,
2892
+ 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
2893
+ })) : void 0,
2894
+ typedBlocks: object.typedBlocks ? Object.fromEntries(Object.entries(object.typedBlocks).map(([key, block]) => [key, { ...block ?? {} }])) : void 0,
2612
2895
  properties: cloneProperties(object.properties),
2613
2896
  placement: object.placement ? structuredClone(object.placement) : null,
2614
2897
  info: { ...object.info }
2615
2898
  };
2616
2899
  }
2900
+ function cloneWorldOrbitEvent(event) {
2901
+ return {
2902
+ ...event,
2903
+ participantObjectIds: [...event.participantObjectIds],
2904
+ tags: [...event.tags],
2905
+ positions: event.positions.map(cloneWorldOrbitEventPose)
2906
+ };
2907
+ }
2908
+ function cloneWorldOrbitEventPose(pose) {
2909
+ return {
2910
+ objectId: pose.objectId,
2911
+ placement: clonePlacement(pose.placement),
2912
+ inner: pose.inner ? { ...pose.inner } : void 0,
2913
+ outer: pose.outer ? { ...pose.outer } : void 0
2914
+ };
2915
+ }
2916
+ function clonePlacement(placement) {
2917
+ return placement ? structuredClone(placement) : null;
2918
+ }
2919
+ function applyEventPoseOverrides(objects, events, activeEventId) {
2920
+ if (!activeEventId) {
2921
+ return;
2922
+ }
2923
+ const event = events.find((entry) => entry.id === activeEventId);
2924
+ if (!event) {
2925
+ return;
2926
+ }
2927
+ const objectMap = new Map(objects.map((object) => [object.id, object]));
2928
+ for (const pose of event.positions) {
2929
+ const object = objectMap.get(pose.objectId);
2930
+ if (!object) {
2931
+ continue;
2932
+ }
2933
+ object.placement = clonePlacement(pose.placement);
2934
+ if (pose.inner) {
2935
+ object.properties.inner = { ...pose.inner };
2936
+ } else {
2937
+ delete object.properties.inner;
2938
+ }
2939
+ if (pose.outer) {
2940
+ object.properties.outer = { ...pose.outer };
2941
+ } else {
2942
+ delete object.properties.outer;
2943
+ }
2944
+ }
2945
+ }
2617
2946
  function cloneProperties(properties) {
2618
2947
  const next = {};
2619
2948
  for (const [key, value] of Object.entries(properties)) {
@@ -2653,71 +2982,83 @@ var WorldOrbit = (() => {
2653
2982
  if (system.defaults.units) {
2654
2983
  properties.units = system.defaults.units;
2655
2984
  }
2985
+ if (system.description) {
2986
+ properties.description = system.description;
2987
+ }
2988
+ if (system.epoch) {
2989
+ properties.epoch = system.epoch;
2990
+ }
2991
+ if (system.referencePlane) {
2992
+ properties.referencePlane = system.referencePlane;
2993
+ }
2656
2994
  return properties;
2657
2995
  }
2658
2996
  function materializeDraftSystemInfo(system) {
2659
- const info = {
2997
+ const info2 = {
2660
2998
  ...system.atlasMetadata
2661
2999
  };
2662
3000
  if (system.defaults.theme) {
2663
- info["atlas.theme"] = system.defaults.theme;
3001
+ info2["atlas.theme"] = system.defaults.theme;
2664
3002
  }
2665
3003
  for (const viewpoint of system.viewpoints) {
2666
3004
  const prefix = `viewpoint.${viewpoint.id}`;
2667
- info[`${prefix}.label`] = viewpoint.label;
3005
+ info2[`${prefix}.label`] = viewpoint.label;
2668
3006
  if (viewpoint.summary) {
2669
- info[`${prefix}.summary`] = viewpoint.summary;
3007
+ info2[`${prefix}.summary`] = viewpoint.summary;
2670
3008
  }
2671
3009
  if (viewpoint.focusObjectId) {
2672
- info[`${prefix}.focus`] = viewpoint.focusObjectId;
3010
+ info2[`${prefix}.focus`] = viewpoint.focusObjectId;
2673
3011
  }
2674
3012
  if (viewpoint.selectedObjectId) {
2675
- info[`${prefix}.select`] = viewpoint.selectedObjectId;
3013
+ info2[`${prefix}.select`] = viewpoint.selectedObjectId;
2676
3014
  }
2677
3015
  if (viewpoint.projection) {
2678
- info[`${prefix}.projection`] = viewpoint.projection;
3016
+ info2[`${prefix}.projection`] = viewpoint.projection;
2679
3017
  }
2680
3018
  if (viewpoint.preset) {
2681
- info[`${prefix}.preset`] = viewpoint.preset;
3019
+ info2[`${prefix}.preset`] = viewpoint.preset;
2682
3020
  }
2683
3021
  if (viewpoint.zoom !== null) {
2684
- info[`${prefix}.zoom`] = String(viewpoint.zoom);
3022
+ info2[`${prefix}.zoom`] = String(viewpoint.zoom);
2685
3023
  }
2686
3024
  if (viewpoint.rotationDeg !== 0) {
2687
- info[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
3025
+ info2[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2688
3026
  }
2689
3027
  const serializedLayers = serializeViewpointLayers(viewpoint.layers);
2690
3028
  if (serializedLayers) {
2691
- info[`${prefix}.layers`] = serializedLayers;
3029
+ info2[`${prefix}.layers`] = serializedLayers;
2692
3030
  }
2693
3031
  if (viewpoint.filter?.query) {
2694
- info[`${prefix}.query`] = viewpoint.filter.query;
3032
+ info2[`${prefix}.query`] = viewpoint.filter.query;
2695
3033
  }
2696
3034
  if ((viewpoint.filter?.objectTypes.length ?? 0) > 0) {
2697
- info[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
3035
+ info2[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
2698
3036
  }
2699
3037
  if ((viewpoint.filter?.tags.length ?? 0) > 0) {
2700
- info[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
3038
+ info2[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
2701
3039
  }
2702
3040
  if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2703
- info[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
3041
+ info2[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
3042
+ }
3043
+ if (viewpoint.events.length > 0) {
3044
+ info2[`${prefix}.events`] = viewpoint.events.join(" ");
2704
3045
  }
2705
3046
  }
2706
3047
  for (const annotation of system.annotations) {
2707
3048
  const prefix = `annotation.${annotation.id}`;
2708
- info[`${prefix}.label`] = annotation.label;
3049
+ info2[`${prefix}.label`] = annotation.label;
2709
3050
  if (annotation.targetObjectId) {
2710
- info[`${prefix}.target`] = annotation.targetObjectId;
3051
+ info2[`${prefix}.target`] = annotation.targetObjectId;
2711
3052
  }
2712
- info[`${prefix}.body`] = annotation.body;
3053
+ info2[`${prefix}.body`] = annotation.body;
2713
3054
  if (annotation.tags.length > 0) {
2714
- info[`${prefix}.tags`] = annotation.tags.join(" ");
3055
+ info2[`${prefix}.tags`] = annotation.tags.join(" ");
2715
3056
  }
2716
3057
  if (annotation.sourceObjectId) {
2717
- info[`${prefix}.source`] = annotation.sourceObjectId;
3058
+ info2[`${prefix}.source`] = annotation.sourceObjectId;
2718
3059
  }
2719
3060
  }
2720
- return info;
3061
+ return info2;
2721
3062
  }
2722
3063
  function serializeViewpointLayers(layers) {
2723
3064
  const tokens = [];
@@ -2726,7 +3067,7 @@ var WorldOrbit = (() => {
2726
3067
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2727
3068
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2728
3069
  }
2729
- for (const key of ["background", "guides", "objects", "labels", "metadata"]) {
3070
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
2730
3071
  if (layers[key] !== void 0) {
2731
3072
  tokens.push(layers[key] ? key : `-${key}`);
2732
3073
  }
@@ -2736,7 +3077,8 @@ var WorldOrbit = (() => {
2736
3077
  function convertAtlasDocumentToLegacyDraft(document) {
2737
3078
  return {
2738
3079
  ...document,
2739
- version: "2.0-draft"
3080
+ version: "2.0-draft",
3081
+ schemaVersion: "2.0-draft"
2740
3082
  };
2741
3083
  }
2742
3084
 
@@ -2778,19 +3120,28 @@ var WorldOrbit = (() => {
2778
3120
  ];
2779
3121
  function formatDocument(document, options = {}) {
2780
3122
  const schema = options.schema ?? "auto";
2781
- const useDraft = schema === "2.0" || schema === "2.0-draft" || document.version === "2.0" || document.version === "2.0-draft";
3123
+ const useDraft = schema === "2.0" || schema === "2.1" || schema === "2.0-draft" || document.version === "2.0" || document.version === "2.1" || document.version === "2.0-draft";
2782
3124
  if (useDraft) {
2783
3125
  if (schema === "2.0-draft") {
2784
- const legacyDraftDocument = document.version === "2.0-draft" ? document : document.version === "2.0" ? {
3126
+ const legacyDraftDocument = document.version === "2.0-draft" ? document : document.version === "2.0" || document.version === "2.1" ? {
2785
3127
  ...document,
2786
- version: "2.0-draft"
3128
+ version: "2.0-draft",
3129
+ schemaVersion: "2.0-draft"
2787
3130
  } : upgradeDocumentToDraftV2(document);
2788
3131
  return formatDraftDocument(legacyDraftDocument);
2789
3132
  }
2790
- const atlasDocument = document.version === "2.0" ? document : document.version === "2.0-draft" ? {
3133
+ const atlasDocument = document.version === "2.0" || document.version === "2.1" ? document : document.version === "2.0-draft" ? {
2791
3134
  ...document,
2792
- version: "2.0"
3135
+ version: "2.0",
3136
+ schemaVersion: "2.0"
2793
3137
  } : upgradeDocumentToV2(document);
3138
+ if (schema === "2.1" && atlasDocument.version !== "2.1") {
3139
+ return formatAtlasDocument({
3140
+ ...atlasDocument,
3141
+ version: "2.1",
3142
+ schemaVersion: "2.1"
3143
+ });
3144
+ }
2794
3145
  return formatAtlasDocument(atlasDocument);
2795
3146
  }
2796
3147
  const lines = [];
@@ -2808,10 +3159,22 @@ var WorldOrbit = (() => {
2808
3159
  return lines.join("\n");
2809
3160
  }
2810
3161
  function formatAtlasDocument(document) {
2811
- const lines = ["schema 2.0", ""];
3162
+ const lines = [`schema ${document.version}`, ""];
2812
3163
  if (document.system) {
2813
3164
  lines.push(...formatAtlasSystem(document.system));
2814
3165
  }
3166
+ for (const group of [...document.groups].sort(compareIdLike)) {
3167
+ lines.push("");
3168
+ lines.push(...formatAtlasGroup(group));
3169
+ }
3170
+ for (const relation of [...document.relations].sort(compareIdLike)) {
3171
+ lines.push("");
3172
+ lines.push(...formatAtlasRelation(relation));
3173
+ }
3174
+ for (const event of [...document.events].sort(compareIdLike)) {
3175
+ lines.push("");
3176
+ lines.push(...formatAtlasEvent(event));
3177
+ }
2815
3178
  const sortedObjects = [...document.objects].sort(compareObjects);
2816
3179
  if (sortedObjects.length > 0 && lines.at(-1) !== "") {
2817
3180
  lines.push("");
@@ -2827,12 +3190,25 @@ var WorldOrbit = (() => {
2827
3190
  function formatDraftDocument(document) {
2828
3191
  const legacy = document.version === "2.0-draft" ? document : {
2829
3192
  ...document,
2830
- version: "2.0-draft"
3193
+ version: "2.0-draft",
3194
+ schemaVersion: "2.0-draft"
2831
3195
  };
2832
3196
  const lines = ["schema 2.0-draft", ""];
2833
3197
  if (legacy.system) {
2834
3198
  lines.push(...formatAtlasSystem(legacy.system));
2835
3199
  }
3200
+ for (const group of [...legacy.groups].sort(compareIdLike)) {
3201
+ lines.push("");
3202
+ lines.push(...formatAtlasGroup(group));
3203
+ }
3204
+ for (const relation of [...legacy.relations].sort(compareIdLike)) {
3205
+ lines.push("");
3206
+ lines.push(...formatAtlasRelation(relation));
3207
+ }
3208
+ for (const event of [...legacy.events].sort(compareIdLike)) {
3209
+ lines.push("");
3210
+ lines.push(...formatAtlasEvent(event));
3211
+ }
2836
3212
  const sortedObjects = [...legacy.objects].sort(compareObjects);
2837
3213
  if (sortedObjects.length > 0 && lines.at(-1) !== "") {
2838
3214
  lines.push("");
@@ -2848,11 +3224,38 @@ var WorldOrbit = (() => {
2848
3224
  function formatSystem(system) {
2849
3225
  return formatLines("system", system.id, system.properties, null, system.info);
2850
3226
  }
3227
+ function formatLines(objectType, id, properties, placement, info2) {
3228
+ const lines = [`${objectType} ${id}`];
3229
+ const fieldLines = [...formatPlacement(placement), ...formatProperties(properties)];
3230
+ for (const fieldLine of fieldLines) {
3231
+ lines.push(` ${fieldLine}`);
3232
+ }
3233
+ const infoEntries = Object.entries(info2).sort(([left], [right]) => left.localeCompare(right));
3234
+ if (infoEntries.length > 0) {
3235
+ if (fieldLines.length > 0) {
3236
+ lines.push("");
3237
+ }
3238
+ lines.push(" info");
3239
+ for (const [key, value] of infoEntries) {
3240
+ lines.push(` ${key} ${quoteIfNeeded(value)}`);
3241
+ }
3242
+ }
3243
+ return lines;
3244
+ }
2851
3245
  function formatAtlasSystem(system) {
2852
3246
  const lines = [`system ${system.id}`];
2853
3247
  if (system.title) {
2854
3248
  lines.push(` title ${quoteIfNeeded(system.title)}`);
2855
3249
  }
3250
+ if (system.description) {
3251
+ lines.push(` description ${quoteIfNeeded(system.description)}`);
3252
+ }
3253
+ if (system.epoch) {
3254
+ lines.push(` epoch ${quoteIfNeeded(system.epoch)}`);
3255
+ }
3256
+ if (system.referencePlane) {
3257
+ lines.push(` referencePlane ${quoteIfNeeded(system.referencePlane)}`);
3258
+ }
2856
3259
  lines.push("");
2857
3260
  lines.push("defaults");
2858
3261
  lines.push(` view ${system.defaults.view}`);
@@ -2887,18 +3290,22 @@ var WorldOrbit = (() => {
2887
3290
  return lines;
2888
3291
  }
2889
3292
  function formatObject(object) {
2890
- return formatLines(object.type, object.id, object.properties, object.placement, object.info);
3293
+ return formatWorldOrbitObject(object.type, object.id, object);
2891
3294
  }
2892
3295
  function formatAtlasObject(object) {
2893
- return formatLines(`object ${object.type}`, object.id, object.properties, object.placement, object.info);
3296
+ return formatWorldOrbitObject(`object ${object.type}`, object.id, object);
2894
3297
  }
2895
- function formatLines(objectType, id, properties, placement, info) {
3298
+ function formatWorldOrbitObject(objectType, id, object) {
2896
3299
  const lines = [`${objectType} ${id}`];
2897
- const fieldLines = [...formatPlacement(placement), ...formatProperties(properties)];
3300
+ const fieldLines = [
3301
+ ...formatPlacement(object.placement),
3302
+ ...formatProperties(object.properties),
3303
+ ...formatObjectMetadata(object)
3304
+ ];
2898
3305
  for (const fieldLine of fieldLines) {
2899
3306
  lines.push(` ${fieldLine}`);
2900
3307
  }
2901
- const infoEntries = Object.entries(info).sort(([left], [right]) => left.localeCompare(right));
3308
+ const infoEntries = Object.entries(object.info).sort(([left], [right]) => left.localeCompare(right));
2902
3309
  if (infoEntries.length > 0) {
2903
3310
  if (fieldLines.length > 0) {
2904
3311
  lines.push("");
@@ -2908,6 +3315,16 @@ var WorldOrbit = (() => {
2908
3315
  lines.push(` ${key} ${quoteIfNeeded(value)}`);
2909
3316
  }
2910
3317
  }
3318
+ for (const blockName of ["climate", "habitability", "settlement"]) {
3319
+ const blockEntries = Object.entries(object.typedBlocks?.[blockName] ?? {}).sort(([left], [right]) => left.localeCompare(right));
3320
+ if (blockEntries.length > 0) {
3321
+ lines.push("");
3322
+ lines.push(` ${blockName}`);
3323
+ for (const [key, value] of blockEntries) {
3324
+ lines.push(` ${key} ${quoteIfNeeded(value)}`);
3325
+ }
3326
+ }
3327
+ }
2911
3328
  return lines;
2912
3329
  }
2913
3330
  function formatPlacement(placement) {
@@ -2936,6 +3353,46 @@ var WorldOrbit = (() => {
2936
3353
  function formatProperties(properties) {
2937
3354
  return Object.keys(properties).sort(compareFieldKeys).map((key) => `${key} ${formatValue(properties[key])}`);
2938
3355
  }
3356
+ function formatObjectMetadata(object) {
3357
+ const lines = [];
3358
+ if (object.groups?.length) {
3359
+ lines.push(`groups ${object.groups.join(" ")}`);
3360
+ }
3361
+ if (object.epoch) {
3362
+ lines.push(`epoch ${quoteIfNeeded(object.epoch)}`);
3363
+ }
3364
+ if (object.referencePlane) {
3365
+ lines.push(`referencePlane ${quoteIfNeeded(object.referencePlane)}`);
3366
+ }
3367
+ if (object.tidalLock !== void 0) {
3368
+ lines.push(`tidalLock ${object.tidalLock ? "true" : "false"}`);
3369
+ }
3370
+ if (object.renderHints?.renderLabel !== void 0) {
3371
+ lines.push(`renderLabel ${object.renderHints.renderLabel ? "true" : "false"}`);
3372
+ }
3373
+ if (object.renderHints?.renderOrbit !== void 0) {
3374
+ lines.push(`renderOrbit ${object.renderHints.renderOrbit ? "true" : "false"}`);
3375
+ }
3376
+ if (object.renderHints?.renderPriority !== void 0) {
3377
+ lines.push(`renderPriority ${object.renderHints.renderPriority}`);
3378
+ }
3379
+ if (object.resonance) {
3380
+ lines.push(`resonance ${object.resonance.targetObjectId} ${object.resonance.ratio}`);
3381
+ }
3382
+ for (const rule of object.deriveRules ?? []) {
3383
+ lines.push(`derive ${rule.field} ${rule.strategy}`);
3384
+ }
3385
+ for (const rule of object.validationRules ?? []) {
3386
+ lines.push(`validate ${rule.rule}`);
3387
+ }
3388
+ if (object.lockedFields?.length) {
3389
+ lines.push(`locked ${object.lockedFields.join(" ")}`);
3390
+ }
3391
+ for (const tolerance of object.tolerances ?? []) {
3392
+ lines.push(`tolerance ${tolerance.field} ${formatValue(tolerance.value)}`);
3393
+ }
3394
+ return lines;
3395
+ }
2939
3396
  function formatAtlasViewpoint(viewpoint) {
2940
3397
  const lines = [`viewpoint ${viewpoint.id}`, ` label ${quoteIfNeeded(viewpoint.label)}`];
2941
3398
  if (viewpoint.focusObjectId) {
@@ -2963,6 +3420,9 @@ var WorldOrbit = (() => {
2963
3420
  if (layerTokens.length > 0) {
2964
3421
  lines.push(` layers ${layerTokens.join(" ")}`);
2965
3422
  }
3423
+ if (viewpoint.events.length > 0) {
3424
+ lines.push(` events ${viewpoint.events.join(" ")}`);
3425
+ }
2966
3426
  if (viewpoint.filter) {
2967
3427
  lines.push(" filter");
2968
3428
  if (viewpoint.filter.query) {
@@ -2991,6 +3451,98 @@ var WorldOrbit = (() => {
2991
3451
  }
2992
3452
  return lines;
2993
3453
  }
3454
+ function formatAtlasGroup(group) {
3455
+ const lines = [`group ${group.id}`, ` label ${quoteIfNeeded(group.label)}`];
3456
+ if (group.summary) {
3457
+ lines.push(` summary ${quoteIfNeeded(group.summary)}`);
3458
+ }
3459
+ if (group.color) {
3460
+ lines.push(` color ${quoteIfNeeded(group.color)}`);
3461
+ }
3462
+ if (group.tags.length > 0) {
3463
+ lines.push(` tags ${group.tags.map(quoteIfNeeded).join(" ")}`);
3464
+ }
3465
+ if (group.hidden) {
3466
+ lines.push(" hidden true");
3467
+ }
3468
+ return lines;
3469
+ }
3470
+ function formatAtlasRelation(relation) {
3471
+ const lines = [`relation ${relation.id}`];
3472
+ if (relation.from) {
3473
+ lines.push(` from ${quoteIfNeeded(relation.from)}`);
3474
+ }
3475
+ if (relation.to) {
3476
+ lines.push(` to ${quoteIfNeeded(relation.to)}`);
3477
+ }
3478
+ if (relation.kind) {
3479
+ lines.push(` kind ${quoteIfNeeded(relation.kind)}`);
3480
+ }
3481
+ if (relation.label) {
3482
+ lines.push(` label ${quoteIfNeeded(relation.label)}`);
3483
+ }
3484
+ if (relation.summary) {
3485
+ lines.push(` summary ${quoteIfNeeded(relation.summary)}`);
3486
+ }
3487
+ if (relation.tags.length > 0) {
3488
+ lines.push(` tags ${relation.tags.map(quoteIfNeeded).join(" ")}`);
3489
+ }
3490
+ if (relation.color) {
3491
+ lines.push(` color ${quoteIfNeeded(relation.color)}`);
3492
+ }
3493
+ if (relation.hidden) {
3494
+ lines.push(" hidden true");
3495
+ }
3496
+ return lines;
3497
+ }
3498
+ function formatAtlasEvent(event) {
3499
+ const lines = [`event ${event.id}`, ` kind ${quoteIfNeeded(event.kind)}`];
3500
+ if (event.label) {
3501
+ lines.push(` label ${quoteIfNeeded(event.label)}`);
3502
+ }
3503
+ if (event.summary) {
3504
+ lines.push(` summary ${quoteIfNeeded(event.summary)}`);
3505
+ }
3506
+ if (event.targetObjectId) {
3507
+ lines.push(` target ${event.targetObjectId}`);
3508
+ }
3509
+ if (event.participantObjectIds.length > 0) {
3510
+ lines.push(` participants ${event.participantObjectIds.join(" ")}`);
3511
+ }
3512
+ if (event.timing) {
3513
+ lines.push(` timing ${quoteIfNeeded(event.timing)}`);
3514
+ }
3515
+ if (event.visibility) {
3516
+ lines.push(` visibility ${quoteIfNeeded(event.visibility)}`);
3517
+ }
3518
+ if (event.tags.length > 0) {
3519
+ lines.push(` tags ${event.tags.map(quoteIfNeeded).join(" ")}`);
3520
+ }
3521
+ if (event.color) {
3522
+ lines.push(` color ${quoteIfNeeded(event.color)}`);
3523
+ }
3524
+ if (event.hidden) {
3525
+ lines.push(" hidden true");
3526
+ }
3527
+ if (event.positions.length > 0) {
3528
+ lines.push("");
3529
+ lines.push(" positions");
3530
+ for (const pose of [...event.positions].sort(comparePoseObjectId)) {
3531
+ lines.push(` pose ${pose.objectId}`);
3532
+ for (const fieldLine of formatEventPoseFields(pose)) {
3533
+ lines.push(` ${fieldLine}`);
3534
+ }
3535
+ }
3536
+ }
3537
+ return lines;
3538
+ }
3539
+ function formatEventPoseFields(pose) {
3540
+ return [
3541
+ ...formatPlacement(pose.placement),
3542
+ ...formatOptionalUnit("inner", pose.inner),
3543
+ ...formatOptionalUnit("outer", pose.outer)
3544
+ ];
3545
+ }
2994
3546
  function formatValue(value) {
2995
3547
  if (Array.isArray(value)) {
2996
3548
  return value.map((item) => quoteIfNeeded(item)).join(" ");
@@ -3032,7 +3584,7 @@ var WorldOrbit = (() => {
3032
3584
  if (orbitFront !== void 0 || orbitBack !== void 0) {
3033
3585
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
3034
3586
  }
3035
- for (const key of ["background", "guides", "objects", "labels", "metadata"]) {
3587
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
3036
3588
  if (layers[key] !== void 0) {
3037
3589
  tokens.push(layers[key] ? key : `-${key}`);
3038
3590
  }
@@ -3057,6 +3609,12 @@ var WorldOrbit = (() => {
3057
3609
  return leftIndex - rightIndex;
3058
3610
  return left.id.localeCompare(right.id);
3059
3611
  }
3612
+ function compareIdLike(left, right) {
3613
+ return left.id.localeCompare(right.id);
3614
+ }
3615
+ function comparePoseObjectId(left, right) {
3616
+ return left.objectId.localeCompare(right.objectId);
3617
+ }
3060
3618
  function objectTypeIndex(objectType) {
3061
3619
  switch (objectType) {
3062
3620
  case "star":
@@ -3086,107 +3644,760 @@ var WorldOrbit = (() => {
3086
3644
  return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
3087
3645
  }
3088
3646
 
3089
- // packages/core/dist/draft-parse.js
3090
- function parseWorldOrbitAtlas(source) {
3091
- return parseAtlasSource(source, "2.0");
3647
+ // packages/core/dist/atlas-utils.js
3648
+ 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)?$/;
3649
+ var BOOLEAN_VALUES2 = /* @__PURE__ */ new Map([
3650
+ ["true", true],
3651
+ ["false", false],
3652
+ ["yes", true],
3653
+ ["no", false]
3654
+ ]);
3655
+ var URL_SCHEME_PATTERN2 = /^[A-Za-z][A-Za-z0-9+.-]*:/;
3656
+ function normalizeIdentifier2(value) {
3657
+ return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
3092
3658
  }
3093
- function parseWorldOrbitDraft(source) {
3094
- return parseAtlasSource(source, "2.0-draft");
3659
+ function humanizeIdentifier3(value) {
3660
+ return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
3095
3661
  }
3096
- function parseAtlasSource(source, outputVersion) {
3097
- const lines = source.split(/\r?\n/);
3098
- let sawSchemaHeader = false;
3099
- let schemaVersion = "2.0";
3100
- let system = null;
3101
- let section = null;
3102
- const objectNodes = [];
3103
- let sawDefaults = false;
3104
- let sawAtlas = false;
3105
- const viewpointIds = /* @__PURE__ */ new Set();
3106
- const annotationIds = /* @__PURE__ */ new Set();
3107
- for (let index = 0; index < lines.length; index++) {
3108
- const rawLine = lines[index];
3109
- const lineNumber = index + 1;
3110
- if (!rawLine.trim()) {
3111
- continue;
3112
- }
3113
- const indent = getIndent(rawLine);
3114
- const tokens = tokenizeLineDetailed(rawLine.slice(indent), {
3115
- line: lineNumber,
3116
- columnOffset: indent
3117
- });
3118
- if (tokens.length === 0) {
3119
- continue;
3120
- }
3121
- if (!sawSchemaHeader) {
3122
- schemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
3123
- sawSchemaHeader = true;
3124
- continue;
3125
- }
3126
- if (indent === 0) {
3127
- section = startTopLevelSection(tokens, lineNumber, system, objectNodes, viewpointIds, annotationIds, {
3128
- sawDefaults,
3129
- sawAtlas
3130
- });
3131
- if (section.kind === "system") {
3132
- system = section.system;
3133
- } else if (section.kind === "defaults") {
3134
- sawDefaults = true;
3135
- } else if (section.kind === "atlas") {
3136
- sawAtlas = true;
3137
- }
3138
- continue;
3139
- }
3140
- if (!section) {
3141
- throw new WorldOrbitError("Indented line without parent atlas section", lineNumber, indent + 1);
3142
- }
3143
- handleSectionLine(section, indent, tokens, lineNumber);
3144
- }
3145
- if (!sawSchemaHeader) {
3146
- throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
3662
+ function parseAtlasUnitValue(input, location, fieldKey) {
3663
+ const match = input.match(UNIT_PATTERN2);
3664
+ if (!match) {
3665
+ throw WorldOrbitError.fromLocation(`Invalid unit value "${input}"`, location);
3147
3666
  }
3148
- const ast = {
3149
- type: "document",
3150
- objects: objectNodes
3667
+ const unitValue = {
3668
+ value: Number(match[1]),
3669
+ unit: match[2] ?? null
3151
3670
  };
3152
- const normalizedObjects = normalizeDocument(ast).objects;
3153
- validateDocument({
3154
- format: "worldorbit",
3155
- version: "1.0",
3156
- system: null,
3157
- objects: normalizedObjects
3158
- });
3159
- const diagnostics = schemaVersion === "2.0-draft" && outputVersion === "2.0" ? [
3160
- {
3161
- code: "load.schema.deprecatedDraft",
3162
- severity: "warning",
3163
- source: "upgrade",
3164
- message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".'
3671
+ if (fieldKey) {
3672
+ const schema = getFieldSchema(fieldKey);
3673
+ if (schema?.unitFamily && !unitFamilyAllowsUnit(schema.unitFamily, unitValue.unit)) {
3674
+ throw WorldOrbitError.fromLocation(`Unit "${unitValue.unit ?? "none"}" is not valid for "${fieldKey}"`, location);
3165
3675
  }
3166
- ] : [];
3676
+ }
3677
+ return unitValue;
3678
+ }
3679
+ function tryParseAtlasUnitValue(input) {
3680
+ const match = input.match(UNIT_PATTERN2);
3681
+ if (!match) {
3682
+ return null;
3683
+ }
3167
3684
  return {
3685
+ value: Number(match[1]),
3686
+ unit: match[2] ?? null
3687
+ };
3688
+ }
3689
+ function parseAtlasNumber(input, key, location) {
3690
+ const value = Number(input);
3691
+ if (!Number.isFinite(value)) {
3692
+ throw WorldOrbitError.fromLocation(`Invalid numeric value "${input}" for "${key}"`, location);
3693
+ }
3694
+ return value;
3695
+ }
3696
+ function parseAtlasBoolean(input, key, location) {
3697
+ const parsed = BOOLEAN_VALUES2.get(input.toLowerCase());
3698
+ if (parsed === void 0) {
3699
+ throw WorldOrbitError.fromLocation(`Invalid boolean value "${input}" for "${key}"`, location);
3700
+ }
3701
+ return parsed;
3702
+ }
3703
+ function parseAtlasAtReference(target, location) {
3704
+ if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
3705
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
3706
+ }
3707
+ const pairedMatch = target.match(/^([A-Za-z0-9._-]+)-([A-Za-z0-9._-]+):(L[1-5])$/);
3708
+ if (pairedMatch) {
3709
+ return {
3710
+ kind: "lagrange",
3711
+ primary: pairedMatch[1],
3712
+ secondary: pairedMatch[2],
3713
+ point: pairedMatch[3]
3714
+ };
3715
+ }
3716
+ const simpleMatch = target.match(/^([A-Za-z0-9._-]+):(L[1-5])$/);
3717
+ if (simpleMatch) {
3718
+ return {
3719
+ kind: "lagrange",
3720
+ primary: simpleMatch[1],
3721
+ secondary: null,
3722
+ point: simpleMatch[2]
3723
+ };
3724
+ }
3725
+ if (/^[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
3726
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
3727
+ }
3728
+ const anchorMatch = target.match(/^([A-Za-z0-9._-]+):([A-Za-z0-9._-]+)$/);
3729
+ if (anchorMatch) {
3730
+ return {
3731
+ kind: "anchor",
3732
+ objectId: anchorMatch[1],
3733
+ anchor: anchorMatch[2]
3734
+ };
3735
+ }
3736
+ return {
3737
+ kind: "named",
3738
+ name: target
3739
+ };
3740
+ }
3741
+ function validateAtlasImageSource(value, location) {
3742
+ if (!value) {
3743
+ throw WorldOrbitError.fromLocation('Field "image" must not be empty', location);
3744
+ }
3745
+ if (value.startsWith("//")) {
3746
+ throw WorldOrbitError.fromLocation('Field "image" must use a relative path, root-relative path, or an http/https URL', location);
3747
+ }
3748
+ const schemeMatch = value.match(URL_SCHEME_PATTERN2);
3749
+ if (!schemeMatch) {
3750
+ return;
3751
+ }
3752
+ const scheme = schemeMatch[0].slice(0, -1).toLowerCase();
3753
+ if (scheme !== "http" && scheme !== "https") {
3754
+ throw WorldOrbitError.fromLocation(`Field "image" does not support the "${scheme}" scheme`, location);
3755
+ }
3756
+ }
3757
+ function normalizeLegacyScalarValue(key, values, location) {
3758
+ const schema = getFieldSchema(key);
3759
+ if (!schema) {
3760
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
3761
+ }
3762
+ if (schema.arity === "single" && values.length !== 1) {
3763
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
3764
+ }
3765
+ switch (schema.kind) {
3766
+ case "list":
3767
+ return values;
3768
+ case "boolean":
3769
+ return parseAtlasBoolean(singleAtlasValue(values, key, location), key, location);
3770
+ case "number":
3771
+ return parseAtlasNumber(singleAtlasValue(values, key, location), key, location);
3772
+ case "unit":
3773
+ return parseAtlasUnitValue(singleAtlasValue(values, key, location), location, key);
3774
+ case "string": {
3775
+ const value = values.join(" ").trim();
3776
+ if (key === "image") {
3777
+ validateAtlasImageSource(value, location);
3778
+ }
3779
+ return value;
3780
+ }
3781
+ }
3782
+ }
3783
+ function ensureAtlasFieldSupported(key, objectType, location) {
3784
+ const schema = getFieldSchema(key);
3785
+ if (!schema) {
3786
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
3787
+ }
3788
+ if (!schema.objectTypes.includes(objectType)) {
3789
+ throw WorldOrbitError.fromLocation(`Field "${key}" is not valid on "${objectType}"`, location);
3790
+ }
3791
+ }
3792
+ function singleAtlasValue(values, key, location) {
3793
+ if (values.length !== 1) {
3794
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
3795
+ }
3796
+ return values[0];
3797
+ }
3798
+
3799
+ // packages/core/dist/atlas-validate.js
3800
+ var SURFACE_TARGET_TYPES2 = /* @__PURE__ */ new Set(["star", "planet", "moon", "asteroid", "comet"]);
3801
+ var EARTH_MASSES_PER_SOLAR = 332946.0487;
3802
+ var JUPITER_MASSES_PER_SOLAR = 1047.3486;
3803
+ var AU_IN_KM2 = 1495978707e-1;
3804
+ var EARTH_RADIUS_IN_KM2 = 6371;
3805
+ var SOLAR_RADIUS_IN_KM2 = 695700;
3806
+ var LY_IN_AU2 = 63241.077;
3807
+ var PC_IN_AU2 = 206264.806;
3808
+ var KPC_IN_AU2 = 206264806;
3809
+ function collectAtlasDiagnostics(document, sourceSchemaVersion) {
3810
+ const diagnostics = [];
3811
+ const objectMap = new Map(document.objects.map((object) => [object.id, object]));
3812
+ const groupIds = new Set(document.groups.map((group) => group.id));
3813
+ const eventIds = new Set(document.events.map((event) => event.id));
3814
+ if (!document.system) {
3815
+ diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
3816
+ }
3817
+ const knownIds = /* @__PURE__ */ new Map();
3818
+ for (const [kind, ids] of [
3819
+ ["group", document.groups.map((group) => group.id)],
3820
+ ["viewpoint", document.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
3821
+ ["annotation", document.system?.annotations.map((annotation) => annotation.id) ?? []],
3822
+ ["relation", document.relations.map((relation) => relation.id)],
3823
+ ["event", document.events.map((event) => event.id)],
3824
+ ["object", document.objects.map((object) => object.id)]
3825
+ ]) {
3826
+ for (const id of ids) {
3827
+ const previous = knownIds.get(id);
3828
+ if (previous) {
3829
+ diagnostics.push(error("validate.id.duplicate", `Duplicate ${kind} id "${id}" already used by ${previous}.`));
3830
+ } else {
3831
+ knownIds.set(id, kind);
3832
+ }
3833
+ }
3834
+ }
3835
+ for (const relation of document.relations) {
3836
+ validateRelation(relation, objectMap, diagnostics);
3837
+ }
3838
+ for (const viewpoint of document.system?.viewpoints ?? []) {
3839
+ validateViewpoint(viewpoint.filter, viewpoint.events ?? [], groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpoint.id);
3840
+ }
3841
+ for (const object of document.objects) {
3842
+ validateObject(object, document.system, objectMap, groupIds, diagnostics);
3843
+ }
3844
+ for (const event of document.events) {
3845
+ validateEvent(event, objectMap, diagnostics);
3846
+ }
3847
+ return diagnostics;
3848
+ }
3849
+ function validateRelation(relation, objectMap, diagnostics) {
3850
+ if (!relation.from) {
3851
+ diagnostics.push(error("validate.relation.from.required", `Relation "${relation.id}" is missing a "from" target.`));
3852
+ } else if (!objectMap.has(relation.from)) {
3853
+ diagnostics.push(error("validate.relation.from.unknown", `Unknown relation source "${relation.from}" on "${relation.id}".`));
3854
+ }
3855
+ if (!relation.to) {
3856
+ diagnostics.push(error("validate.relation.to.required", `Relation "${relation.id}" is missing a "to" target.`));
3857
+ } else if (!objectMap.has(relation.to)) {
3858
+ diagnostics.push(error("validate.relation.to.unknown", `Unknown relation target "${relation.to}" on "${relation.id}".`));
3859
+ }
3860
+ if (!relation.kind) {
3861
+ diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
3862
+ }
3863
+ }
3864
+ function validateViewpoint(filter, eventRefs, groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpointId) {
3865
+ if (sourceSchemaVersion === "2.1") {
3866
+ if (filter) {
3867
+ for (const groupId of filter.groupIds) {
3868
+ if (!groupIds.has(groupId)) {
3869
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.groups`));
3870
+ }
3871
+ }
3872
+ }
3873
+ for (const eventId of eventRefs) {
3874
+ if (!eventIds.has(eventId)) {
3875
+ diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.events`));
3876
+ }
3877
+ }
3878
+ }
3879
+ }
3880
+ function validateObject(object, system, objectMap, groupIds, diagnostics) {
3881
+ const placement = object.placement;
3882
+ const orbitPlacement = placement?.mode === "orbit" ? placement : null;
3883
+ const parentObject = placement?.mode === "orbit" ? objectMap.get(placement.target) ?? null : null;
3884
+ if (object.groups) {
3885
+ for (const groupId of object.groups) {
3886
+ if (!groupIds.has(groupId)) {
3887
+ diagnostics.push(warn("validate.group.unknown", `Unknown group "${groupId}" on "${object.id}".`, object.id, "groups"));
3888
+ }
3889
+ }
3890
+ }
3891
+ if (orbitPlacement) {
3892
+ if (!objectMap.has(orbitPlacement.target)) {
3893
+ diagnostics.push(error("validate.orbit.target.unknown", `Unknown placement target "${orbitPlacement.target}" on "${object.id}".`, object.id, "orbit"));
3894
+ }
3895
+ if (orbitPlacement.distance && orbitPlacement.semiMajor) {
3896
+ diagnostics.push(error("validate.orbit.distanceConflict", `Object "${object.id}" cannot declare both "distance" and "semiMajor".`, object.id, "distance"));
3897
+ }
3898
+ if (orbitPlacement.phase && !object.epoch && !system?.epoch) {
3899
+ diagnostics.push(warn("validate.phase.epochMissing", `Object "${object.id}" sets "phase" without an object or system epoch.`, object.id, "phase"));
3900
+ }
3901
+ if (orbitPlacement.inclination && !object.referencePlane && !system?.referencePlane) {
3902
+ diagnostics.push(warn("validate.inclination.referencePlaneMissing", `Object "${object.id}" sets "inclination" without an object or system reference plane.`, object.id, "inclination"));
3903
+ }
3904
+ if (orbitPlacement.period && !massInSolar(parentObject?.properties.mass)) {
3905
+ diagnostics.push(warn("validate.period.massMissing", `Object "${object.id}" sets "period" but its central mass cannot be derived.`, object.id, "period"));
3906
+ }
3907
+ }
3908
+ if (placement?.mode === "surface") {
3909
+ const target = objectMap.get(placement.target);
3910
+ if (!target) {
3911
+ diagnostics.push(error("validate.surface.target.unknown", `Unknown placement target "${placement.target}" on "${object.id}".`, object.id, "surface"));
3912
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
3913
+ diagnostics.push(error("validate.surface.target.invalid", `Surface target "${placement.target}" on "${object.id}" is not surface-capable.`, object.id, "surface"));
3914
+ }
3915
+ }
3916
+ if (placement?.mode === "at") {
3917
+ if (object.type !== "structure" && object.type !== "phenomenon") {
3918
+ diagnostics.push(error("validate.at.objectType", `Only structures and phenomena may use "at" placement; found "${object.type}" on "${object.id}".`, object.id, "at"));
3919
+ }
3920
+ if (!validateAtTarget(object, objectMap, diagnostics)) {
3921
+ diagnostics.push(error("validate.at.target.unknown", `Unknown at-reference target "${placement.target}" on "${object.id}".`, object.id, "at"));
3922
+ }
3923
+ }
3924
+ if (object.resonance) {
3925
+ const target = objectMap.get(object.resonance.targetObjectId);
3926
+ if (!target) {
3927
+ diagnostics.push(error("validate.resonance.target.unknown", `Unknown resonance target "${object.resonance.targetObjectId}" on "${object.id}".`, object.id, "resonance"));
3928
+ } else if (object.placement?.mode !== "orbit" || target.placement?.mode !== "orbit" || object.placement.target !== target.placement.target) {
3929
+ diagnostics.push(warn("validate.resonance.orbitMismatch", `Resonance target "${object.resonance.targetObjectId}" on "${object.id}" does not share a compatible orbital parent.`, object.id, "resonance"));
3930
+ }
3931
+ }
3932
+ for (const rule of object.deriveRules ?? []) {
3933
+ if (rule.field !== "period" || rule.strategy !== "kepler") {
3934
+ diagnostics.push(warn("validate.derive.unsupported", `Unsupported derive rule "${rule.field} ${rule.strategy}" on "${object.id}".`, object.id, "derive"));
3935
+ continue;
3936
+ }
3937
+ const derivedPeriodDays = keplerPeriodDays(object, parentObject);
3938
+ if (derivedPeriodDays === null) {
3939
+ diagnostics.push(warn("validate.derive.inputsMissing", `Object "${object.id}" requests "derive period kepler" but lacks enough input data.`, object.id, "derive"));
3940
+ continue;
3941
+ }
3942
+ if (!orbitPlacement?.period) {
3943
+ diagnostics.push(info("validate.derive.period.available", `Object "${object.id}" can derive a Kepler period of ${formatDays(derivedPeriodDays)}.`, object.id, "derive"));
3944
+ }
3945
+ }
3946
+ for (const rule of object.validationRules ?? []) {
3947
+ if (rule.rule !== "kepler") {
3948
+ diagnostics.push(warn("validate.rule.unsupported", `Unsupported validation rule "${rule.rule}" on "${object.id}".`, object.id, "validate"));
3949
+ continue;
3950
+ }
3951
+ const actualPeriodDays = durationInDays(orbitPlacement?.period);
3952
+ const derivedPeriodDays = keplerPeriodDays(object, parentObject);
3953
+ if (actualPeriodDays === null || derivedPeriodDays === null) {
3954
+ continue;
3955
+ }
3956
+ const toleranceDays = toleranceForField(object, "period");
3957
+ if (Math.abs(actualPeriodDays - derivedPeriodDays) > toleranceDays) {
3958
+ diagnostics.push(error("validate.kepler.mismatch", `Object "${object.id}" fails Kepler validation for "period".`, object.id, "validate"));
3959
+ }
3960
+ }
3961
+ }
3962
+ function validateEvent(event, objectMap, diagnostics) {
3963
+ const fieldPrefix = `event.${event.id}`;
3964
+ const referencedIds = /* @__PURE__ */ new Set();
3965
+ if (!event.kind.trim()) {
3966
+ diagnostics.push(error("validate.event.kind.required", `Event "${event.id}" is missing a "kind" value.`, void 0, `${fieldPrefix}.kind`));
3967
+ }
3968
+ if (!event.targetObjectId && event.participantObjectIds.length === 0) {
3969
+ diagnostics.push(error("validate.event.references.required", `Event "${event.id}" must define a "target" or at least one participant.`, void 0, `${fieldPrefix}.participants`));
3970
+ }
3971
+ if (event.targetObjectId) {
3972
+ referencedIds.add(event.targetObjectId);
3973
+ if (!objectMap.has(event.targetObjectId)) {
3974
+ diagnostics.push(error("validate.event.target.unknown", `Unknown event target "${event.targetObjectId}" on "${event.id}".`, void 0, `${fieldPrefix}.target`));
3975
+ }
3976
+ }
3977
+ const seenParticipants = /* @__PURE__ */ new Set();
3978
+ for (const participantId of event.participantObjectIds) {
3979
+ referencedIds.add(participantId);
3980
+ if (seenParticipants.has(participantId)) {
3981
+ diagnostics.push(warn("validate.event.participants.duplicate", `Event "${event.id}" repeats participant "${participantId}".`, void 0, `${fieldPrefix}.participants`));
3982
+ continue;
3983
+ }
3984
+ seenParticipants.add(participantId);
3985
+ if (!objectMap.has(participantId)) {
3986
+ diagnostics.push(error("validate.event.participants.unknown", `Unknown event participant "${participantId}" on "${event.id}".`, void 0, `${fieldPrefix}.participants`));
3987
+ }
3988
+ }
3989
+ if (event.targetObjectId && event.participantObjectIds.length > 0 && !event.participantObjectIds.includes(event.targetObjectId)) {
3990
+ diagnostics.push(warn("validate.event.target.notParticipant", `Event "${event.id}" defines a target outside its participants list.`, void 0, `${fieldPrefix}.target`));
3991
+ }
3992
+ if (event.positions.length === 0) {
3993
+ diagnostics.push(warn("validate.event.positions.missing", `Event "${event.id}" has no positions block and cannot drive a scene snapshot.`, void 0, `${fieldPrefix}.positions`));
3994
+ }
3995
+ if (/(?:^|[-_])(solar-eclipse|lunar-eclipse|transit|occultation)(?:$|[-_])/.test(event.kind) && referencedIds.size < 3) {
3996
+ diagnostics.push(warn("validate.event.kind.participants", `Event "${event.id}" looks like an eclipse or transit but references fewer than three bodies.`, void 0, `${fieldPrefix}.participants`));
3997
+ }
3998
+ const poseIds = /* @__PURE__ */ new Set();
3999
+ for (const pose of event.positions) {
4000
+ const poseFieldPrefix = `${fieldPrefix}.pose.${pose.objectId}`;
4001
+ if (poseIds.has(pose.objectId)) {
4002
+ diagnostics.push(error("validate.event.pose.duplicate", `Event "${event.id}" defines "${pose.objectId}" more than once in positions.`, void 0, poseFieldPrefix));
4003
+ continue;
4004
+ }
4005
+ poseIds.add(pose.objectId);
4006
+ const object = objectMap.get(pose.objectId);
4007
+ if (!object) {
4008
+ diagnostics.push(error("validate.event.pose.object.unknown", `Unknown event pose object "${pose.objectId}" on "${event.id}".`, void 0, poseFieldPrefix));
4009
+ continue;
4010
+ }
4011
+ if (!referencedIds.has(pose.objectId)) {
4012
+ diagnostics.push(warn("validate.event.pose.unreferenced", `Event pose "${pose.objectId}" on "${event.id}" is not listed in target/participants.`, void 0, poseFieldPrefix));
4013
+ }
4014
+ validateEventPose(pose, object, objectMap, diagnostics, poseFieldPrefix, event.id);
4015
+ }
4016
+ }
4017
+ function validateEventPose(pose, object, objectMap, diagnostics, fieldPrefix, eventId) {
4018
+ const placement = pose.placement;
4019
+ if (!placement) {
4020
+ diagnostics.push(error("validate.event.pose.placement.required", `Event "${eventId}" pose "${pose.objectId}" is missing a placement mode.`, void 0, fieldPrefix));
4021
+ return;
4022
+ }
4023
+ if (placement.mode === "orbit") {
4024
+ if (!objectMap.has(placement.target)) {
4025
+ diagnostics.push(error("validate.event.pose.orbit.target.unknown", `Unknown event orbit target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.orbit`));
4026
+ }
4027
+ if (placement.distance && placement.semiMajor) {
4028
+ diagnostics.push(error("validate.event.pose.orbit.distanceConflict", `Event "${eventId}" pose "${pose.objectId}" cannot declare both "distance" and "semiMajor".`, void 0, `${fieldPrefix}.distance`));
4029
+ }
4030
+ return;
4031
+ }
4032
+ if (placement.mode === "surface") {
4033
+ const target = objectMap.get(placement.target);
4034
+ if (!target) {
4035
+ diagnostics.push(error("validate.event.pose.surface.target.unknown", `Unknown event surface target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.surface`));
4036
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
4037
+ diagnostics.push(error("validate.event.pose.surface.target.invalid", `Event surface target "${placement.target}" on "${eventId}:${pose.objectId}" is not surface-capable.`, void 0, `${fieldPrefix}.surface`));
4038
+ }
4039
+ return;
4040
+ }
4041
+ if (placement.mode === "at") {
4042
+ if (object.type !== "structure" && object.type !== "phenomenon") {
4043
+ diagnostics.push(error("validate.event.pose.at.objectType", `Only structures and phenomena may use "at" placement in events; found "${object.type}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
4044
+ }
4045
+ const reference = placement.reference;
4046
+ if (reference.kind === "named" && !objectMap.has(reference.name)) {
4047
+ diagnostics.push(error("validate.event.pose.at.target.unknown", `Unknown event at-reference target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
4048
+ } else if (reference.kind === "anchor" && !objectMap.has(reference.objectId)) {
4049
+ diagnostics.push(error("validate.event.pose.anchor.target.unknown", `Unknown event anchor target "${reference.objectId}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
4050
+ } else if (reference.kind === "lagrange") {
4051
+ if (!objectMap.has(reference.primary)) {
4052
+ diagnostics.push(error("validate.event.pose.lagrange.primary.unknown", `Unknown event Lagrange target "${reference.primary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
4053
+ } else if (reference.secondary && !objectMap.has(reference.secondary)) {
4054
+ diagnostics.push(error("validate.event.pose.lagrange.secondary.unknown", `Unknown event Lagrange target "${reference.secondary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
4055
+ }
4056
+ }
4057
+ }
4058
+ }
4059
+ function validateAtTarget(object, objectMap, diagnostics) {
4060
+ const reference = object.placement?.mode === "at" ? object.placement.reference : null;
4061
+ if (!reference) {
4062
+ return true;
4063
+ }
4064
+ if (reference.kind === "named") {
4065
+ return objectMap.has(reference.name);
4066
+ }
4067
+ if (reference.kind === "anchor") {
4068
+ if (!objectMap.has(reference.objectId)) {
4069
+ diagnostics.push(error("validate.anchor.target.unknown", `Unknown anchor target "${reference.objectId}" on "${object.id}".`, object.id, "at"));
4070
+ return false;
4071
+ }
4072
+ return true;
4073
+ }
4074
+ if (!objectMap.has(reference.primary)) {
4075
+ diagnostics.push(error("validate.lagrange.primary.unknown", `Unknown Lagrange reference "${reference.primary}" on "${object.id}".`, object.id, "at"));
4076
+ return false;
4077
+ }
4078
+ if (reference.secondary && !objectMap.has(reference.secondary)) {
4079
+ diagnostics.push(error("validate.lagrange.secondary.unknown", `Unknown Lagrange reference "${reference.secondary}" on "${object.id}".`, object.id, "at"));
4080
+ return false;
4081
+ }
4082
+ return true;
4083
+ }
4084
+ function keplerPeriodDays(object, parentObject) {
4085
+ const placement = object.placement;
4086
+ if (!placement || placement.mode !== "orbit") {
4087
+ return null;
4088
+ }
4089
+ const semiMajorAu = distanceInAu(placement.semiMajor ?? placement.distance);
4090
+ const centralMassSolar = massInSolar(parentObject?.properties.mass);
4091
+ if (semiMajorAu === null || centralMassSolar === null || centralMassSolar <= 0) {
4092
+ return null;
4093
+ }
4094
+ const periodYears = Math.sqrt(semiMajorAu ** 3 / centralMassSolar);
4095
+ return periodYears * 365.25;
4096
+ }
4097
+ function distanceInAu(value) {
4098
+ if (!value)
4099
+ return null;
4100
+ switch (value.unit) {
4101
+ case null:
4102
+ case "au":
4103
+ return value.value;
4104
+ case "km":
4105
+ return value.value / AU_IN_KM2;
4106
+ case "m":
4107
+ return value.value / (AU_IN_KM2 * 1e3);
4108
+ case "ly":
4109
+ return value.value * LY_IN_AU2;
4110
+ case "pc":
4111
+ return value.value * PC_IN_AU2;
4112
+ case "kpc":
4113
+ return value.value * KPC_IN_AU2;
4114
+ case "re":
4115
+ return value.value * EARTH_RADIUS_IN_KM2 / AU_IN_KM2;
4116
+ case "sol":
4117
+ return value.value * SOLAR_RADIUS_IN_KM2 / AU_IN_KM2;
4118
+ default:
4119
+ return null;
4120
+ }
4121
+ }
4122
+ function massInSolar(value) {
4123
+ if (!value || typeof value !== "object" || !("value" in value)) {
4124
+ return null;
4125
+ }
4126
+ const unitValue = value;
4127
+ switch (unitValue.unit) {
4128
+ case null:
4129
+ case "sol":
4130
+ return unitValue.value;
4131
+ case "me":
4132
+ return unitValue.value / EARTH_MASSES_PER_SOLAR;
4133
+ case "mj":
4134
+ return unitValue.value / JUPITER_MASSES_PER_SOLAR;
4135
+ default:
4136
+ return null;
4137
+ }
4138
+ }
4139
+ function durationInDays(value) {
4140
+ if (!value)
4141
+ return null;
4142
+ switch (value.unit) {
4143
+ case null:
4144
+ case "d":
4145
+ return value.value;
4146
+ case "s":
4147
+ return value.value / 86400;
4148
+ case "min":
4149
+ return value.value / 1440;
4150
+ case "h":
4151
+ return value.value / 24;
4152
+ case "y":
4153
+ return value.value * 365.25;
4154
+ case "ky":
4155
+ return value.value * 365250;
4156
+ case "my":
4157
+ return value.value * 36525e4;
4158
+ case "gy":
4159
+ return value.value * 36525e7;
4160
+ default:
4161
+ return null;
4162
+ }
4163
+ }
4164
+ function toleranceForField(object, field) {
4165
+ const tolerance = object.tolerances?.find((entry) => entry.field === field)?.value;
4166
+ if (typeof tolerance === "number") {
4167
+ return tolerance;
4168
+ }
4169
+ if (tolerance && typeof tolerance === "object" && "value" in tolerance) {
4170
+ return durationInDays(tolerance) ?? 0;
4171
+ }
4172
+ return 0;
4173
+ }
4174
+ function formatDays(days) {
4175
+ return `${Math.round(days * 100) / 100}d`;
4176
+ }
4177
+ function error(code, message, objectId, field) {
4178
+ return { code, severity: "error", source: "validate", message, objectId, field };
4179
+ }
4180
+ function warn(code, message, objectId, field) {
4181
+ return { code, severity: "warning", source: "validate", message, objectId, field };
4182
+ }
4183
+ function info(code, message, objectId, field) {
4184
+ return { code, severity: "info", source: "validate", message, objectId, field };
4185
+ }
4186
+
4187
+ // packages/core/dist/draft-parse.js
4188
+ var STRUCTURED_TYPED_BLOCKS = /* @__PURE__ */ new Set([
4189
+ "climate",
4190
+ "habitability",
4191
+ "settlement"
4192
+ ]);
4193
+ var DRAFT_OBJECT_FIELD_SPECS = /* @__PURE__ */ new Map();
4194
+ for (const key of [
4195
+ "orbit",
4196
+ "distance",
4197
+ "semiMajor",
4198
+ "eccentricity",
4199
+ "period",
4200
+ "angle",
4201
+ "inclination",
4202
+ "phase",
4203
+ "at",
4204
+ "surface",
4205
+ "free",
4206
+ "kind",
4207
+ "class",
4208
+ "culture",
4209
+ "tags",
4210
+ "color",
4211
+ "image",
4212
+ "hidden",
4213
+ "radius",
4214
+ "mass",
4215
+ "density",
4216
+ "gravity",
4217
+ "temperature",
4218
+ "albedo",
4219
+ "atmosphere",
4220
+ "inner",
4221
+ "outer",
4222
+ "on",
4223
+ "source",
4224
+ "cycle"
4225
+ ]) {
4226
+ const schema = getFieldSchema(key);
4227
+ if (schema) {
4228
+ DRAFT_OBJECT_FIELD_SPECS.set(key, {
4229
+ key,
4230
+ version: "2.0",
4231
+ inlineMode: schema.arity === "multiple" ? "multiple" : "single",
4232
+ allowRepeat: false,
4233
+ legacySchema: schema
4234
+ });
4235
+ }
4236
+ }
4237
+ for (const spec of [
4238
+ { key: "groups", inlineMode: "multiple", allowRepeat: false },
4239
+ { key: "epoch", inlineMode: "single", allowRepeat: false },
4240
+ { key: "referencePlane", inlineMode: "single", allowRepeat: false },
4241
+ { key: "tidalLock", inlineMode: "single", allowRepeat: false },
4242
+ { key: "renderLabel", inlineMode: "single", allowRepeat: false },
4243
+ { key: "renderOrbit", inlineMode: "single", allowRepeat: false },
4244
+ { key: "renderPriority", inlineMode: "single", allowRepeat: false },
4245
+ { key: "resonance", inlineMode: "pair", allowRepeat: false },
4246
+ { key: "derive", inlineMode: "pair", allowRepeat: true },
4247
+ { key: "validate", inlineMode: "single", allowRepeat: true },
4248
+ { key: "locked", inlineMode: "multiple", allowRepeat: false },
4249
+ { key: "tolerance", inlineMode: "pair", allowRepeat: true }
4250
+ ]) {
4251
+ DRAFT_OBJECT_FIELD_SPECS.set(spec.key, {
4252
+ key: spec.key,
4253
+ version: "2.1",
4254
+ inlineMode: spec.inlineMode,
4255
+ allowRepeat: spec.allowRepeat
4256
+ });
4257
+ }
4258
+ var DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
4259
+ var EVENT_POSE_FIELD_KEYS = /* @__PURE__ */ new Set([
4260
+ "orbit",
4261
+ "distance",
4262
+ "semiMajor",
4263
+ "eccentricity",
4264
+ "period",
4265
+ "angle",
4266
+ "inclination",
4267
+ "phase",
4268
+ "at",
4269
+ "surface",
4270
+ "free",
4271
+ "inner",
4272
+ "outer"
4273
+ ]);
4274
+ function parseWorldOrbitAtlas(source) {
4275
+ return parseAtlasSource(source);
4276
+ }
4277
+ function parseWorldOrbitDraft(source) {
4278
+ return parseAtlasSource(source, "2.0-draft");
4279
+ }
4280
+ function parseAtlasSource(source, forcedOutputVersion) {
4281
+ const prepared = preprocessAtlasSource(source);
4282
+ const lines = prepared.source.split(/\r?\n/);
4283
+ const diagnostics = [];
4284
+ let sawSchemaHeader = false;
4285
+ let sourceSchemaVersion = "2.0";
4286
+ let system = null;
4287
+ let section = null;
4288
+ const objectNodes = [];
4289
+ const groups = [];
4290
+ const relations = [];
4291
+ const events = [];
4292
+ const eventPoseNodes = /* @__PURE__ */ new Map();
4293
+ let sawDefaults = false;
4294
+ let sawAtlas = false;
4295
+ const viewpointIds = /* @__PURE__ */ new Set();
4296
+ const annotationIds = /* @__PURE__ */ new Set();
4297
+ const groupIds = /* @__PURE__ */ new Set();
4298
+ const relationIds = /* @__PURE__ */ new Set();
4299
+ const eventIds = /* @__PURE__ */ new Set();
4300
+ for (let index = 0; index < lines.length; index++) {
4301
+ const rawLine = lines[index];
4302
+ const lineNumber = index + 1;
4303
+ if (!rawLine.trim()) {
4304
+ continue;
4305
+ }
4306
+ const indent = getIndent(rawLine);
4307
+ const tokens = tokenizeLineDetailed(rawLine.slice(indent), {
4308
+ line: lineNumber,
4309
+ columnOffset: indent
4310
+ });
4311
+ if (tokens.length === 0) {
4312
+ continue;
4313
+ }
4314
+ if (!sawSchemaHeader) {
4315
+ sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
4316
+ sawSchemaHeader = true;
4317
+ if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
4318
+ diagnostics.push({
4319
+ code: "parse.schema21.commentCompatibility",
4320
+ severity: "warning",
4321
+ source: "parse",
4322
+ message: `Comments require schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
4323
+ line: prepared.comments[0].line,
4324
+ column: prepared.comments[0].column
4325
+ });
4326
+ }
4327
+ continue;
4328
+ }
4329
+ if (indent === 0) {
4330
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, { sawDefaults, sawAtlas });
4331
+ if (section.kind === "system") {
4332
+ system = section.system;
4333
+ } else if (section.kind === "defaults") {
4334
+ sawDefaults = true;
4335
+ } else if (section.kind === "atlas") {
4336
+ sawAtlas = true;
4337
+ }
4338
+ continue;
4339
+ }
4340
+ if (!section) {
4341
+ throw new WorldOrbitError("Indented line without parent atlas section", lineNumber, indent + 1);
4342
+ }
4343
+ handleSectionLine(section, indent, tokens, lineNumber);
4344
+ }
4345
+ if (!sawSchemaHeader) {
4346
+ throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
4347
+ }
4348
+ const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
4349
+ const normalizedEvents = events.map((event) => normalizeDraftEvent(event, eventPoseNodes.get(event.id) ?? []));
4350
+ const outputVersion = forcedOutputVersion ?? (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
4351
+ const baseDocument = {
3168
4352
  format: "worldorbit",
3169
- version: outputVersion,
3170
4353
  sourceVersion: "1.0",
3171
4354
  system,
3172
- objects: normalizedObjects,
4355
+ groups,
4356
+ relations,
4357
+ events: normalizedEvents,
4358
+ objects,
3173
4359
  diagnostics
3174
4360
  };
4361
+ if (outputVersion === "2.0-draft") {
4362
+ const document2 = {
4363
+ ...baseDocument,
4364
+ version: "2.0-draft",
4365
+ schemaVersion: "2.0-draft"
4366
+ };
4367
+ document2.diagnostics.push(...collectAtlasDiagnostics(document2, sourceSchemaVersion));
4368
+ return document2;
4369
+ }
4370
+ const document = {
4371
+ ...baseDocument,
4372
+ version: outputVersion,
4373
+ schemaVersion: outputVersion
4374
+ };
4375
+ if (sourceSchemaVersion === "2.0-draft") {
4376
+ document.diagnostics.push({
4377
+ code: "load.schema.deprecatedDraft",
4378
+ severity: "warning",
4379
+ source: "upgrade",
4380
+ message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".'
4381
+ });
4382
+ }
4383
+ document.diagnostics.push(...collectAtlasDiagnostics(document, sourceSchemaVersion));
4384
+ return document;
3175
4385
  }
3176
4386
  function assertDraftSchemaHeader(tokens, line) {
3177
- if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || tokens[1].value.toLowerCase() !== "2.0-draft" && tokens[1].value.toLowerCase() !== "2.0") {
3178
- throw new WorldOrbitError('Expected atlas header "schema 2.0" or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
4387
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
4388
+ throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
3179
4389
  }
3180
- return tokens[1].value.toLowerCase() === "2.0-draft" ? "2.0-draft" : "2.0";
4390
+ const version = tokens[1].value.toLowerCase();
4391
+ return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
3181
4392
  }
3182
- function startTopLevelSection(tokens, line, system, objectNodes, viewpointIds, annotationIds, flags) {
4393
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, flags) {
3183
4394
  const keyword = tokens[0]?.value.toLowerCase();
3184
4395
  switch (keyword) {
3185
4396
  case "system":
3186
4397
  if (system) {
3187
4398
  throw new WorldOrbitError('Atlas section "system" may only appear once', line, tokens[0].column);
3188
4399
  }
3189
- return startSystemSection(tokens, line);
4400
+ return startSystemSection(tokens, line, sourceSchemaVersion, diagnostics);
3190
4401
  case "defaults":
3191
4402
  if (!system) {
3192
4403
  throw new WorldOrbitError('Atlas section "defaults" requires a preceding system declaration', line, tokens[0].column);
@@ -3216,19 +4427,28 @@ var WorldOrbit = (() => {
3216
4427
  if (!system) {
3217
4428
  throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
3218
4429
  }
3219
- return startViewpointSection(tokens, line, system, viewpointIds);
4430
+ return startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics);
3220
4431
  case "annotation":
3221
4432
  if (!system) {
3222
4433
  throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
3223
4434
  }
3224
4435
  return startAnnotationSection(tokens, line, system, annotationIds);
4436
+ case "group":
4437
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "group", { line, column: tokens[0].column });
4438
+ return startGroupSection(tokens, line, groups, groupIds);
4439
+ case "relation":
4440
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
4441
+ return startRelationSection(tokens, line, relations, relationIds);
4442
+ case "event":
4443
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "event", { line, column: tokens[0].column });
4444
+ return startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics);
3225
4445
  case "object":
3226
- return startObjectSection(tokens, line, objectNodes);
4446
+ return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
3227
4447
  default:
3228
4448
  throw new WorldOrbitError(`Unknown atlas section "${tokens[0]?.value ?? ""}"`, line, tokens[0]?.column ?? 1);
3229
4449
  }
3230
4450
  }
3231
- function startSystemSection(tokens, line) {
4451
+ function startSystemSection(tokens, line, sourceSchemaVersion, diagnostics) {
3232
4452
  if (tokens.length !== 2) {
3233
4453
  throw new WorldOrbitError("Invalid atlas system declaration", line, tokens[0]?.column ?? 1);
3234
4454
  }
@@ -3236,6 +4456,9 @@ var WorldOrbit = (() => {
3236
4456
  type: "system",
3237
4457
  id: tokens[1].value,
3238
4458
  title: null,
4459
+ description: null,
4460
+ epoch: null,
4461
+ referencePlane: null,
3239
4462
  defaults: {
3240
4463
  view: "topdown",
3241
4464
  scale: null,
@@ -3250,10 +4473,12 @@ var WorldOrbit = (() => {
3250
4473
  return {
3251
4474
  kind: "system",
3252
4475
  system,
4476
+ sourceSchemaVersion,
4477
+ diagnostics,
3253
4478
  seenFields: /* @__PURE__ */ new Set()
3254
4479
  };
3255
4480
  }
3256
- function startViewpointSection(tokens, line, system, viewpointIds) {
4481
+ function startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics) {
3257
4482
  if (tokens.length !== 2) {
3258
4483
  throw new WorldOrbitError("Invalid viewpoint declaration", line, tokens[0]?.column ?? 1);
3259
4484
  }
@@ -3270,6 +4495,7 @@ var WorldOrbit = (() => {
3270
4495
  summary: "",
3271
4496
  focusObjectId: null,
3272
4497
  selectedObjectId: null,
4498
+ events: [],
3273
4499
  projection: system.defaults.view,
3274
4500
  preset: system.defaults.preset,
3275
4501
  zoom: null,
@@ -3282,6 +4508,8 @@ var WorldOrbit = (() => {
3282
4508
  return {
3283
4509
  kind: "viewpoint",
3284
4510
  viewpoint,
4511
+ sourceSchemaVersion,
4512
+ diagnostics,
3285
4513
  seenFields: /* @__PURE__ */ new Set(),
3286
4514
  inFilter: false,
3287
4515
  filterIndent: null,
@@ -3315,7 +4543,107 @@ var WorldOrbit = (() => {
3315
4543
  seenFields: /* @__PURE__ */ new Set()
3316
4544
  };
3317
4545
  }
3318
- function startObjectSection(tokens, line, objectNodes) {
4546
+ function startGroupSection(tokens, line, groups, groupIds) {
4547
+ if (tokens.length !== 2) {
4548
+ throw new WorldOrbitError("Invalid group declaration", line, tokens[0]?.column ?? 1);
4549
+ }
4550
+ const id = normalizeIdentifier2(tokens[1].value);
4551
+ if (!id) {
4552
+ throw new WorldOrbitError("Group id must not be empty", line, tokens[1].column);
4553
+ }
4554
+ if (groupIds.has(id)) {
4555
+ throw new WorldOrbitError(`Duplicate group id "${id}"`, line, tokens[1].column);
4556
+ }
4557
+ const group = {
4558
+ id,
4559
+ label: humanizeIdentifier3(id),
4560
+ summary: "",
4561
+ color: null,
4562
+ tags: [],
4563
+ hidden: false
4564
+ };
4565
+ groups.push(group);
4566
+ groupIds.add(id);
4567
+ return {
4568
+ kind: "group",
4569
+ group,
4570
+ seenFields: /* @__PURE__ */ new Set()
4571
+ };
4572
+ }
4573
+ function startRelationSection(tokens, line, relations, relationIds) {
4574
+ if (tokens.length !== 2) {
4575
+ throw new WorldOrbitError("Invalid relation declaration", line, tokens[0]?.column ?? 1);
4576
+ }
4577
+ const id = normalizeIdentifier2(tokens[1].value);
4578
+ if (!id) {
4579
+ throw new WorldOrbitError("Relation id must not be empty", line, tokens[1].column);
4580
+ }
4581
+ if (relationIds.has(id)) {
4582
+ throw new WorldOrbitError(`Duplicate relation id "${id}"`, line, tokens[1].column);
4583
+ }
4584
+ const relation = {
4585
+ id,
4586
+ from: "",
4587
+ to: "",
4588
+ kind: "",
4589
+ label: null,
4590
+ summary: null,
4591
+ tags: [],
4592
+ color: null,
4593
+ hidden: false
4594
+ };
4595
+ relations.push(relation);
4596
+ relationIds.add(id);
4597
+ return {
4598
+ kind: "relation",
4599
+ relation,
4600
+ seenFields: /* @__PURE__ */ new Set()
4601
+ };
4602
+ }
4603
+ function startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics) {
4604
+ if (tokens.length !== 2) {
4605
+ throw new WorldOrbitError("Invalid event declaration", line, tokens[0]?.column ?? 1);
4606
+ }
4607
+ const id = normalizeIdentifier2(tokens[1].value);
4608
+ if (!id) {
4609
+ throw new WorldOrbitError("Event id must not be empty", line, tokens[1].column);
4610
+ }
4611
+ if (eventIds.has(id)) {
4612
+ throw new WorldOrbitError(`Duplicate event id "${id}"`, line, tokens[1].column);
4613
+ }
4614
+ const event = {
4615
+ id,
4616
+ kind: "",
4617
+ label: humanizeIdentifier3(id),
4618
+ summary: null,
4619
+ targetObjectId: null,
4620
+ participantObjectIds: [],
4621
+ timing: null,
4622
+ visibility: null,
4623
+ tags: [],
4624
+ color: null,
4625
+ hidden: false,
4626
+ positions: []
4627
+ };
4628
+ const rawPoses = [];
4629
+ events.push(event);
4630
+ eventPoseNodes.set(id, rawPoses);
4631
+ eventIds.add(id);
4632
+ return {
4633
+ kind: "event",
4634
+ event,
4635
+ sourceSchemaVersion,
4636
+ diagnostics,
4637
+ seenFields: /* @__PURE__ */ new Set(),
4638
+ rawPoses,
4639
+ inPositions: false,
4640
+ positionsIndent: null,
4641
+ activePose: null,
4642
+ poseIndent: null,
4643
+ activePoseSeenFields: /* @__PURE__ */ new Set()
4644
+ };
4645
+ }
4646
+ function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
3319
4647
  if (tokens.length < 3) {
3320
4648
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
3321
4649
  }
@@ -3326,12 +4654,11 @@ var WorldOrbit = (() => {
3326
4654
  throw new WorldOrbitError(`Unknown object type "${objectTypeToken.value}"`, line, objectTypeToken.column);
3327
4655
  }
3328
4656
  const objectNode = {
3329
- type: "object",
3330
4657
  objectType,
3331
- name: idToken.value,
3332
- inlineFields: parseInlineFields2(tokens.slice(3), line),
3333
- blockFields: [],
4658
+ id: idToken.value,
4659
+ fields: parseInlineObjectFields(tokens.slice(3), line, objectType, sourceSchemaVersion, diagnostics),
3334
4660
  infoEntries: [],
4661
+ typedBlockEntries: {},
3335
4662
  location: {
3336
4663
  line,
3337
4664
  column: objectTypeToken.column
@@ -3341,8 +4668,12 @@ var WorldOrbit = (() => {
3341
4668
  return {
3342
4669
  kind: "object",
3343
4670
  objectNode,
3344
- inInfoBlock: false,
3345
- infoIndent: null
4671
+ sourceSchemaVersion,
4672
+ diagnostics,
4673
+ activeBlock: null,
4674
+ blockIndent: null,
4675
+ seenInfoKeys: /* @__PURE__ */ new Set(),
4676
+ seenTypedBlockKeys: {}
3346
4677
  };
3347
4678
  }
3348
4679
  function handleSectionLine(section, indent, tokens, line) {
@@ -3362,6 +4693,15 @@ var WorldOrbit = (() => {
3362
4693
  case "annotation":
3363
4694
  applyAnnotationField(section, tokens, line);
3364
4695
  return;
4696
+ case "group":
4697
+ applyGroupField(section, tokens, line);
4698
+ return;
4699
+ case "relation":
4700
+ applyRelationField(section, tokens, line);
4701
+ return;
4702
+ case "event":
4703
+ applyEventField(section, indent, tokens, line);
4704
+ return;
3365
4705
  case "object":
3366
4706
  applyObjectField(section, indent, tokens, line);
3367
4707
  return;
@@ -3369,10 +4709,35 @@ var WorldOrbit = (() => {
3369
4709
  }
3370
4710
  function applySystemField(section, tokens, line) {
3371
4711
  const key = requireUniqueField(tokens, section.seenFields, line);
3372
- if (key !== "title") {
3373
- throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
4712
+ const value = joinFieldValue(tokens, line);
4713
+ switch (key) {
4714
+ case "title":
4715
+ section.system.title = value;
4716
+ return;
4717
+ case "description":
4718
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
4719
+ line,
4720
+ column: tokens[0].column
4721
+ });
4722
+ section.system.description = value;
4723
+ return;
4724
+ case "epoch":
4725
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
4726
+ line,
4727
+ column: tokens[0].column
4728
+ });
4729
+ section.system.epoch = value;
4730
+ return;
4731
+ case "referenceplane":
4732
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "referencePlane", {
4733
+ line,
4734
+ column: tokens[0].column
4735
+ });
4736
+ section.system.referencePlane = value;
4737
+ return;
4738
+ default:
4739
+ throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
3374
4740
  }
3375
- section.system.title = joinFieldValue(tokens, line);
3376
4741
  }
3377
4742
  function applyDefaultsField(section, tokens, line) {
3378
4743
  const key = requireUniqueField(tokens, section.seenFields, line);
@@ -3403,14 +4768,11 @@ var WorldOrbit = (() => {
3403
4768
  section.metadataIndent = null;
3404
4769
  }
3405
4770
  if (section.inMetadata) {
3406
- if (tokens.length < 2) {
3407
- throw new WorldOrbitError("Invalid atlas metadata entry", line, tokens[0]?.column ?? 1);
4771
+ const entry = parseInfoLikeEntry(tokens, line, "Invalid atlas metadata entry");
4772
+ if (entry.key in section.system.atlasMetadata) {
4773
+ throw new WorldOrbitError(`Duplicate atlas metadata key "${entry.key}"`, line, tokens[0].column);
3408
4774
  }
3409
- const key = tokens[0].value;
3410
- if (key in section.system.atlasMetadata) {
3411
- throw new WorldOrbitError(`Duplicate atlas metadata key "${key}"`, line, tokens[0].column);
3412
- }
3413
- section.system.atlasMetadata[key] = joinFieldValue(tokens, line);
4775
+ section.system.atlasMetadata[entry.key] = entry.value;
3414
4776
  return;
3415
4777
  }
3416
4778
  if (tokens.length === 1 && tokens[0].value.toLowerCase() === "metadata") {
@@ -3466,7 +4828,14 @@ var WorldOrbit = (() => {
3466
4828
  section.viewpoint.rotationDeg = parseFiniteNumber2(value, line, tokens[0].column, "rotation");
3467
4829
  return;
3468
4830
  case "layers":
3469
- section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line);
4831
+ section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line, section.sourceSchemaVersion, section.diagnostics);
4832
+ return;
4833
+ case "events":
4834
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.events", {
4835
+ line,
4836
+ column: tokens[0].column
4837
+ });
4838
+ section.viewpoint.events = parseTokenList(tokens.slice(1), line, "events");
3470
4839
  return;
3471
4840
  default:
3472
4841
  throw new WorldOrbitError(`Unknown viewpoint field "${tokens[0].value}"`, line, tokens[0].column);
@@ -3512,21 +4881,202 @@ var WorldOrbit = (() => {
3512
4881
  throw new WorldOrbitError(`Unknown annotation field "${tokens[0].value}"`, line, tokens[0].column);
3513
4882
  }
3514
4883
  }
3515
- function applyObjectField(section, indent, tokens, line) {
3516
- if (tokens.length === 1 && tokens[0].value === "info") {
3517
- section.inInfoBlock = true;
3518
- section.infoIndent = indent;
4884
+ function applyGroupField(section, tokens, line) {
4885
+ const key = requireUniqueField(tokens, section.seenFields, line);
4886
+ switch (key) {
4887
+ case "label":
4888
+ section.group.label = joinFieldValue(tokens, line);
4889
+ return;
4890
+ case "summary":
4891
+ section.group.summary = joinFieldValue(tokens, line);
4892
+ return;
4893
+ case "color":
4894
+ section.group.color = joinFieldValue(tokens, line);
4895
+ return;
4896
+ case "tags":
4897
+ section.group.tags = parseTokenList(tokens.slice(1), line, "tags");
4898
+ return;
4899
+ case "hidden":
4900
+ section.group.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4901
+ line,
4902
+ column: tokens[0].column
4903
+ });
4904
+ return;
4905
+ default:
4906
+ throw new WorldOrbitError(`Unknown group field "${tokens[0].value}"`, line, tokens[0].column);
4907
+ }
4908
+ }
4909
+ function applyRelationField(section, tokens, line) {
4910
+ const key = requireUniqueField(tokens, section.seenFields, line);
4911
+ switch (key) {
4912
+ case "from":
4913
+ section.relation.from = joinFieldValue(tokens, line);
4914
+ return;
4915
+ case "to":
4916
+ section.relation.to = joinFieldValue(tokens, line);
4917
+ return;
4918
+ case "kind":
4919
+ section.relation.kind = joinFieldValue(tokens, line);
4920
+ return;
4921
+ case "label":
4922
+ section.relation.label = joinFieldValue(tokens, line);
4923
+ return;
4924
+ case "summary":
4925
+ section.relation.summary = joinFieldValue(tokens, line);
4926
+ return;
4927
+ case "tags":
4928
+ section.relation.tags = parseTokenList(tokens.slice(1), line, "tags");
4929
+ return;
4930
+ case "color":
4931
+ section.relation.color = joinFieldValue(tokens, line);
4932
+ return;
4933
+ case "hidden":
4934
+ section.relation.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4935
+ line,
4936
+ column: tokens[0].column
4937
+ });
4938
+ return;
4939
+ default:
4940
+ throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
4941
+ }
4942
+ }
4943
+ function applyEventField(section, indent, tokens, line) {
4944
+ if (section.activePose && indent <= (section.poseIndent ?? 0)) {
4945
+ section.activePose = null;
4946
+ section.poseIndent = null;
4947
+ section.activePoseSeenFields.clear();
4948
+ }
4949
+ if (!section.activePose && section.inPositions && indent <= (section.positionsIndent ?? 0)) {
4950
+ section.inPositions = false;
4951
+ section.positionsIndent = null;
4952
+ }
4953
+ if (section.activePose) {
4954
+ section.activePose.fields.push(parseEventPoseField(tokens, line, section.activePoseSeenFields));
4955
+ return;
4956
+ }
4957
+ if (section.inPositions) {
4958
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "pose") {
4959
+ throw new WorldOrbitError(`Unknown event positions field "${tokens[0].value}"`, line, tokens[0]?.column ?? 1);
4960
+ }
4961
+ const objectId = tokens[1].value;
4962
+ if (!objectId.trim()) {
4963
+ throw new WorldOrbitError("Event pose object id must not be empty", line, tokens[1].column);
4964
+ }
4965
+ const rawPose = {
4966
+ objectId,
4967
+ fields: [],
4968
+ location: { line, column: tokens[0].column }
4969
+ };
4970
+ section.rawPoses.push(rawPose);
4971
+ section.activePose = rawPose;
4972
+ section.poseIndent = indent;
4973
+ section.activePoseSeenFields = /* @__PURE__ */ new Set();
3519
4974
  return;
3520
4975
  }
3521
- if (section.inInfoBlock && indent <= (section.infoIndent ?? 0)) {
3522
- section.inInfoBlock = false;
3523
- section.infoIndent = null;
4976
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "positions") {
4977
+ if (section.seenFields.has("positions")) {
4978
+ throw new WorldOrbitError('Duplicate event field "positions"', line, tokens[0].column);
4979
+ }
4980
+ section.seenFields.add("positions");
4981
+ section.inPositions = true;
4982
+ section.positionsIndent = indent;
4983
+ return;
4984
+ }
4985
+ const key = requireUniqueField(tokens, section.seenFields, line);
4986
+ switch (key) {
4987
+ case "kind":
4988
+ section.event.kind = joinFieldValue(tokens, line);
4989
+ return;
4990
+ case "label":
4991
+ section.event.label = joinFieldValue(tokens, line);
4992
+ return;
4993
+ case "summary":
4994
+ section.event.summary = joinFieldValue(tokens, line);
4995
+ return;
4996
+ case "target":
4997
+ section.event.targetObjectId = joinFieldValue(tokens, line);
4998
+ return;
4999
+ case "participants":
5000
+ section.event.participantObjectIds = parseTokenList(tokens.slice(1), line, "participants");
5001
+ return;
5002
+ case "timing":
5003
+ section.event.timing = joinFieldValue(tokens, line);
5004
+ return;
5005
+ case "visibility":
5006
+ section.event.visibility = joinFieldValue(tokens, line);
5007
+ return;
5008
+ case "tags":
5009
+ section.event.tags = parseTokenList(tokens.slice(1), line, "tags");
5010
+ return;
5011
+ case "color":
5012
+ section.event.color = joinFieldValue(tokens, line);
5013
+ return;
5014
+ case "hidden":
5015
+ section.event.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
5016
+ line,
5017
+ column: tokens[0].column
5018
+ });
5019
+ return;
5020
+ default:
5021
+ throw new WorldOrbitError(`Unknown event field "${tokens[0].value}"`, line, tokens[0].column);
5022
+ }
5023
+ }
5024
+ function parseEventPoseField(tokens, line, seenFields) {
5025
+ if (tokens.length < 2) {
5026
+ throw new WorldOrbitError("Invalid event pose field line", line, tokens[0]?.column ?? 1);
5027
+ }
5028
+ const key = tokens[0].value;
5029
+ if (!EVENT_POSE_FIELD_KEYS.has(key)) {
5030
+ throw new WorldOrbitError(`Unknown event pose field "${key}"`, line, tokens[0].column);
5031
+ }
5032
+ if (seenFields.has(key)) {
5033
+ throw new WorldOrbitError(`Duplicate event pose field "${key}"`, line, tokens[0].column);
5034
+ }
5035
+ seenFields.add(key);
5036
+ return {
5037
+ type: "field",
5038
+ key,
5039
+ values: tokens.slice(1).map((token) => token.value),
5040
+ location: { line, column: tokens[0].column }
5041
+ };
5042
+ }
5043
+ function applyObjectField(section, indent, tokens, line) {
5044
+ if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
5045
+ section.activeBlock = null;
5046
+ section.blockIndent = null;
5047
+ }
5048
+ if (tokens.length === 1) {
5049
+ const blockName = tokens[0].value.toLowerCase();
5050
+ if (blockName === "info" || STRUCTURED_TYPED_BLOCKS.has(blockName)) {
5051
+ if (blockName !== "info") {
5052
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, blockName, { line, column: tokens[0].column });
5053
+ }
5054
+ section.activeBlock = blockName;
5055
+ section.blockIndent = indent;
5056
+ return;
5057
+ }
3524
5058
  }
3525
- if (section.inInfoBlock) {
3526
- section.objectNode.infoEntries.push(parseInfoEntry2(tokens, line));
5059
+ if (section.activeBlock) {
5060
+ const entry = parseInfoLikeEntry(tokens, line, `Invalid ${section.activeBlock} entry`);
5061
+ if (section.activeBlock === "info") {
5062
+ if (section.seenInfoKeys.has(entry.key)) {
5063
+ throw new WorldOrbitError(`Duplicate info key "${entry.key}"`, line, tokens[0].column);
5064
+ }
5065
+ section.seenInfoKeys.add(entry.key);
5066
+ section.objectNode.infoEntries.push(entry);
5067
+ return;
5068
+ }
5069
+ const typedBlock = section.activeBlock;
5070
+ const seenKeys = section.seenTypedBlockKeys[typedBlock] ?? (section.seenTypedBlockKeys[typedBlock] = /* @__PURE__ */ new Set());
5071
+ if (seenKeys.has(entry.key)) {
5072
+ throw new WorldOrbitError(`Duplicate ${typedBlock} key "${entry.key}"`, line, tokens[0].column);
5073
+ }
5074
+ seenKeys.add(entry.key);
5075
+ const entries = section.objectNode.typedBlockEntries[typedBlock] ?? (section.objectNode.typedBlockEntries[typedBlock] = []);
5076
+ entries.push(entry);
3527
5077
  return;
3528
5078
  }
3529
- section.objectNode.blockFields.push(parseField2(tokens, line));
5079
+ section.objectNode.fields.push(parseObjectField(tokens, line, section.objectNode.objectType, section.sourceSchemaVersion, section.diagnostics));
3530
5080
  }
3531
5081
  function requireUniqueField(tokens, seenFields, line) {
3532
5082
  if (tokens.length < 2) {
@@ -3546,50 +5096,46 @@ var WorldOrbit = (() => {
3546
5096
  return tokens.slice(1).map((token) => token.value).join(" ").trim();
3547
5097
  }
3548
5098
  function parseObjectTypeTokens(tokens, line) {
3549
- if (tokens.length === 0) {
3550
- throw new WorldOrbitError("Missing value for atlas field", line);
3551
- }
3552
- return tokens.map((token) => {
3553
- const value = token.value;
3554
- if (value !== "star" && value !== "planet" && value !== "moon" && value !== "belt" && value !== "asteroid" && value !== "comet" && value !== "ring" && value !== "structure" && value !== "phenomenon") {
3555
- throw new WorldOrbitError(`Unknown viewpoint object type "${token.value}"`, line, token.column);
3556
- }
3557
- return value;
3558
- });
5099
+ 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");
3559
5100
  }
3560
- function parseTokenList(tokens, line, field) {
3561
- if (tokens.length === 0) {
3562
- throw new WorldOrbitError(`Missing value for field "${field}"`, line);
5101
+ function parseLayerTokens(tokens, line, sourceSchemaVersion, diagnostics) {
5102
+ const layers = {};
5103
+ for (const token of parseTokenList(tokens, line, "layers")) {
5104
+ const enabled = !token.startsWith("-") && !token.startsWith("!");
5105
+ const raw = token.replace(/^[-!]+/, "").toLowerCase();
5106
+ if (raw === "orbits") {
5107
+ layers["orbits-back"] = enabled;
5108
+ layers["orbits-front"] = enabled;
5109
+ continue;
5110
+ }
5111
+ if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "events" || raw === "objects" || raw === "labels" || raw === "metadata") {
5112
+ if (raw === "events" && sourceSchemaVersion && diagnostics) {
5113
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "layers.events", {
5114
+ line,
5115
+ column: tokens[0]?.column ?? 1
5116
+ });
5117
+ }
5118
+ layers[raw] = enabled;
5119
+ }
3563
5120
  }
3564
- return tokens.map((token) => token.value);
5121
+ return layers;
3565
5122
  }
3566
- function parseLayerTokens(tokens, line) {
5123
+ function parseTokenList(tokens, line, fieldName) {
3567
5124
  if (tokens.length === 0) {
3568
- throw new WorldOrbitError('Missing value for field "layers"', line);
5125
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, 1);
3569
5126
  }
3570
- const next = {};
3571
- for (const token of tokens) {
3572
- const enabled = !token.value.startsWith("-") && !token.value.startsWith("!");
3573
- const rawLayer = token.value.replace(/^[-!]+/, "").toLowerCase();
3574
- if (rawLayer === "orbits") {
3575
- next["orbits-back"] = enabled;
3576
- next["orbits-front"] = enabled;
3577
- continue;
3578
- }
3579
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
3580
- next[rawLayer] = enabled;
3581
- continue;
3582
- }
3583
- throw new WorldOrbitError(`Unknown layer token "${token.value}"`, line, token.column);
5127
+ const values = tokens.map((token) => token.value).filter(Boolean);
5128
+ if (values.length === 0) {
5129
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, tokens[0]?.column ?? 1);
3584
5130
  }
3585
- return next;
5131
+ return values;
3586
5132
  }
3587
5133
  function parseProjectionValue(value, line, column) {
3588
5134
  const normalized = value.toLowerCase();
3589
- if (normalized === "topdown" || normalized === "isometric") {
3590
- return normalized;
5135
+ if (normalized !== "topdown" && normalized !== "isometric") {
5136
+ throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
3591
5137
  }
3592
- throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
5138
+ return normalized;
3593
5139
  }
3594
5140
  function parsePresetValue(value, line, column) {
3595
5141
  const normalized = value.toLowerCase();
@@ -3599,16 +5145,16 @@ var WorldOrbit = (() => {
3599
5145
  throw new WorldOrbitError(`Unknown render preset "${value}"`, line, column);
3600
5146
  }
3601
5147
  function parsePositiveNumber2(value, line, column, field) {
3602
- const parsed = Number(value);
3603
- if (!Number.isFinite(parsed) || parsed <= 0) {
3604
- throw new WorldOrbitError(`Field "${field}" expects a positive number`, line, column);
5148
+ const parsed = parseFiniteNumber2(value, line, column, field);
5149
+ if (parsed <= 0) {
5150
+ throw new WorldOrbitError(`Field "${field}" must be greater than zero`, line, column);
3605
5151
  }
3606
5152
  return parsed;
3607
5153
  }
3608
5154
  function parseFiniteNumber2(value, line, column, field) {
3609
5155
  const parsed = Number(value);
3610
5156
  if (!Number.isFinite(parsed)) {
3611
- throw new WorldOrbitError(`Field "${field}" expects a finite number`, line, column);
5157
+ throw new WorldOrbitError(`Invalid numeric value "${value}" for "${field}"`, line, column);
3612
5158
  }
3613
5159
  return parsed;
3614
5160
  }
@@ -3620,28 +5166,43 @@ var WorldOrbit = (() => {
3620
5166
  groupIds: []
3621
5167
  };
3622
5168
  }
3623
- function parseInlineFields2(tokens, line) {
5169
+ function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
3624
5170
  const fields = [];
3625
5171
  let index = 0;
3626
5172
  while (index < tokens.length) {
3627
5173
  const keyToken = tokens[index];
3628
- const schema = getFieldSchema(keyToken.value);
3629
- if (!schema) {
5174
+ const spec = getDraftObjectFieldSpec(keyToken.value);
5175
+ if (!spec) {
3630
5176
  throw new WorldOrbitError(`Unknown field "${keyToken.value}"`, line, keyToken.column);
3631
5177
  }
5178
+ if (spec.version === "2.1") {
5179
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, keyToken.value, {
5180
+ line,
5181
+ column: keyToken.column
5182
+ });
5183
+ }
3632
5184
  index++;
3633
5185
  const valueTokens = [];
3634
- if (schema.arity === "multiple") {
3635
- while (index < tokens.length && !isKnownFieldKey(tokens[index].value)) {
3636
- valueTokens.push(tokens[index]);
3637
- index++;
3638
- }
3639
- } else {
5186
+ if (spec.inlineMode === "single") {
3640
5187
  const nextToken = tokens[index];
3641
5188
  if (nextToken) {
3642
5189
  valueTokens.push(nextToken);
3643
5190
  index++;
3644
5191
  }
5192
+ } else if (spec.inlineMode === "pair") {
5193
+ for (let count = 0; count < 2; count++) {
5194
+ const nextToken = tokens[index];
5195
+ if (!nextToken) {
5196
+ break;
5197
+ }
5198
+ valueTokens.push(nextToken);
5199
+ index++;
5200
+ }
5201
+ } else {
5202
+ while (index < tokens.length && !DRAFT_OBJECT_FIELD_KEYS.has(tokens[index].value)) {
5203
+ valueTokens.push(tokens[index]);
5204
+ index++;
5205
+ }
3645
5206
  }
3646
5207
  if (valueTokens.length === 0) {
3647
5208
  throw new WorldOrbitError(`Missing value for field "${keyToken.value}"`, line, keyToken.column);
@@ -3653,25 +5214,35 @@ var WorldOrbit = (() => {
3653
5214
  location: { line, column: keyToken.column }
3654
5215
  });
3655
5216
  }
5217
+ validateDraftObjectFieldCompatibility(fields, objectType);
3656
5218
  return fields;
3657
5219
  }
3658
- function parseField2(tokens, line) {
5220
+ function parseObjectField(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
3659
5221
  if (tokens.length < 2) {
3660
5222
  throw new WorldOrbitError("Invalid field line", line, tokens[0]?.column ?? 1);
3661
5223
  }
3662
- if (!getFieldSchema(tokens[0].value)) {
5224
+ const spec = getDraftObjectFieldSpec(tokens[0].value);
5225
+ if (!spec) {
3663
5226
  throw new WorldOrbitError(`Unknown field "${tokens[0].value}"`, line, tokens[0].column);
3664
5227
  }
3665
- return {
5228
+ if (spec.version === "2.1") {
5229
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, tokens[0].value, {
5230
+ line,
5231
+ column: tokens[0].column
5232
+ });
5233
+ }
5234
+ const field = {
3666
5235
  type: "field",
3667
5236
  key: tokens[0].value,
3668
5237
  values: tokens.slice(1).map((token) => token.value),
3669
5238
  location: { line, column: tokens[0].column }
3670
5239
  };
5240
+ validateDraftObjectFieldCompatibility([field], objectType);
5241
+ return field;
3671
5242
  }
3672
- function parseInfoEntry2(tokens, line) {
5243
+ function parseInfoLikeEntry(tokens, line, errorMessage) {
3673
5244
  if (tokens.length < 2) {
3674
- throw new WorldOrbitError("Invalid info entry", line, tokens[0]?.column ?? 1);
5245
+ throw new WorldOrbitError(errorMessage, line, tokens[0]?.column ?? 1);
3675
5246
  }
3676
5247
  return {
3677
5248
  type: "info-entry",
@@ -3680,23 +5251,374 @@ var WorldOrbit = (() => {
3680
5251
  location: { line, column: tokens[0].column }
3681
5252
  };
3682
5253
  }
3683
- function normalizeIdentifier2(value) {
3684
- return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
5254
+ function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
5255
+ const fieldMap = collectDraftFields(node.fields);
5256
+ const placement = extractPlacementFromFieldMap(fieldMap);
5257
+ const properties = normalizeDraftProperties(node.objectType, fieldMap);
5258
+ const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
5259
+ const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
5260
+ const referencePlane = parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0]);
5261
+ const tidalLock = fieldMap.has("tidalLock") ? parseAtlasBoolean(singleFieldValue2(fieldMap.get("tidalLock")[0]), "tidalLock", fieldMap.get("tidalLock")[0].location) : void 0;
5262
+ const resonance = fieldMap.has("resonance") ? parseResonanceField(fieldMap.get("resonance")[0]) : void 0;
5263
+ const renderHints = extractRenderHints(fieldMap);
5264
+ const deriveRules = fieldMap.get("derive")?.map((field) => parseDeriveField(field));
5265
+ const validationRules = fieldMap.get("validate")?.map((field) => ({
5266
+ rule: singleFieldValue2(field)
5267
+ }));
5268
+ const lockedFields = fieldMap.has("locked") ? [...new Set(fieldMap.get("locked").flatMap((field) => field.values))] : void 0;
5269
+ const tolerances = fieldMap.get("tolerance")?.map((field) => parseToleranceField(field));
5270
+ const typedBlocks = normalizeTypedBlocks(node.typedBlockEntries);
5271
+ const info2 = normalizeInfoEntries(node.infoEntries, "info");
5272
+ const object = {
5273
+ type: node.objectType,
5274
+ id: node.id,
5275
+ properties,
5276
+ placement,
5277
+ info: info2
5278
+ };
5279
+ if (groups.length > 0)
5280
+ object.groups = groups;
5281
+ if (epoch)
5282
+ object.epoch = epoch;
5283
+ if (referencePlane)
5284
+ object.referencePlane = referencePlane;
5285
+ if (tidalLock !== void 0)
5286
+ object.tidalLock = tidalLock;
5287
+ if (resonance)
5288
+ object.resonance = resonance;
5289
+ if (renderHints)
5290
+ object.renderHints = renderHints;
5291
+ if (deriveRules?.length)
5292
+ object.deriveRules = deriveRules;
5293
+ if (validationRules?.length)
5294
+ object.validationRules = validationRules;
5295
+ if (lockedFields?.length)
5296
+ object.lockedFields = lockedFields;
5297
+ if (tolerances?.length)
5298
+ object.tolerances = tolerances;
5299
+ if (typedBlocks && Object.keys(typedBlocks).length > 0)
5300
+ object.typedBlocks = typedBlocks;
5301
+ if (sourceSchemaVersion !== "2.1") {
5302
+ 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) {
5303
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
5304
+ }
5305
+ }
5306
+ return object;
3685
5307
  }
3686
- function humanizeIdentifier3(value) {
3687
- return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
5308
+ function normalizeDraftEvent(event, rawPoses) {
5309
+ return {
5310
+ ...event,
5311
+ participantObjectIds: [...new Set(event.participantObjectIds)],
5312
+ tags: [...new Set(event.tags)],
5313
+ positions: rawPoses.map((pose) => normalizeDraftEventPose(pose))
5314
+ };
5315
+ }
5316
+ function normalizeDraftEventPose(rawPose) {
5317
+ const fieldMap = collectDraftFields(rawPose.fields);
5318
+ const placement = extractPlacementFromFieldMap(fieldMap);
5319
+ return {
5320
+ objectId: rawPose.objectId,
5321
+ placement,
5322
+ inner: parseOptionalUnitField(fieldMap.get("inner")?.[0], "inner"),
5323
+ outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer")
5324
+ };
5325
+ }
5326
+ function collectDraftFields(fields) {
5327
+ const grouped = /* @__PURE__ */ new Map();
5328
+ for (const field of fields) {
5329
+ const spec = getDraftObjectFieldSpec(field.key);
5330
+ if (!spec) {
5331
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
5332
+ }
5333
+ if (!spec.allowRepeat && grouped.has(field.key)) {
5334
+ throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
5335
+ }
5336
+ const existing = grouped.get(field.key) ?? [];
5337
+ existing.push(field);
5338
+ grouped.set(field.key, existing);
5339
+ }
5340
+ return grouped;
5341
+ }
5342
+ function extractPlacementFromFieldMap(fieldMap) {
5343
+ const orbitField = fieldMap.get("orbit")?.[0];
5344
+ const atField = fieldMap.get("at")?.[0];
5345
+ const surfaceField = fieldMap.get("surface")?.[0];
5346
+ const freeField = fieldMap.get("free")?.[0];
5347
+ const count = [orbitField, atField, surfaceField, freeField].filter(Boolean).length;
5348
+ if (count > 1) {
5349
+ const conflictingField = orbitField ?? atField ?? surfaceField ?? freeField;
5350
+ throw WorldOrbitError.fromLocation("Object has multiple placement modes", conflictingField?.location);
5351
+ }
5352
+ if (orbitField) {
5353
+ return {
5354
+ mode: "orbit",
5355
+ target: singleFieldValue2(orbitField),
5356
+ distance: parseOptionalUnitField(fieldMap.get("distance")?.[0], "distance"),
5357
+ semiMajor: parseOptionalUnitField(fieldMap.get("semiMajor")?.[0], "semiMajor"),
5358
+ eccentricity: parseOptionalNumberField(fieldMap.get("eccentricity")?.[0], "eccentricity"),
5359
+ period: parseOptionalUnitField(fieldMap.get("period")?.[0], "period"),
5360
+ angle: parseOptionalUnitField(fieldMap.get("angle")?.[0], "angle"),
5361
+ inclination: parseOptionalUnitField(fieldMap.get("inclination")?.[0], "inclination"),
5362
+ phase: parseOptionalUnitField(fieldMap.get("phase")?.[0], "phase")
5363
+ };
5364
+ }
5365
+ if (atField) {
5366
+ const target = singleFieldValue2(atField);
5367
+ return {
5368
+ mode: "at",
5369
+ target,
5370
+ reference: parseAtlasAtReference(target, atField.location)
5371
+ };
5372
+ }
5373
+ if (surfaceField) {
5374
+ return {
5375
+ mode: "surface",
5376
+ target: singleFieldValue2(surfaceField)
5377
+ };
5378
+ }
5379
+ if (freeField) {
5380
+ const raw = singleFieldValue2(freeField);
5381
+ const distance = tryParseAtlasUnitValue(raw);
5382
+ return {
5383
+ mode: "free",
5384
+ distance: distance ?? void 0,
5385
+ descriptor: distance ? void 0 : raw
5386
+ };
5387
+ }
5388
+ return null;
5389
+ }
5390
+ function normalizeDraftProperties(objectType, fieldMap) {
5391
+ const properties = {};
5392
+ for (const [key, fields] of fieldMap.entries()) {
5393
+ const field = fields[0];
5394
+ const spec = getDraftObjectFieldSpec(key);
5395
+ if (!field || !spec?.legacySchema || spec.legacySchema.placement) {
5396
+ continue;
5397
+ }
5398
+ ensureAtlasFieldSupported(key, objectType, field.location);
5399
+ properties[key] = normalizeLegacyScalarValue(key, field.values, field.location);
5400
+ }
5401
+ return properties;
5402
+ }
5403
+ function normalizeInfoEntries(entries, label) {
5404
+ const normalized = {};
5405
+ for (const entry of entries) {
5406
+ if (entry.key in normalized) {
5407
+ throw WorldOrbitError.fromLocation(`Duplicate ${label} key "${entry.key}"`, entry.location);
5408
+ }
5409
+ normalized[entry.key] = entry.value;
5410
+ }
5411
+ return normalized;
5412
+ }
5413
+ function normalizeTypedBlocks(typedBlockEntries) {
5414
+ const typedBlocks = {};
5415
+ for (const blockName of Object.keys(typedBlockEntries)) {
5416
+ const entries = typedBlockEntries[blockName];
5417
+ if (entries?.length) {
5418
+ typedBlocks[blockName] = normalizeInfoEntries(entries, blockName);
5419
+ }
5420
+ }
5421
+ return typedBlocks;
5422
+ }
5423
+ function extractRenderHints(fieldMap) {
5424
+ const renderHints = {};
5425
+ const renderLabelField = fieldMap.get("renderLabel")?.[0];
5426
+ const renderOrbitField = fieldMap.get("renderOrbit")?.[0];
5427
+ const renderPriorityField = fieldMap.get("renderPriority")?.[0];
5428
+ if (renderLabelField) {
5429
+ renderHints.renderLabel = parseAtlasBoolean(singleFieldValue2(renderLabelField), "renderLabel", renderLabelField.location);
5430
+ }
5431
+ if (renderOrbitField) {
5432
+ renderHints.renderOrbit = parseAtlasBoolean(singleFieldValue2(renderOrbitField), "renderOrbit", renderOrbitField.location);
5433
+ }
5434
+ if (renderPriorityField) {
5435
+ renderHints.renderPriority = parseAtlasNumber(singleFieldValue2(renderPriorityField), "renderPriority", renderPriorityField.location);
5436
+ }
5437
+ return Object.keys(renderHints).length > 0 ? renderHints : void 0;
5438
+ }
5439
+ function parseResonanceField(field) {
5440
+ if (field.values.length !== 2) {
5441
+ throw WorldOrbitError.fromLocation('Field "resonance" expects "<targetObjectId> <ratio>"', field.location);
5442
+ }
5443
+ const ratio = field.values[1];
5444
+ if (!/^\d+:\d+$/.test(ratio)) {
5445
+ throw WorldOrbitError.fromLocation(`Invalid resonance ratio "${ratio}"`, field.location);
5446
+ }
5447
+ return {
5448
+ targetObjectId: field.values[0],
5449
+ ratio
5450
+ };
5451
+ }
5452
+ function parseDeriveField(field) {
5453
+ if (field.values.length !== 2) {
5454
+ throw WorldOrbitError.fromLocation('Field "derive" expects "<field> <strategy>"', field.location);
5455
+ }
5456
+ return {
5457
+ field: field.values[0],
5458
+ strategy: field.values[1]
5459
+ };
5460
+ }
5461
+ function parseToleranceField(field) {
5462
+ if (field.values.length !== 2) {
5463
+ throw WorldOrbitError.fromLocation('Field "tolerance" expects "<field> <value>"', field.location);
5464
+ }
5465
+ const rawValue = field.values[1];
5466
+ const unitValue = tryParseAtlasUnitValue(rawValue);
5467
+ const numericValue = Number(rawValue);
5468
+ return {
5469
+ field: field.values[0],
5470
+ value: unitValue ?? (Number.isFinite(numericValue) ? numericValue : rawValue)
5471
+ };
5472
+ }
5473
+ function parseOptionalTokenList(field) {
5474
+ return field ? [...new Set(field.values)] : [];
5475
+ }
5476
+ function parseOptionalJoinedValue(field) {
5477
+ if (!field) {
5478
+ return null;
5479
+ }
5480
+ return field.values.join(" ").trim() || null;
5481
+ }
5482
+ function parseOptionalUnitField(field, key) {
5483
+ return field ? parseAtlasUnitValue(singleFieldValue2(field), field.location, key) : void 0;
5484
+ }
5485
+ function parseOptionalNumberField(field, key) {
5486
+ return field ? parseAtlasNumber(singleFieldValue2(field), key, field.location) : void 0;
5487
+ }
5488
+ function singleFieldValue2(field) {
5489
+ return singleAtlasValue(field.values, field.key, field.location);
5490
+ }
5491
+ function getDraftObjectFieldSpec(key) {
5492
+ return DRAFT_OBJECT_FIELD_SPECS.get(key);
5493
+ }
5494
+ function validateDraftObjectFieldCompatibility(fields, objectType) {
5495
+ for (const field of fields) {
5496
+ const spec = getDraftObjectFieldSpec(field.key);
5497
+ if (!spec) {
5498
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
5499
+ }
5500
+ if (spec.legacySchema) {
5501
+ ensureAtlasFieldSupported(field.key, objectType, field.location);
5502
+ continue;
5503
+ }
5504
+ if ((field.key === "renderLabel" || field.key === "renderOrbit" || field.key === "tidalLock") && field.values.length !== 1) {
5505
+ throw WorldOrbitError.fromLocation(`Field "${field.key}" expects exactly one value`, field.location);
5506
+ }
5507
+ }
5508
+ }
5509
+ function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
5510
+ if (sourceSchemaVersion === "2.1") {
5511
+ return;
5512
+ }
5513
+ diagnostics.push({
5514
+ code: "parse.schema21.featureCompatibility",
5515
+ severity: "warning",
5516
+ source: "parse",
5517
+ message: `Feature "${featureName}" requires schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
5518
+ line: location.line,
5519
+ column: location.column
5520
+ });
5521
+ }
5522
+ function preprocessAtlasSource(source) {
5523
+ const chars = [...source];
5524
+ const comments = [];
5525
+ let inString = false;
5526
+ let inBlockComment = false;
5527
+ let blockCommentStart = null;
5528
+ let line = 1;
5529
+ let column = 1;
5530
+ for (let index = 0; index < chars.length; index++) {
5531
+ const ch = chars[index];
5532
+ const next = chars[index + 1];
5533
+ if (inBlockComment) {
5534
+ if (ch === "*" && next === "/") {
5535
+ chars[index] = " ";
5536
+ chars[index + 1] = " ";
5537
+ inBlockComment = false;
5538
+ blockCommentStart = null;
5539
+ index++;
5540
+ column += 2;
5541
+ continue;
5542
+ }
5543
+ if (ch !== "\n" && ch !== "\r") {
5544
+ chars[index] = " ";
5545
+ }
5546
+ if (ch === "\n") {
5547
+ line++;
5548
+ column = 1;
5549
+ } else {
5550
+ column++;
5551
+ }
5552
+ continue;
5553
+ }
5554
+ if (!inString && ch === "/" && next === "*") {
5555
+ comments.push({ kind: "block", line, column });
5556
+ chars[index] = " ";
5557
+ chars[index + 1] = " ";
5558
+ inBlockComment = true;
5559
+ blockCommentStart = { line, column };
5560
+ index++;
5561
+ column += 2;
5562
+ continue;
5563
+ }
5564
+ if (!inString && ch === "#" && !isHexColorLiteral(chars, index)) {
5565
+ comments.push({ kind: "line", line, column });
5566
+ chars[index] = " ";
5567
+ let inner = index + 1;
5568
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
5569
+ chars[inner] = " ";
5570
+ inner++;
5571
+ }
5572
+ column += inner - index;
5573
+ index = inner - 1;
5574
+ continue;
5575
+ }
5576
+ if (ch === '"' && chars[index - 1] !== "\\") {
5577
+ inString = !inString;
5578
+ }
5579
+ if (ch === "\n") {
5580
+ line++;
5581
+ column = 1;
5582
+ } else {
5583
+ column++;
5584
+ }
5585
+ }
5586
+ if (inBlockComment) {
5587
+ throw WorldOrbitError.fromLocation("Unclosed block comment", blockCommentStart ?? void 0);
5588
+ }
5589
+ return {
5590
+ source: chars.join(""),
5591
+ comments
5592
+ };
5593
+ }
5594
+ function isHexColorLiteral(chars, start) {
5595
+ let index = start + 1;
5596
+ let length = 0;
5597
+ while (index < chars.length && /[0-9a-f]/i.test(chars[index] ?? "")) {
5598
+ index++;
5599
+ length++;
5600
+ }
5601
+ if (![3, 4, 6, 8].includes(length)) {
5602
+ return false;
5603
+ }
5604
+ const next = chars[index];
5605
+ return next === void 0 || next === " " || next === " " || next === "\r" || next === "\n";
3688
5606
  }
3689
5607
 
3690
5608
  // packages/core/dist/atlas-edit.js
3691
- function createEmptyAtlasDocument(systemId = "WorldOrbit") {
5609
+ function createEmptyAtlasDocument(systemId = "WorldOrbit", version = "2.0") {
3692
5610
  return {
3693
5611
  format: "worldorbit",
3694
- version: "2.0",
5612
+ version,
5613
+ schemaVersion: version,
3695
5614
  sourceVersion: "1.0",
3696
5615
  system: {
3697
5616
  type: "system",
3698
5617
  id: systemId,
3699
5618
  title: systemId,
5619
+ description: null,
5620
+ epoch: null,
5621
+ referencePlane: null,
3700
5622
  defaults: {
3701
5623
  view: "topdown",
3702
5624
  scale: null,
@@ -3708,6 +5630,9 @@ var WorldOrbit = (() => {
3708
5630
  viewpoints: [],
3709
5631
  annotations: []
3710
5632
  },
5633
+ groups: [],
5634
+ relations: [],
5635
+ events: [],
3711
5636
  objects: [],
3712
5637
  diagnostics: []
3713
5638
  };
@@ -3721,14 +5646,26 @@ var WorldOrbit = (() => {
3721
5646
  for (const key of Object.keys(document.system.atlasMetadata).sort()) {
3722
5647
  paths.push({ kind: "metadata", key });
3723
5648
  }
3724
- for (const viewpoint of [...document.system.viewpoints].sort(compareIdLike)) {
5649
+ for (const viewpoint of [...document.system.viewpoints].sort(compareIdLike2)) {
3725
5650
  paths.push({ kind: "viewpoint", id: viewpoint.id });
3726
5651
  }
3727
- for (const annotation of [...document.system.annotations].sort(compareIdLike)) {
5652
+ for (const annotation of [...document.system.annotations].sort(compareIdLike2)) {
3728
5653
  paths.push({ kind: "annotation", id: annotation.id });
3729
5654
  }
3730
5655
  }
3731
- for (const object of [...document.objects].sort(compareIdLike)) {
5656
+ for (const group of [...document.groups].sort(compareIdLike2)) {
5657
+ paths.push({ kind: "group", id: group.id });
5658
+ }
5659
+ for (const relation of [...document.relations].sort(compareIdLike2)) {
5660
+ paths.push({ kind: "relation", id: relation.id });
5661
+ }
5662
+ for (const event of [...document.events].sort(compareIdLike2)) {
5663
+ paths.push({ kind: "event", id: event.id });
5664
+ for (const pose of [...event.positions].sort(comparePoseObjectId2)) {
5665
+ paths.push({ kind: "event-pose", id: event.id, key: pose.objectId });
5666
+ }
5667
+ }
5668
+ for (const object of [...document.objects].sort(compareIdLike2)) {
3732
5669
  paths.push({ kind: "object", id: object.id });
3733
5670
  }
3734
5671
  return paths;
@@ -3741,12 +5678,20 @@ var WorldOrbit = (() => {
3741
5678
  return document.system?.defaults ?? null;
3742
5679
  case "metadata":
3743
5680
  return path.key ? document.system?.atlasMetadata[path.key] ?? null : null;
5681
+ case "group":
5682
+ return path.id ? findGroup(document, path.id) : null;
5683
+ case "event":
5684
+ return path.id ? findEvent(document, path.id) : null;
5685
+ case "event-pose":
5686
+ return path.id && path.key ? findEventPose(document, path.id, path.key) : null;
3744
5687
  case "object":
3745
5688
  return path.id ? findObject(document, path.id) : null;
3746
5689
  case "viewpoint":
3747
5690
  return path.id ? findViewpoint(document.system, path.id) : null;
3748
5691
  case "annotation":
3749
5692
  return path.id ? findAnnotation(document.system, path.id) : null;
5693
+ case "relation":
5694
+ return path.id ? findRelation(document, path.id) : null;
3750
5695
  }
3751
5696
  }
3752
5697
  function upsertAtlasDocumentNode(document, path, value) {
@@ -3772,6 +5717,24 @@ var WorldOrbit = (() => {
3772
5717
  system.atlasMetadata[path.key] = String(value);
3773
5718
  }
3774
5719
  return next;
5720
+ case "group":
5721
+ if (!path.id) {
5722
+ throw new Error('Group updates require an "id" value.');
5723
+ }
5724
+ upsertById(next.groups, value);
5725
+ return next;
5726
+ case "event":
5727
+ if (!path.id) {
5728
+ throw new Error('Event updates require an "id" value.');
5729
+ }
5730
+ upsertById(next.events, value);
5731
+ return next;
5732
+ case "event-pose":
5733
+ if (!path.id || !path.key) {
5734
+ throw new Error('Event pose updates require an event "id" and pose "key" value.');
5735
+ }
5736
+ upsertEventPose(next.events, path.id, value);
5737
+ return next;
3775
5738
  case "object":
3776
5739
  if (!path.id) {
3777
5740
  throw new Error('Object updates require an "id" value.');
@@ -3790,6 +5753,12 @@ var WorldOrbit = (() => {
3790
5753
  }
3791
5754
  upsertById(system.annotations, value);
3792
5755
  return next;
5756
+ case "relation":
5757
+ if (!path.id) {
5758
+ throw new Error('Relation updates require an "id" value.');
5759
+ }
5760
+ upsertById(next.relations, value);
5761
+ return next;
3793
5762
  }
3794
5763
  }
3795
5764
  function updateAtlasDocumentNode(document, path, updater) {
@@ -3809,6 +5778,24 @@ var WorldOrbit = (() => {
3809
5778
  next.objects = next.objects.filter((object) => object.id !== path.id);
3810
5779
  }
3811
5780
  return next;
5781
+ case "group":
5782
+ if (path.id) {
5783
+ next.groups = next.groups.filter((group) => group.id !== path.id);
5784
+ }
5785
+ return next;
5786
+ case "event":
5787
+ if (path.id) {
5788
+ next.events = next.events.filter((event) => event.id !== path.id);
5789
+ }
5790
+ return next;
5791
+ case "event-pose":
5792
+ if (path.id && path.key) {
5793
+ const event = findEvent(next, path.id);
5794
+ if (event) {
5795
+ event.positions = event.positions.filter((pose) => pose.objectId !== path.key);
5796
+ }
5797
+ }
5798
+ return next;
3812
5799
  case "viewpoint":
3813
5800
  if (path.id) {
3814
5801
  system.viewpoints = system.viewpoints.filter((viewpoint) => viewpoint.id !== path.id);
@@ -3819,6 +5806,11 @@ var WorldOrbit = (() => {
3819
5806
  system.annotations = system.annotations.filter((annotation) => annotation.id !== path.id);
3820
5807
  }
3821
5808
  return next;
5809
+ case "relation":
5810
+ if (path.id) {
5811
+ next.relations = next.relations.filter((relation) => relation.id !== path.id);
5812
+ }
5813
+ return next;
3822
5814
  default:
3823
5815
  return next;
3824
5816
  }
@@ -3836,6 +5828,15 @@ var WorldOrbit = (() => {
3836
5828
  id: diagnostic.objectId
3837
5829
  };
3838
5830
  }
5831
+ if (diagnostic.field?.startsWith("group.")) {
5832
+ const parts = diagnostic.field.split(".");
5833
+ if (parts[1] && findGroup(document, parts[1])) {
5834
+ return {
5835
+ kind: "group",
5836
+ id: parts[1]
5837
+ };
5838
+ }
5839
+ }
3839
5840
  if (diagnostic.field?.startsWith("viewpoint.")) {
3840
5841
  const parts = diagnostic.field.split(".");
3841
5842
  if (parts[1] && findViewpoint(document.system, parts[1])) {
@@ -3854,6 +5855,31 @@ var WorldOrbit = (() => {
3854
5855
  };
3855
5856
  }
3856
5857
  }
5858
+ if (diagnostic.field?.startsWith("relation.")) {
5859
+ const parts = diagnostic.field.split(".");
5860
+ if (parts[1] && findRelation(document, parts[1])) {
5861
+ return {
5862
+ kind: "relation",
5863
+ id: parts[1]
5864
+ };
5865
+ }
5866
+ }
5867
+ if (diagnostic.field?.startsWith("event.")) {
5868
+ const parts = diagnostic.field.split(".");
5869
+ if (parts[1] && findEvent(document, parts[1])) {
5870
+ if (parts[2] === "pose" && parts[3] && findEventPose(document, parts[1], parts[3])) {
5871
+ return {
5872
+ kind: "event-pose",
5873
+ id: parts[1],
5874
+ key: parts[3]
5875
+ };
5876
+ }
5877
+ return {
5878
+ kind: "event",
5879
+ id: parts[1]
5880
+ };
5881
+ }
5882
+ }
3857
5883
  if (diagnostic.field && diagnostic.field in ensureSystem(document).atlasMetadata) {
3858
5884
  return {
3859
5885
  kind: "metadata",
@@ -3863,9 +5889,11 @@ var WorldOrbit = (() => {
3863
5889
  return null;
3864
5890
  }
3865
5891
  function validateAtlasDocumentWithDiagnostics(document) {
3866
- const materialized = materializeAtlasDocument(document);
3867
- const result = validateDocumentWithDiagnostics(materialized);
3868
- return resolveAtlasDiagnostics(document, result.diagnostics);
5892
+ const diagnostics = [
5893
+ ...document.diagnostics,
5894
+ ...collectAtlasDiagnostics(document, document.version)
5895
+ ];
5896
+ return resolveAtlasDiagnostics(document, diagnostics);
3869
5897
  }
3870
5898
  function ensureSystem(document) {
3871
5899
  if (document.system) {
@@ -3877,6 +5905,18 @@ var WorldOrbit = (() => {
3877
5905
  function findObject(document, objectId) {
3878
5906
  return document.objects.find((object) => object.id === objectId) ?? null;
3879
5907
  }
5908
+ function findGroup(document, groupId) {
5909
+ return document.groups.find((group) => group.id === groupId) ?? null;
5910
+ }
5911
+ function findRelation(document, relationId) {
5912
+ return document.relations.find((relation) => relation.id === relationId) ?? null;
5913
+ }
5914
+ function findEvent(document, eventId) {
5915
+ return document.events.find((event) => event.id === eventId) ?? null;
5916
+ }
5917
+ function findEventPose(document, eventId, objectId) {
5918
+ return findEvent(document, eventId)?.positions.find((pose) => pose.objectId === objectId) ?? null;
5919
+ }
3880
5920
  function findViewpoint(system, viewpointId) {
3881
5921
  return system?.viewpoints.find((viewpoint) => viewpoint.id === viewpointId) ?? null;
3882
5922
  }
@@ -3887,20 +5927,37 @@ var WorldOrbit = (() => {
3887
5927
  const index = items.findIndex((item) => item.id === value.id);
3888
5928
  if (index === -1) {
3889
5929
  items.push(value);
3890
- items.sort(compareIdLike);
5930
+ items.sort(compareIdLike2);
3891
5931
  return;
3892
5932
  }
3893
5933
  items[index] = value;
3894
5934
  }
3895
- function compareIdLike(left, right) {
5935
+ function upsertEventPose(events, eventId, value) {
5936
+ const event = events.find((entry) => entry.id === eventId);
5937
+ if (!event) {
5938
+ throw new Error(`Unknown event "${eventId}" for pose update.`);
5939
+ }
5940
+ const index = event.positions.findIndex((entry) => entry.objectId === value.objectId);
5941
+ if (index === -1) {
5942
+ event.positions.push(value);
5943
+ event.positions.sort(comparePoseObjectId2);
5944
+ return;
5945
+ }
5946
+ event.positions[index] = value;
5947
+ }
5948
+ function compareIdLike2(left, right) {
3896
5949
  return left.id.localeCompare(right.id);
3897
5950
  }
5951
+ function comparePoseObjectId2(left, right) {
5952
+ return left.objectId.localeCompare(right.objectId);
5953
+ }
3898
5954
 
3899
5955
  // packages/core/dist/load.js
3900
- var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0)?$/i;
5956
+ var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1)?$/i;
5957
+ var ATLAS_SCHEMA_21_PATTERN = /^schema\s+2\.1$/i;
3901
5958
  var LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
3902
5959
  function detectWorldOrbitSchemaVersion(source) {
3903
- for (const line of source.split(/\r?\n/)) {
5960
+ for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
3904
5961
  const trimmed = line.trim();
3905
5962
  if (!trimmed) {
3906
5963
  continue;
@@ -3908,6 +5965,9 @@ var WorldOrbit = (() => {
3908
5965
  if (LEGACY_DRAFT_SCHEMA_PATTERN.test(trimmed)) {
3909
5966
  return "2.0-draft";
3910
5967
  }
5968
+ if (ATLAS_SCHEMA_21_PATTERN.test(trimmed)) {
5969
+ return "2.1";
5970
+ }
3911
5971
  if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
3912
5972
  return "2.0";
3913
5973
  }
@@ -3915,6 +5975,49 @@ var WorldOrbit = (() => {
3915
5975
  }
3916
5976
  return "1.0";
3917
5977
  }
5978
+ function stripCommentsForSchemaDetection(source) {
5979
+ const chars = [...source];
5980
+ let inString = false;
5981
+ let inBlockComment = false;
5982
+ for (let index = 0; index < chars.length; index++) {
5983
+ const ch = chars[index];
5984
+ const next = chars[index + 1];
5985
+ if (inBlockComment) {
5986
+ if (ch === "*" && next === "/") {
5987
+ chars[index] = " ";
5988
+ chars[index + 1] = " ";
5989
+ inBlockComment = false;
5990
+ index++;
5991
+ continue;
5992
+ }
5993
+ if (ch !== "\n" && ch !== "\r") {
5994
+ chars[index] = " ";
5995
+ }
5996
+ continue;
5997
+ }
5998
+ if (!inString && ch === "/" && next === "*") {
5999
+ chars[index] = " ";
6000
+ chars[index + 1] = " ";
6001
+ inBlockComment = true;
6002
+ index++;
6003
+ continue;
6004
+ }
6005
+ if (!inString && ch === "#") {
6006
+ chars[index] = " ";
6007
+ let inner = index + 1;
6008
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
6009
+ chars[inner] = " ";
6010
+ inner++;
6011
+ }
6012
+ index = inner - 1;
6013
+ continue;
6014
+ }
6015
+ if (ch === '"' && chars[index - 1] !== "\\") {
6016
+ inString = !inString;
6017
+ }
6018
+ }
6019
+ return chars.join("");
6020
+ }
3918
6021
  function loadWorldOrbitSource(source) {
3919
6022
  const result = loadWorldOrbitSourceWithDiagnostics(source);
3920
6023
  if (!result.ok || !result.value) {
@@ -3925,36 +6028,36 @@ var WorldOrbit = (() => {
3925
6028
  }
3926
6029
  function loadWorldOrbitSourceWithDiagnostics(source) {
3927
6030
  const schemaVersion = detectWorldOrbitSchemaVersion(source);
3928
- if (schemaVersion === "2.0" || schemaVersion === "2.0-draft") {
6031
+ if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1") {
3929
6032
  return loadAtlasSourceWithDiagnostics(source, schemaVersion);
3930
6033
  }
3931
6034
  let ast;
3932
6035
  try {
3933
6036
  ast = parseWorldOrbit(source);
3934
- } catch (error) {
6037
+ } catch (error2) {
3935
6038
  return {
3936
6039
  ok: false,
3937
6040
  value: null,
3938
- diagnostics: [diagnosticFromError(error, "parse")]
6041
+ diagnostics: [diagnosticFromError(error2, "parse")]
3939
6042
  };
3940
6043
  }
3941
6044
  let document;
3942
6045
  try {
3943
6046
  document = normalizeDocument(ast);
3944
- } catch (error) {
6047
+ } catch (error2) {
3945
6048
  return {
3946
6049
  ok: false,
3947
6050
  value: null,
3948
- diagnostics: [diagnosticFromError(error, "normalize")]
6051
+ diagnostics: [diagnosticFromError(error2, "normalize")]
3949
6052
  };
3950
6053
  }
3951
6054
  try {
3952
6055
  validateDocument(document);
3953
- } catch (error) {
6056
+ } catch (error2) {
3954
6057
  return {
3955
6058
  ok: false,
3956
6059
  value: null,
3957
- diagnostics: [diagnosticFromError(error, "validate")]
6060
+ diagnostics: [diagnosticFromError(error2, "validate")]
3958
6061
  };
3959
6062
  }
3960
6063
  return {
@@ -3974,30 +6077,29 @@ var WorldOrbit = (() => {
3974
6077
  let atlasDocument;
3975
6078
  try {
3976
6079
  atlasDocument = parseWorldOrbitAtlas(source);
3977
- } catch (error) {
6080
+ } catch (error2) {
3978
6081
  return {
3979
6082
  ok: false,
3980
6083
  value: null,
3981
- diagnostics: [diagnosticFromError(error, "parse", "load.atlas.failed")]
6084
+ diagnostics: [diagnosticFromError(error2, "parse", "load.atlas.failed")]
3982
6085
  };
3983
6086
  }
3984
- let document;
3985
- try {
3986
- document = materializeAtlasDocument(atlasDocument);
3987
- } catch (error) {
6087
+ const atlasDiagnostics = [...atlasDocument.diagnostics];
6088
+ if (atlasDiagnostics.some((diagnostic) => diagnostic.severity === "error")) {
3988
6089
  return {
3989
6090
  ok: false,
3990
6091
  value: null,
3991
- diagnostics: [diagnosticFromError(error, "normalize", "load.atlas.materialize.failed")]
6092
+ diagnostics: atlasDiagnostics
3992
6093
  };
3993
6094
  }
6095
+ let document;
3994
6096
  try {
3995
- validateDocument(document);
3996
- } catch (error) {
6097
+ document = materializeAtlasDocument(atlasDocument);
6098
+ } catch (error2) {
3997
6099
  return {
3998
6100
  ok: false,
3999
6101
  value: null,
4000
- diagnostics: [diagnosticFromError(error, "validate", "load.atlas.validate.failed")]
6102
+ diagnostics: [diagnosticFromError(error2, "normalize", "load.atlas.materialize.failed")]
4001
6103
  };
4002
6104
  }
4003
6105
  const loaded = {
@@ -4006,12 +6108,12 @@ var WorldOrbit = (() => {
4006
6108
  document,
4007
6109
  atlasDocument,
4008
6110
  draftDocument: atlasDocument,
4009
- diagnostics: [...atlasDocument.diagnostics]
6111
+ diagnostics: atlasDiagnostics
4010
6112
  };
4011
6113
  return {
4012
6114
  ok: true,
4013
6115
  value: loaded,
4014
- diagnostics: [...atlasDocument.diagnostics]
6116
+ diagnostics: atlasDiagnostics
4015
6117
  };
4016
6118
  }
4017
6119
 
@@ -4077,5 +6179,5 @@ var WorldOrbit = (() => {
4077
6179
  function stringify(document, options = {}) {
4078
6180
  return formatDocument(document, options);
4079
6181
  }
4080
- return __toCommonJS(dist_exports);
6182
+ return __toCommonJS(index_exports);
4081
6183
  })();