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/markdown/dist/index.js
22
- var dist_exports = {};
23
- __export(dist_exports, {
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
24
  rehypeWorldOrbit: () => rehypeWorldOrbit,
25
25
  remarkWorldOrbit: () => remarkWorldOrbit,
26
26
  renderWorldOrbitBlock: () => renderWorldOrbitBlock,
@@ -329,13 +329,13 @@ var WorldOrbit = (() => {
329
329
  function unitFamilyAllowsUnit(family, unit) {
330
330
  switch (family) {
331
331
  case "distance":
332
- return unit === null || ["au", "km", "re", "sol"].includes(unit);
332
+ return unit === null || ["au", "km", "m", "ly", "pc", "kpc", "re", "sol"].includes(unit);
333
333
  case "radius":
334
- return unit === null || ["km", "re", "sol"].includes(unit);
334
+ return unit === null || ["km", "m", "re", "rj", "sol"].includes(unit);
335
335
  case "mass":
336
- return unit === null || ["me", "sol"].includes(unit);
336
+ return unit === null || ["me", "mj", "sol"].includes(unit);
337
337
  case "duration":
338
- return unit === null || ["h", "d", "y"].includes(unit);
338
+ return unit === null || ["s", "min", "h", "d", "y", "ky", "my", "gy"].includes(unit);
339
339
  case "angle":
340
340
  return unit === null || unit === "deg";
341
341
  case "generic":
@@ -539,7 +539,7 @@ var WorldOrbit = (() => {
539
539
  }
540
540
 
541
541
  // packages/core/dist/normalize.js
542
- var UNIT_PATTERN = /^(-?\d+(?:\.\d+)?)(au|km|re|sol|me|d|y|h|deg)?$/;
542
+ var UNIT_PATTERN = /^(-?\d+(?:\.\d+)?)(kpc|min|mj|rj|ky|my|gy|au|km|me|re|pc|ly|deg|sol|K|m|s|h|d|y)?$/;
543
543
  var BOOLEAN_VALUES = /* @__PURE__ */ new Map([
544
544
  ["true", true],
545
545
  ["false", false],
@@ -564,7 +564,11 @@ var WorldOrbit = (() => {
564
564
  return {
565
565
  format: "worldorbit",
566
566
  version: "1.0",
567
+ schemaVersion: "1.0",
567
568
  system,
569
+ groups: [],
570
+ relations: [],
571
+ events: [],
568
572
  objects
569
573
  };
570
574
  }
@@ -574,13 +578,17 @@ var WorldOrbit = (() => {
574
578
  const fieldMap = collectFields(mergedFields);
575
579
  const placement = extractPlacement(node.objectType, fieldMap);
576
580
  const properties = normalizeProperties(fieldMap);
577
- const info = normalizeInfo(node.infoEntries);
581
+ const info2 = normalizeInfo(node.infoEntries);
578
582
  if (node.objectType === "system") {
579
583
  return {
580
584
  type: "system",
581
585
  id: node.name,
586
+ title: typeof properties.title === "string" ? properties.title : null,
587
+ description: null,
588
+ epoch: null,
589
+ referencePlane: null,
582
590
  properties,
583
- info
591
+ info: info2
584
592
  };
585
593
  }
586
594
  return {
@@ -588,7 +596,7 @@ var WorldOrbit = (() => {
588
596
  id: node.name,
589
597
  properties,
590
598
  placement,
591
- info
599
+ info: info2
592
600
  };
593
601
  }
594
602
  function validateFieldCompatibility(objectType, fields) {
@@ -718,14 +726,14 @@ var WorldOrbit = (() => {
718
726
  }
719
727
  }
720
728
  function normalizeInfo(entries) {
721
- const info = {};
729
+ const info2 = {};
722
730
  for (const entry of entries) {
723
- if (entry.key in info) {
731
+ if (entry.key in info2) {
724
732
  throw WorldOrbitError.fromLocation(`Duplicate info key "${entry.key}"`, entry.location);
725
733
  }
726
- info[entry.key] = entry.value;
734
+ info2[entry.key] = entry.value;
727
735
  }
728
- return info;
736
+ return info2;
729
737
  }
730
738
  function parseAtReference(target, location) {
731
739
  if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
@@ -896,37 +904,41 @@ var WorldOrbit = (() => {
896
904
  }
897
905
 
898
906
  // packages/core/dist/diagnostics.js
899
- function diagnosticFromError(error, source, code = `${source}.failed`) {
900
- if (error instanceof WorldOrbitError) {
907
+ function diagnosticFromError(error2, source, code = `${source}.failed`) {
908
+ if (error2 instanceof WorldOrbitError) {
901
909
  return {
902
910
  code,
903
911
  severity: "error",
904
912
  source,
905
- message: error.message,
906
- line: error.line,
907
- column: error.column
913
+ message: error2.message,
914
+ line: error2.line,
915
+ column: error2.column
908
916
  };
909
917
  }
910
- if (error instanceof Error) {
918
+ if (error2 instanceof Error) {
911
919
  return {
912
920
  code,
913
921
  severity: "error",
914
922
  source,
915
- message: error.message
923
+ message: error2.message
916
924
  };
917
925
  }
918
926
  return {
919
927
  code,
920
928
  severity: "error",
921
929
  source,
922
- message: String(error)
930
+ message: String(error2)
923
931
  };
924
932
  }
925
933
 
926
934
  // packages/core/dist/scene.js
927
935
  var AU_IN_KM = 1495978707e-1;
928
936
  var EARTH_RADIUS_IN_KM = 6371;
937
+ var JUPITER_RADIUS_IN_KM = 71492;
929
938
  var SOLAR_RADIUS_IN_KM = 695700;
939
+ var LY_IN_AU = 63241.077;
940
+ var PC_IN_AU = 206264.806;
941
+ var KPC_IN_AU = 206264806;
930
942
  var ISO_FLATTENING = 0.68;
931
943
  var MIN_ISO_MINOR_SCALE = 0.2;
932
944
  var ARC_SAMPLE_COUNT = 28;
@@ -940,8 +952,10 @@ var WorldOrbit = (() => {
940
952
  const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
941
953
  const spacingFactor = layoutPresetSpacing(layoutPreset);
942
954
  const systemId = document2.system?.id ?? null;
943
- const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
944
- const relationships = buildSceneRelationships(document2.objects, objectMap);
955
+ const activeEventId = options.activeEventId ?? null;
956
+ const effectiveObjects = createEffectiveObjects(document2.objects, document2.events ?? [], activeEventId);
957
+ const objectMap = new Map(effectiveObjects.map((object) => [object.id, object]));
958
+ const relationships = buildSceneRelationships(effectiveObjects, objectMap);
945
959
  const positions = /* @__PURE__ */ new Map();
946
960
  const orbitDrafts = [];
947
961
  const leaderDrafts = [];
@@ -950,7 +964,7 @@ var WorldOrbit = (() => {
950
964
  const atObjects = [];
951
965
  const surfaceChildren = /* @__PURE__ */ new Map();
952
966
  const orbitChildren = /* @__PURE__ */ new Map();
953
- for (const object of document2.objects) {
967
+ for (const object of effectiveObjects) {
954
968
  const placement = object.placement;
955
969
  if (!placement) {
956
970
  rootObjects.push(object);
@@ -1045,11 +1059,14 @@ var WorldOrbit = (() => {
1045
1059
  const objects = [...positions.values()].map((position) => createSceneObject(position, scaleModel, relationships));
1046
1060
  const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
1047
1061
  const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
1048
- const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
1049
- const layers = createSceneLayers(orbitVisuals, leaders, objects, labels);
1050
- const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
1062
+ const labels = createSceneLabels(objects, width, height, scaleModel.labelMultiplier);
1063
+ const relations = createSceneRelations(document2, objects);
1064
+ const events = createSceneEvents(document2.events ?? [], objects, activeEventId);
1065
+ const layers = createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels);
1066
+ const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, scaleModel.labelMultiplier);
1067
+ const semanticGroups = createSceneSemanticGroups(document2, objects);
1051
1068
  const viewpoints = createSceneViewpoints(document2, projection, frame.preset, relationships, objectMap);
1052
- const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
1069
+ const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, scaleModel.labelMultiplier);
1053
1070
  return {
1054
1071
  width,
1055
1072
  height,
@@ -1057,7 +1074,7 @@ var WorldOrbit = (() => {
1057
1074
  renderPreset: frame.preset,
1058
1075
  projection,
1059
1076
  scaleModel,
1060
- title: String(document2.system?.properties.title ?? document2.system?.id ?? "WorldOrbit") || "WorldOrbit",
1077
+ title: String(document2.system?.title ?? document2.system?.properties.title ?? document2.system?.id ?? "WorldOrbit") || "WorldOrbit",
1061
1078
  subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
1062
1079
  systemId,
1063
1080
  viewMode: projection,
@@ -1073,13 +1090,46 @@ var WorldOrbit = (() => {
1073
1090
  contentBounds,
1074
1091
  layers,
1075
1092
  groups,
1093
+ semanticGroups,
1076
1094
  viewpoints,
1095
+ events,
1096
+ activeEventId,
1077
1097
  objects,
1078
1098
  orbitVisuals,
1099
+ relations,
1079
1100
  leaders,
1080
1101
  labels
1081
1102
  };
1082
1103
  }
1104
+ function createEffectiveObjects(objects, events, activeEventId) {
1105
+ const cloned = objects.map((object) => structuredClone(object));
1106
+ if (!activeEventId) {
1107
+ return cloned;
1108
+ }
1109
+ const activeEvent = events.find((event) => event.id === activeEventId);
1110
+ if (!activeEvent) {
1111
+ return cloned;
1112
+ }
1113
+ const objectMap = new Map(cloned.map((object) => [object.id, object]));
1114
+ for (const pose of activeEvent.positions) {
1115
+ const object = objectMap.get(pose.objectId);
1116
+ if (!object) {
1117
+ continue;
1118
+ }
1119
+ object.placement = pose.placement ? structuredClone(pose.placement) : null;
1120
+ if (pose.inner) {
1121
+ object.properties.inner = { ...pose.inner };
1122
+ } else {
1123
+ delete object.properties.inner;
1124
+ }
1125
+ if (pose.outer) {
1126
+ object.properties.outer = { ...pose.outer };
1127
+ } else {
1128
+ delete object.properties.outer;
1129
+ }
1130
+ }
1131
+ return cloned;
1132
+ }
1083
1133
  function resolveLayoutPreset(document2) {
1084
1134
  const rawScale = String(document2.system?.properties.scale ?? "balanced").toLowerCase();
1085
1135
  switch (rawScale) {
@@ -1174,6 +1224,7 @@ var WorldOrbit = (() => {
1174
1224
  }
1175
1225
  function createSceneObject(position, scaleModel, relationships) {
1176
1226
  const { object, x, y, radius, sortKey, anchorX, anchorY } = position;
1227
+ const renderPriority = object.renderHints?.renderPriority ?? 0;
1177
1228
  return {
1178
1229
  renderId: createRenderId(object.id),
1179
1230
  objectId: object.id,
@@ -1182,11 +1233,12 @@ var WorldOrbit = (() => {
1182
1233
  ancestorIds: relationships.ancestorIds.get(object.id) ?? [],
1183
1234
  childIds: relationships.childIds.get(object.id) ?? [],
1184
1235
  groupId: relationships.groupIds.get(object.id) ?? null,
1236
+ semanticGroupIds: [...object.groups ?? []],
1185
1237
  x,
1186
1238
  y,
1187
1239
  radius,
1188
1240
  visualRadius: visualExtentForObject(object, radius, scaleModel),
1189
- sortKey,
1241
+ sortKey: sortKey + renderPriority * 1e-3,
1190
1242
  anchorX,
1191
1243
  anchorY,
1192
1244
  label: object.id,
@@ -1203,6 +1255,7 @@ var WorldOrbit = (() => {
1203
1255
  object: draft.object,
1204
1256
  parentId: draft.parentId,
1205
1257
  groupId,
1258
+ semanticGroupIds: [...draft.object.groups ?? []],
1206
1259
  kind: draft.kind,
1207
1260
  cx: draft.cx,
1208
1261
  cy: draft.cy,
@@ -1214,7 +1267,7 @@ var WorldOrbit = (() => {
1214
1267
  bandThickness: draft.bandThickness,
1215
1268
  frontArcPath: draft.frontArcPath,
1216
1269
  backArcPath: draft.backArcPath,
1217
- hidden: draft.object.properties.hidden === true
1270
+ hidden: draft.object.properties.hidden === true || draft.object.renderHints?.renderOrbit === false
1218
1271
  };
1219
1272
  }
1220
1273
  function createLeaderLine(draft) {
@@ -1223,6 +1276,7 @@ var WorldOrbit = (() => {
1223
1276
  objectId: draft.object.id,
1224
1277
  object: draft.object,
1225
1278
  groupId: draft.groupId,
1279
+ semanticGroupIds: [...draft.object.groups ?? []],
1226
1280
  x1: draft.x1,
1227
1281
  y1: draft.y1,
1228
1282
  x2: draft.x2,
@@ -1231,42 +1285,144 @@ var WorldOrbit = (() => {
1231
1285
  hidden: draft.object.properties.hidden === true
1232
1286
  };
1233
1287
  }
1234
- function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1288
+ function createSceneLabels(objects, sceneWidth, sceneHeight, labelMultiplier) {
1235
1289
  const labels = [];
1236
1290
  const occupied = [];
1237
- const visibleObjects = [...objects].filter((object) => !object.hidden).sort((left, right) => left.sortKey - right.sortKey);
1291
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1292
+ const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort(compareLabelPlacementOrder);
1238
1293
  for (const object of visibleObjects) {
1239
- const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1240
- const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
1241
- let labelY = object.y + direction * (object.radius + 18 * labelMultiplier);
1242
- let secondaryY = labelY + direction * (16 * labelMultiplier);
1243
- let bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1244
- let attempts = 0;
1245
- while (occupied.some((entry) => rectsOverlap(entry, bounds)) && attempts < 10) {
1246
- labelY += direction * 14 * labelMultiplier;
1247
- secondaryY += direction * 14 * labelMultiplier;
1248
- bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1249
- attempts += 1;
1250
- }
1251
- occupied.push(bounds);
1294
+ const placement = selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) ?? createLabelPlacement(object, defaultVerticalDirection(object, objectMap.get(object.parentId ?? "") ?? null, sceneHeight), 0, labelMultiplier);
1295
+ occupied.push(createLabelRect(object, placement, labelMultiplier));
1252
1296
  labels.push({
1253
1297
  renderId: `${object.renderId}-label`,
1254
1298
  objectId: object.objectId,
1255
1299
  object: object.object,
1256
1300
  groupId: object.groupId,
1301
+ semanticGroupIds: [...object.semanticGroupIds],
1257
1302
  label: object.label,
1258
1303
  secondaryLabel: object.secondaryLabel,
1259
- x: object.x,
1260
- y: labelY,
1261
- secondaryY,
1262
- textAnchor: "middle",
1263
- direction: direction < 0 ? "above" : "below",
1304
+ x: placement.x,
1305
+ y: placement.labelY,
1306
+ secondaryY: placement.secondaryY,
1307
+ textAnchor: placement.textAnchor,
1308
+ direction: placement.direction,
1264
1309
  hidden: object.hidden
1265
1310
  });
1266
1311
  }
1267
1312
  return labels;
1268
1313
  }
1269
- function createSceneLayers(orbitVisuals, leaders, objects, labels) {
1314
+ function compareLabelPlacementOrder(left, right) {
1315
+ const priorityDiff = labelPlacementPriority(left) - labelPlacementPriority(right);
1316
+ if (priorityDiff !== 0) {
1317
+ return priorityDiff;
1318
+ }
1319
+ const renderPriorityDiff = (right.object.renderHints?.renderPriority ?? 0) - (left.object.renderHints?.renderPriority ?? 0);
1320
+ if (renderPriorityDiff !== 0) {
1321
+ return renderPriorityDiff;
1322
+ }
1323
+ return left.sortKey - right.sortKey;
1324
+ }
1325
+ function labelPlacementPriority(object) {
1326
+ switch (object.object.type) {
1327
+ case "star":
1328
+ return 0;
1329
+ case "planet":
1330
+ return 1;
1331
+ case "moon":
1332
+ return 2;
1333
+ case "belt":
1334
+ case "ring":
1335
+ return 3;
1336
+ case "asteroid":
1337
+ case "comet":
1338
+ return 4;
1339
+ case "structure":
1340
+ case "phenomenon":
1341
+ return 5;
1342
+ }
1343
+ }
1344
+ function selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) {
1345
+ for (const direction of preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight)) {
1346
+ const maxAttempts = direction === "left" || direction === "right" ? 4 : 6;
1347
+ for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
1348
+ const placement = createLabelPlacement(object, direction, attempt, labelMultiplier);
1349
+ const rect = createLabelRect(object, placement, labelMultiplier);
1350
+ if (!occupied.some((entry) => rectsOverlap(entry, rect))) {
1351
+ return placement;
1352
+ }
1353
+ }
1354
+ }
1355
+ return null;
1356
+ }
1357
+ function preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight) {
1358
+ const parent = object.parentId ? objectMap.get(object.parentId) ?? null : null;
1359
+ const vertical = defaultVerticalDirection(object, parent, sceneHeight);
1360
+ const oppositeVertical = vertical === "below" ? "above" : "below";
1361
+ const horizontal = defaultHorizontalDirection(object, parent, sceneWidth);
1362
+ const oppositeHorizontal = horizontal === "right" ? "left" : "right";
1363
+ 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";
1364
+ return preferHorizontal ? [horizontal, vertical, oppositeHorizontal, oppositeVertical] : [vertical, horizontal, oppositeVertical, oppositeHorizontal];
1365
+ }
1366
+ function defaultVerticalDirection(object, parent, sceneHeight) {
1367
+ if (parent && Math.abs(object.y - parent.y) > 6) {
1368
+ return object.y >= parent.y ? "below" : "above";
1369
+ }
1370
+ return object.y > sceneHeight * 0.62 ? "above" : "below";
1371
+ }
1372
+ function defaultHorizontalDirection(object, parent, sceneWidth) {
1373
+ if (parent && Math.abs(object.x - parent.x) > 6) {
1374
+ return object.x >= parent.x ? "right" : "left";
1375
+ }
1376
+ return object.x >= sceneWidth / 2 ? "right" : "left";
1377
+ }
1378
+ function createLabelPlacement(object, direction, attempt, labelMultiplier) {
1379
+ const step = 14 * labelMultiplier;
1380
+ switch (direction) {
1381
+ case "above": {
1382
+ const labelY = object.y - (object.radius + 18 * labelMultiplier + attempt * step);
1383
+ return {
1384
+ x: object.x,
1385
+ labelY,
1386
+ secondaryY: labelY - 16 * labelMultiplier,
1387
+ textAnchor: "middle",
1388
+ direction
1389
+ };
1390
+ }
1391
+ case "below": {
1392
+ const labelY = object.y + object.radius + 18 * labelMultiplier + attempt * step;
1393
+ return {
1394
+ x: object.x,
1395
+ labelY,
1396
+ secondaryY: labelY + 16 * labelMultiplier,
1397
+ textAnchor: "middle",
1398
+ direction
1399
+ };
1400
+ }
1401
+ case "left": {
1402
+ const x = object.x - (object.visualRadius + 16 * labelMultiplier + attempt * step);
1403
+ const labelY = object.y - 4 * labelMultiplier;
1404
+ return {
1405
+ x,
1406
+ labelY,
1407
+ secondaryY: labelY + 16 * labelMultiplier,
1408
+ textAnchor: "end",
1409
+ direction
1410
+ };
1411
+ }
1412
+ case "right": {
1413
+ const x = object.x + object.visualRadius + 16 * labelMultiplier + attempt * step;
1414
+ const labelY = object.y - 4 * labelMultiplier;
1415
+ return {
1416
+ x,
1417
+ labelY,
1418
+ secondaryY: labelY + 16 * labelMultiplier,
1419
+ textAnchor: "start",
1420
+ direction
1421
+ };
1422
+ }
1423
+ }
1424
+ }
1425
+ function createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels) {
1270
1426
  const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1271
1427
  const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1272
1428
  return [
@@ -1277,6 +1433,14 @@ var WorldOrbit = (() => {
1277
1433
  },
1278
1434
  { id: "orbits-back", renderIds: backOrbitIds },
1279
1435
  { id: "orbits-front", renderIds: frontOrbitIds },
1436
+ {
1437
+ id: "relations",
1438
+ renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId)
1439
+ },
1440
+ {
1441
+ id: "events",
1442
+ renderIds: events.filter((event) => !event.hidden).map((event) => event.renderId)
1443
+ },
1280
1444
  {
1281
1445
  id: "objects",
1282
1446
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
@@ -1288,7 +1452,7 @@ var WorldOrbit = (() => {
1288
1452
  { id: "metadata", renderIds: ["wo-title", "wo-subtitle", "wo-meta"] }
1289
1453
  ];
1290
1454
  }
1291
- function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships) {
1455
+ function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, labelMultiplier) {
1292
1456
  const groups = /* @__PURE__ */ new Map();
1293
1457
  const ensureGroup = (groupId) => {
1294
1458
  if (!groupId) {
@@ -1337,10 +1501,63 @@ var WorldOrbit = (() => {
1337
1501
  }
1338
1502
  }
1339
1503
  for (const group of groups.values()) {
1340
- group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels);
1504
+ group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier);
1341
1505
  }
1342
1506
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1343
1507
  }
1508
+ function createSceneSemanticGroups(document2, objects) {
1509
+ return [...document2.groups].map((group) => ({
1510
+ id: group.id,
1511
+ label: group.label,
1512
+ summary: group.summary,
1513
+ color: group.color,
1514
+ tags: [...group.tags],
1515
+ hidden: group.hidden,
1516
+ objectIds: objects.filter((object) => !object.hidden && object.semanticGroupIds.includes(group.id)).map((object) => object.objectId)
1517
+ })).sort((left, right) => left.label.localeCompare(right.label));
1518
+ }
1519
+ function createSceneRelations(document2, objects) {
1520
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1521
+ return document2.relations.map((relation) => {
1522
+ const from = objectMap.get(relation.from);
1523
+ const to = objectMap.get(relation.to);
1524
+ return {
1525
+ renderId: `${createRenderId(relation.id)}-relation`,
1526
+ relationId: relation.id,
1527
+ relation,
1528
+ fromObjectId: relation.from,
1529
+ toObjectId: relation.to,
1530
+ x1: from?.x ?? 0,
1531
+ y1: from?.y ?? 0,
1532
+ x2: to?.x ?? 0,
1533
+ y2: to?.y ?? 0,
1534
+ hidden: relation.hidden || !from || !to || from.hidden || to.hidden
1535
+ };
1536
+ }).sort((left, right) => left.relation.id.localeCompare(right.relation.id));
1537
+ }
1538
+ function createSceneEvents(events, objects, activeEventId) {
1539
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1540
+ return events.map((event) => {
1541
+ const objectIds = [.../* @__PURE__ */ new Set([
1542
+ ...event.targetObjectId ? [event.targetObjectId] : [],
1543
+ ...event.participantObjectIds
1544
+ ])];
1545
+ const positions = objectIds.map((objectId) => objectMap.get(objectId)).filter(Boolean);
1546
+ const centroidX = positions.length > 0 ? positions.reduce((sum, object) => sum + object.x, 0) / positions.length : 0;
1547
+ const centroidY = positions.length > 0 ? positions.reduce((sum, object) => sum + object.y, 0) / positions.length : 0;
1548
+ return {
1549
+ renderId: `${createRenderId(event.id)}-event`,
1550
+ eventId: event.id,
1551
+ event,
1552
+ objectIds,
1553
+ participantIds: [...event.participantObjectIds],
1554
+ targetObjectId: event.targetObjectId,
1555
+ x: centroidX,
1556
+ y: centroidY,
1557
+ hidden: event.hidden || positions.length === 0 || positions.every((object) => object.hidden) || activeEventId !== null && event.id !== activeEventId
1558
+ };
1559
+ }).sort((left, right) => left.event.id.localeCompare(right.event.id));
1560
+ }
1344
1561
  function createSceneViewpoints(document2, projection, preset, relationships, objectMap) {
1345
1562
  const generatedOverview = createGeneratedOverviewViewpoint(document2, projection, preset);
1346
1563
  const drafts = /* @__PURE__ */ new Map();
@@ -1358,7 +1575,7 @@ var WorldOrbit = (() => {
1358
1575
  }
1359
1576
  const field = fieldParts.join(".").toLowerCase();
1360
1577
  const draft = drafts.get(id) ?? { id };
1361
- applyViewpointField(draft, field, value, projection, preset, relationships, objectMap);
1578
+ applyViewpointField(draft, field, value, document2, projection, preset, relationships, objectMap);
1362
1579
  drafts.set(id, draft);
1363
1580
  }
1364
1581
  const viewpoints = [...drafts.values()].map((draft) => finalizeViewpointDraft(draft, projection, preset, objectMap)).filter(Boolean);
@@ -1386,13 +1603,15 @@ var WorldOrbit = (() => {
1386
1603
  });
1387
1604
  }
1388
1605
  function createGeneratedOverviewViewpoint(document2, projection, preset) {
1389
- const label = document2.system?.properties.title ? `${String(document2.system.properties.title)} Overview` : "Overview";
1606
+ const title = document2.system?.title ?? document2.system?.properties.title;
1607
+ const label = title ? `${String(title)} Overview` : "Overview";
1390
1608
  return {
1391
1609
  id: "overview",
1392
1610
  label,
1393
1611
  summary: "Fit the whole system with the current atlas defaults.",
1394
1612
  objectId: null,
1395
1613
  selectedObjectId: null,
1614
+ eventIds: [],
1396
1615
  projection,
1397
1616
  preset,
1398
1617
  rotationDeg: 0,
@@ -1402,7 +1621,7 @@ var WorldOrbit = (() => {
1402
1621
  generated: true
1403
1622
  };
1404
1623
  }
1405
- function applyViewpointField(draft, field, value, projection, preset, relationships, objectMap) {
1624
+ function applyViewpointField(draft, field, value, document2, projection, preset, relationships, objectMap) {
1406
1625
  const normalizedValue = value.trim();
1407
1626
  switch (field) {
1408
1627
  case "label":
@@ -1429,6 +1648,9 @@ var WorldOrbit = (() => {
1429
1648
  draft.select = normalizedValue;
1430
1649
  }
1431
1650
  return;
1651
+ case "events":
1652
+ draft.eventIds = splitListValue(normalizedValue);
1653
+ return;
1432
1654
  case "projection":
1433
1655
  case "view":
1434
1656
  draft.projection = parseViewProjection(normalizedValue) ?? projection;
@@ -1469,7 +1691,7 @@ var WorldOrbit = (() => {
1469
1691
  case "groups":
1470
1692
  draft.filter = {
1471
1693
  ...draft.filter ?? createEmptyViewpointFilter(),
1472
- groupIds: parseViewpointGroups(normalizedValue, relationships, objectMap)
1694
+ groupIds: parseViewpointGroups(normalizedValue, document2, relationships, objectMap)
1473
1695
  };
1474
1696
  return;
1475
1697
  }
@@ -1485,6 +1707,7 @@ var WorldOrbit = (() => {
1485
1707
  summary: draft.summary?.trim() || createViewpointSummary(label, objectId, filter),
1486
1708
  objectId,
1487
1709
  selectedObjectId,
1710
+ eventIds: [...new Set(draft.eventIds ?? [])],
1488
1711
  projection: draft.projection ?? projection,
1489
1712
  preset: draft.preset ?? preset,
1490
1713
  rotationDeg: draft.rotationDeg ?? 0,
@@ -1542,7 +1765,7 @@ var WorldOrbit = (() => {
1542
1765
  next["orbits-front"] = enabled;
1543
1766
  continue;
1544
1767
  }
1545
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1768
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "events" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1546
1769
  next[rawLayer] = enabled;
1547
1770
  }
1548
1771
  }
@@ -1551,8 +1774,11 @@ var WorldOrbit = (() => {
1551
1774
  function parseViewpointObjectTypes(value) {
1552
1775
  return splitListValue(value).filter((entry) => entry === "star" || entry === "planet" || entry === "moon" || entry === "belt" || entry === "asteroid" || entry === "comet" || entry === "ring" || entry === "structure" || entry === "phenomenon");
1553
1776
  }
1554
- function parseViewpointGroups(value, relationships, objectMap) {
1777
+ function parseViewpointGroups(value, document2, relationships, objectMap) {
1555
1778
  return splitListValue(value).map((entry) => {
1779
+ if (document2.schemaVersion === "2.1" || document2.groups.some((group) => group.id === entry)) {
1780
+ return entry;
1781
+ }
1556
1782
  if (entry.startsWith("wo-") && entry.endsWith("-group")) {
1557
1783
  return entry;
1558
1784
  }
@@ -1587,7 +1813,7 @@ var WorldOrbit = (() => {
1587
1813
  }
1588
1814
  return parts.join(" - ");
1589
1815
  }
1590
- function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels) {
1816
+ function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, labelMultiplier) {
1591
1817
  let minX = Number.POSITIVE_INFINITY;
1592
1818
  let minY = Number.POSITIVE_INFINITY;
1593
1819
  let maxX = Number.NEGATIVE_INFINITY;
@@ -1617,7 +1843,7 @@ var WorldOrbit = (() => {
1617
1843
  for (const label of labels) {
1618
1844
  if (label.hidden)
1619
1845
  continue;
1620
- includeLabelBounds(label, include);
1846
+ includeLabelBounds(label, include, labelMultiplier);
1621
1847
  }
1622
1848
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
1623
1849
  return createBounds(0, 0, width, height);
@@ -1655,13 +1881,10 @@ var WorldOrbit = (() => {
1655
1881
  include(object.x - object.visualRadius - 24, object.y - object.visualRadius - 16);
1656
1882
  include(object.x + object.visualRadius + 24, object.y + object.visualRadius + 36);
1657
1883
  }
1658
- function includeLabelBounds(label, include) {
1659
- const labelScale = 1;
1660
- const labelHalfWidth = estimateLabelHalfWidthFromText(label.label, label.secondaryLabel, labelScale);
1661
- include(label.x - labelHalfWidth, label.y - 18);
1662
- include(label.x + labelHalfWidth, label.y + 8);
1663
- include(label.x - labelHalfWidth, label.secondaryY - 14);
1664
- include(label.x + labelHalfWidth, label.secondaryY + 8);
1884
+ function includeLabelBounds(label, include, labelMultiplier) {
1885
+ const bounds = createLabelRectFromText(label.x, label.y, label.secondaryY, label.textAnchor, label.direction, label.label, label.secondaryLabel, labelMultiplier);
1886
+ include(bounds.left, bounds.top);
1887
+ include(bounds.right, bounds.bottom);
1665
1888
  }
1666
1889
  function placeObject(object, x, y, depth, positions, orbitDrafts, leaderDrafts, context) {
1667
1890
  if (positions.has(object.id)) {
@@ -1683,8 +1906,9 @@ var WorldOrbit = (() => {
1683
1906
  }
1684
1907
  const orbiting = [...context.orbitChildren.get(object.id) ?? []].sort(compareOrbiting);
1685
1908
  const orbitMetricContext = computeOrbitMetricContext(orbiting, parent.radius, context.spacingFactor, context.scaleModel);
1909
+ const orbitRadiiPx = resolveOrbitRadiiPx(orbiting, orbitMetricContext);
1686
1910
  orbiting.forEach((child, index) => {
1687
- const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, context);
1911
+ const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, orbitRadiiPx[index] ?? orbitMetricContext.innerPx, context);
1688
1912
  orbitDrafts.push({
1689
1913
  object: child,
1690
1914
  parentId: object.id,
@@ -1758,7 +1982,8 @@ var WorldOrbit = (() => {
1758
1982
  metricSpread: 0,
1759
1983
  innerPx,
1760
1984
  stepPx,
1761
- pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx)
1985
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
1986
+ minimumGapPx: stepPx * 0.42
1762
1987
  };
1763
1988
  }
1764
1989
  const minMetric = Math.min(...presentMetrics);
@@ -1771,10 +1996,11 @@ var WorldOrbit = (() => {
1771
1996
  metricSpread,
1772
1997
  innerPx,
1773
1998
  stepPx,
1774
- pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx)
1999
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
2000
+ minimumGapPx: stepPx * 0.42
1775
2001
  };
1776
2002
  }
1777
- function resolveOrbitGeometry(object, index, count, parent, metricContext, context) {
2003
+ function resolveOrbitGeometry(object, index, count, parent, metricContext, orbitRadiusPx, context) {
1778
2004
  const placement = object.placement;
1779
2005
  const band = object.type === "belt" || object.type === "ring";
1780
2006
  if (!placement || placement.mode !== "orbit") {
@@ -1792,7 +2018,7 @@ var WorldOrbit = (() => {
1792
2018
  };
1793
2019
  }
1794
2020
  const eccentricity = clampNumber(typeof placement.eccentricity === "number" ? placement.eccentricity : 0, 0, 0.92);
1795
- const semiMajor = resolveOrbitRadiusPx(object, index, metricContext);
2021
+ const semiMajor = orbitRadiusPx;
1796
2022
  const baseMinor = Math.max(semiMajor * Math.sqrt(1 - eccentricity * eccentricity), semiMajor * 0.18);
1797
2023
  const inclinationDeg = unitValueToDegrees(placement.inclination) ?? 0;
1798
2024
  const inclinationScale = context.projection === "isometric" ? Math.max(MIN_ISO_MINOR_SCALE, Math.cos(degreesToRadians(inclinationDeg))) * ISO_FLATTENING : 1;
@@ -1822,15 +2048,19 @@ var WorldOrbit = (() => {
1822
2048
  objectY: objectPoint.y
1823
2049
  };
1824
2050
  }
1825
- function resolveOrbitRadiusPx(object, index, metricContext) {
1826
- const metric = orbitMetric(object);
1827
- if (metric === null) {
1828
- return metricContext.innerPx + index * metricContext.stepPx;
1829
- }
1830
- if (metricContext.metricSpread > 0) {
1831
- return metricContext.innerPx + (metric - metricContext.minMetric) / metricContext.metricSpread * metricContext.pixelSpread;
1832
- }
1833
- return metricContext.innerPx + Math.log10(metric + 1) * metricContext.stepPx;
2051
+ function resolveOrbitRadiusPx(metric, metricContext) {
2052
+ return metricContext.innerPx + metricContext.stepPx * log2(Math.max(metric, 0) + 1);
2053
+ }
2054
+ function resolveOrbitRadiiPx(objects, metricContext) {
2055
+ const radii = [];
2056
+ objects.forEach((object, index) => {
2057
+ const metric = orbitMetric(object);
2058
+ const fallbackRadius = metricContext.innerPx + index * metricContext.stepPx;
2059
+ const baseRadius = metric === null ? fallbackRadius : resolveOrbitRadiusPx(metric, metricContext);
2060
+ const minimumRadius = index === 0 ? metricContext.innerPx : (radii[index - 1] ?? metricContext.innerPx) + metricContext.minimumGapPx;
2061
+ radii.push(Math.max(baseRadius, minimumRadius));
2062
+ });
2063
+ return radii;
1834
2064
  }
1835
2065
  function orbitMetric(object) {
1836
2066
  if (!object.placement || object.placement.mode !== "orbit") {
@@ -1838,6 +2068,9 @@ var WorldOrbit = (() => {
1838
2068
  }
1839
2069
  return toDistanceMetric(object.placement.semiMajor ?? object.placement.distance ?? null);
1840
2070
  }
2071
+ function log2(value) {
2072
+ return Math.log(value) / Math.log(2);
2073
+ }
1841
2074
  function resolveOrbitPhase(phase, index, count) {
1842
2075
  const degreeValue = phase ? unitValueToDegrees(phase) : null;
1843
2076
  if (degreeValue !== null) {
@@ -2041,7 +2274,7 @@ var WorldOrbit = (() => {
2041
2274
  return null;
2042
2275
  }
2043
2276
  }
2044
- function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels) {
2277
+ function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier) {
2045
2278
  let minX = Number.POSITIVE_INFINITY;
2046
2279
  let minY = Number.POSITIVE_INFINITY;
2047
2280
  let maxX = Number.NEGATIVE_INFINITY;
@@ -2070,7 +2303,7 @@ var WorldOrbit = (() => {
2070
2303
  }
2071
2304
  for (const label of labels) {
2072
2305
  if (!label.hidden && group.labelIds.includes(label.objectId)) {
2073
- includeLabelBounds(label, include);
2306
+ includeLabelBounds(label, include, labelMultiplier);
2074
2307
  }
2075
2308
  }
2076
2309
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
@@ -2095,12 +2328,28 @@ var WorldOrbit = (() => {
2095
2328
  }
2096
2329
  return current.id;
2097
2330
  }
2098
- function createLabelRect(x, labelY, secondaryY, labelHalfWidth, direction) {
2331
+ function createLabelRect(object, placement, labelMultiplier) {
2332
+ return createLabelRectFromText(placement.x, placement.labelY, placement.secondaryY, placement.textAnchor, placement.direction, object.label, object.secondaryLabel, labelMultiplier);
2333
+ }
2334
+ function createLabelRectFromText(x, labelY, secondaryY, textAnchor, direction, label, secondaryLabel, labelMultiplier) {
2335
+ const labelHalfWidth = estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier);
2336
+ const labelWidth = labelHalfWidth * 2;
2337
+ const topPadding = direction === "above" ? 18 : 12;
2338
+ const bottomPadding = direction === "above" ? 8 : 12;
2339
+ let left = x - labelHalfWidth;
2340
+ let right = x + labelHalfWidth;
2341
+ if (textAnchor === "start") {
2342
+ left = x;
2343
+ right = x + labelWidth;
2344
+ } else if (textAnchor === "end") {
2345
+ left = x - labelWidth;
2346
+ right = x;
2347
+ }
2099
2348
  return {
2100
- left: x - labelHalfWidth,
2101
- right: x + labelHalfWidth,
2102
- top: Math.min(labelY, secondaryY) - (direction < 0 ? 18 : 12),
2103
- bottom: Math.max(labelY, secondaryY) + (direction < 0 ? 8 : 12)
2349
+ left,
2350
+ right,
2351
+ top: Math.min(labelY, secondaryY) - topPadding,
2352
+ bottom: Math.max(labelY, secondaryY) + bottomPadding
2104
2353
  };
2105
2354
  }
2106
2355
  function rectsOverlap(left, right) {
@@ -2161,8 +2410,18 @@ var WorldOrbit = (() => {
2161
2410
  return value.value;
2162
2411
  case "km":
2163
2412
  return value.value / AU_IN_KM;
2413
+ case "m":
2414
+ return value.value / 1e3 / AU_IN_KM;
2415
+ case "ly":
2416
+ return value.value * LY_IN_AU;
2417
+ case "pc":
2418
+ return value.value * PC_IN_AU;
2419
+ case "kpc":
2420
+ return value.value * KPC_IN_AU;
2164
2421
  case "re":
2165
2422
  return value.value * EARTH_RADIUS_IN_KM / AU_IN_KM;
2423
+ case "rj":
2424
+ return value.value * JUPITER_RADIUS_IN_KM / AU_IN_KM;
2166
2425
  case "sol":
2167
2426
  return value.value * SOLAR_RADIUS_IN_KM / AU_IN_KM;
2168
2427
  default:
@@ -2277,11 +2536,6 @@ var WorldOrbit = (() => {
2277
2536
  function customColorFor(value) {
2278
2537
  return typeof value === "string" && value.trim() ? value : void 0;
2279
2538
  }
2280
- function estimateLabelHalfWidth(object, labelMultiplier) {
2281
- const primaryWidth = object.label.length * 4.6 * labelMultiplier + 18;
2282
- const secondaryWidth = object.secondaryLabel.length * 3.9 * labelMultiplier + 18;
2283
- return Math.max(primaryWidth, secondaryWidth, object.visualRadius + 18);
2284
- }
2285
2539
  function estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier) {
2286
2540
  const primaryWidth = label.length * 4.6 * labelMultiplier + 18;
2287
2541
  const secondaryWidth = secondaryLabel.length * 3.9 * labelMultiplier + 18;
@@ -2298,28 +2552,95 @@ var WorldOrbit = (() => {
2298
2552
  }
2299
2553
 
2300
2554
  // packages/core/dist/draft.js
2301
- function materializeAtlasDocument(document2) {
2555
+ function materializeAtlasDocument(document2, options = {}) {
2302
2556
  const system = document2.system ? {
2303
2557
  type: "system",
2304
2558
  id: document2.system.id,
2559
+ title: document2.system.title,
2560
+ description: document2.system.description,
2561
+ epoch: document2.system.epoch,
2562
+ referencePlane: document2.system.referencePlane,
2305
2563
  properties: materializeDraftSystemProperties(document2.system),
2306
2564
  info: materializeDraftSystemInfo(document2.system)
2307
2565
  } : null;
2566
+ const objects = document2.objects.map(cloneWorldOrbitObject);
2567
+ applyEventPoseOverrides(objects, document2.events ?? [], options.activeEventId ?? null);
2308
2568
  return {
2309
2569
  format: "worldorbit",
2310
2570
  version: "1.0",
2571
+ schemaVersion: document2.version,
2311
2572
  system,
2312
- objects: document2.objects.map(cloneWorldOrbitObject)
2573
+ groups: structuredClone(document2.groups ?? []),
2574
+ relations: structuredClone(document2.relations ?? []),
2575
+ events: document2.events.map(cloneWorldOrbitEvent),
2576
+ objects
2313
2577
  };
2314
2578
  }
2315
2579
  function cloneWorldOrbitObject(object) {
2316
2580
  return {
2317
2581
  ...object,
2582
+ groups: object.groups ? [...object.groups] : void 0,
2583
+ resonance: object.resonance ? { ...object.resonance } : object.resonance,
2584
+ renderHints: object.renderHints ? { ...object.renderHints } : object.renderHints,
2585
+ deriveRules: object.deriveRules ? object.deriveRules.map((rule) => ({ ...rule })) : void 0,
2586
+ validationRules: object.validationRules ? object.validationRules.map((rule) => ({ ...rule })) : void 0,
2587
+ lockedFields: object.lockedFields ? [...object.lockedFields] : void 0,
2588
+ tolerances: object.tolerances ? object.tolerances.map((entry) => ({
2589
+ field: entry.field,
2590
+ 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
2591
+ })) : void 0,
2592
+ typedBlocks: object.typedBlocks ? Object.fromEntries(Object.entries(object.typedBlocks).map(([key, block]) => [key, { ...block ?? {} }])) : void 0,
2318
2593
  properties: cloneProperties(object.properties),
2319
2594
  placement: object.placement ? structuredClone(object.placement) : null,
2320
2595
  info: { ...object.info }
2321
2596
  };
2322
2597
  }
2598
+ function cloneWorldOrbitEvent(event) {
2599
+ return {
2600
+ ...event,
2601
+ participantObjectIds: [...event.participantObjectIds],
2602
+ tags: [...event.tags],
2603
+ positions: event.positions.map(cloneWorldOrbitEventPose)
2604
+ };
2605
+ }
2606
+ function cloneWorldOrbitEventPose(pose) {
2607
+ return {
2608
+ objectId: pose.objectId,
2609
+ placement: clonePlacement(pose.placement),
2610
+ inner: pose.inner ? { ...pose.inner } : void 0,
2611
+ outer: pose.outer ? { ...pose.outer } : void 0
2612
+ };
2613
+ }
2614
+ function clonePlacement(placement) {
2615
+ return placement ? structuredClone(placement) : null;
2616
+ }
2617
+ function applyEventPoseOverrides(objects, events, activeEventId) {
2618
+ if (!activeEventId) {
2619
+ return;
2620
+ }
2621
+ const event = events.find((entry) => entry.id === activeEventId);
2622
+ if (!event) {
2623
+ return;
2624
+ }
2625
+ const objectMap = new Map(objects.map((object) => [object.id, object]));
2626
+ for (const pose of event.positions) {
2627
+ const object = objectMap.get(pose.objectId);
2628
+ if (!object) {
2629
+ continue;
2630
+ }
2631
+ object.placement = clonePlacement(pose.placement);
2632
+ if (pose.inner) {
2633
+ object.properties.inner = { ...pose.inner };
2634
+ } else {
2635
+ delete object.properties.inner;
2636
+ }
2637
+ if (pose.outer) {
2638
+ object.properties.outer = { ...pose.outer };
2639
+ } else {
2640
+ delete object.properties.outer;
2641
+ }
2642
+ }
2643
+ }
2323
2644
  function cloneProperties(properties) {
2324
2645
  const next = {};
2325
2646
  for (const [key, value] of Object.entries(properties)) {
@@ -2350,71 +2671,83 @@ var WorldOrbit = (() => {
2350
2671
  if (system.defaults.units) {
2351
2672
  properties.units = system.defaults.units;
2352
2673
  }
2674
+ if (system.description) {
2675
+ properties.description = system.description;
2676
+ }
2677
+ if (system.epoch) {
2678
+ properties.epoch = system.epoch;
2679
+ }
2680
+ if (system.referencePlane) {
2681
+ properties.referencePlane = system.referencePlane;
2682
+ }
2353
2683
  return properties;
2354
2684
  }
2355
2685
  function materializeDraftSystemInfo(system) {
2356
- const info = {
2686
+ const info2 = {
2357
2687
  ...system.atlasMetadata
2358
2688
  };
2359
2689
  if (system.defaults.theme) {
2360
- info["atlas.theme"] = system.defaults.theme;
2690
+ info2["atlas.theme"] = system.defaults.theme;
2361
2691
  }
2362
2692
  for (const viewpoint of system.viewpoints) {
2363
2693
  const prefix = `viewpoint.${viewpoint.id}`;
2364
- info[`${prefix}.label`] = viewpoint.label;
2694
+ info2[`${prefix}.label`] = viewpoint.label;
2365
2695
  if (viewpoint.summary) {
2366
- info[`${prefix}.summary`] = viewpoint.summary;
2696
+ info2[`${prefix}.summary`] = viewpoint.summary;
2367
2697
  }
2368
2698
  if (viewpoint.focusObjectId) {
2369
- info[`${prefix}.focus`] = viewpoint.focusObjectId;
2699
+ info2[`${prefix}.focus`] = viewpoint.focusObjectId;
2370
2700
  }
2371
2701
  if (viewpoint.selectedObjectId) {
2372
- info[`${prefix}.select`] = viewpoint.selectedObjectId;
2702
+ info2[`${prefix}.select`] = viewpoint.selectedObjectId;
2373
2703
  }
2374
2704
  if (viewpoint.projection) {
2375
- info[`${prefix}.projection`] = viewpoint.projection;
2705
+ info2[`${prefix}.projection`] = viewpoint.projection;
2376
2706
  }
2377
2707
  if (viewpoint.preset) {
2378
- info[`${prefix}.preset`] = viewpoint.preset;
2708
+ info2[`${prefix}.preset`] = viewpoint.preset;
2379
2709
  }
2380
2710
  if (viewpoint.zoom !== null) {
2381
- info[`${prefix}.zoom`] = String(viewpoint.zoom);
2711
+ info2[`${prefix}.zoom`] = String(viewpoint.zoom);
2382
2712
  }
2383
2713
  if (viewpoint.rotationDeg !== 0) {
2384
- info[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2714
+ info2[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2385
2715
  }
2386
2716
  const serializedLayers = serializeViewpointLayers(viewpoint.layers);
2387
2717
  if (serializedLayers) {
2388
- info[`${prefix}.layers`] = serializedLayers;
2718
+ info2[`${prefix}.layers`] = serializedLayers;
2389
2719
  }
2390
2720
  if (viewpoint.filter?.query) {
2391
- info[`${prefix}.query`] = viewpoint.filter.query;
2721
+ info2[`${prefix}.query`] = viewpoint.filter.query;
2392
2722
  }
2393
2723
  if ((viewpoint.filter?.objectTypes.length ?? 0) > 0) {
2394
- info[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
2724
+ info2[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
2395
2725
  }
2396
2726
  if ((viewpoint.filter?.tags.length ?? 0) > 0) {
2397
- info[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
2727
+ info2[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
2398
2728
  }
2399
2729
  if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2400
- info[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2730
+ info2[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2731
+ }
2732
+ if (viewpoint.events.length > 0) {
2733
+ info2[`${prefix}.events`] = viewpoint.events.join(" ");
2401
2734
  }
2402
2735
  }
2403
2736
  for (const annotation of system.annotations) {
2404
2737
  const prefix = `annotation.${annotation.id}`;
2405
- info[`${prefix}.label`] = annotation.label;
2738
+ info2[`${prefix}.label`] = annotation.label;
2406
2739
  if (annotation.targetObjectId) {
2407
- info[`${prefix}.target`] = annotation.targetObjectId;
2740
+ info2[`${prefix}.target`] = annotation.targetObjectId;
2408
2741
  }
2409
- info[`${prefix}.body`] = annotation.body;
2742
+ info2[`${prefix}.body`] = annotation.body;
2410
2743
  if (annotation.tags.length > 0) {
2411
- info[`${prefix}.tags`] = annotation.tags.join(" ");
2744
+ info2[`${prefix}.tags`] = annotation.tags.join(" ");
2412
2745
  }
2413
2746
  if (annotation.sourceObjectId) {
2414
- info[`${prefix}.source`] = annotation.sourceObjectId;
2747
+ info2[`${prefix}.source`] = annotation.sourceObjectId;
2415
2748
  }
2416
2749
  }
2417
- return info;
2750
+ return info2;
2418
2751
  }
2419
2752
  function serializeViewpointLayers(layers) {
2420
2753
  const tokens = [];
@@ -2423,7 +2756,7 @@ var WorldOrbit = (() => {
2423
2756
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2424
2757
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2425
2758
  }
2426
- for (const key of ["background", "guides", "objects", "labels", "metadata"]) {
2759
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
2427
2760
  if (layers[key] !== void 0) {
2428
2761
  tokens.push(layers[key] ? key : `-${key}`);
2429
2762
  }
@@ -2431,171 +2764,838 @@ var WorldOrbit = (() => {
2431
2764
  return tokens.join(" ");
2432
2765
  }
2433
2766
 
2434
- // packages/core/dist/draft-parse.js
2435
- function parseWorldOrbitAtlas(source) {
2436
- return parseAtlasSource(source, "2.0");
2767
+ // packages/core/dist/atlas-utils.js
2768
+ 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)?$/;
2769
+ var BOOLEAN_VALUES2 = /* @__PURE__ */ new Map([
2770
+ ["true", true],
2771
+ ["false", false],
2772
+ ["yes", true],
2773
+ ["no", false]
2774
+ ]);
2775
+ var URL_SCHEME_PATTERN2 = /^[A-Za-z][A-Za-z0-9+.-]*:/;
2776
+ function normalizeIdentifier(value) {
2777
+ return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
2437
2778
  }
2438
- function parseAtlasSource(source, outputVersion) {
2439
- const lines = source.split(/\r?\n/);
2440
- let sawSchemaHeader = false;
2441
- let schemaVersion = "2.0";
2442
- let system = null;
2443
- let section = null;
2444
- const objectNodes = [];
2445
- let sawDefaults = false;
2446
- let sawAtlas = false;
2447
- const viewpointIds = /* @__PURE__ */ new Set();
2448
- const annotationIds = /* @__PURE__ */ new Set();
2449
- for (let index = 0; index < lines.length; index++) {
2450
- const rawLine = lines[index];
2451
- const lineNumber = index + 1;
2452
- if (!rawLine.trim()) {
2453
- continue;
2454
- }
2455
- const indent = getIndent(rawLine);
2456
- const tokens = tokenizeLineDetailed(rawLine.slice(indent), {
2457
- line: lineNumber,
2458
- columnOffset: indent
2459
- });
2460
- if (tokens.length === 0) {
2461
- continue;
2462
- }
2463
- if (!sawSchemaHeader) {
2464
- schemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
2465
- sawSchemaHeader = true;
2466
- continue;
2467
- }
2468
- if (indent === 0) {
2469
- section = startTopLevelSection(tokens, lineNumber, system, objectNodes, viewpointIds, annotationIds, {
2470
- sawDefaults,
2471
- sawAtlas
2472
- });
2473
- if (section.kind === "system") {
2474
- system = section.system;
2475
- } else if (section.kind === "defaults") {
2476
- sawDefaults = true;
2477
- } else if (section.kind === "atlas") {
2478
- sawAtlas = true;
2479
- }
2480
- continue;
2481
- }
2482
- if (!section) {
2483
- throw new WorldOrbitError("Indented line without parent atlas section", lineNumber, indent + 1);
2484
- }
2485
- handleSectionLine(section, indent, tokens, lineNumber);
2486
- }
2487
- if (!sawSchemaHeader) {
2488
- throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
2779
+ function humanizeIdentifier2(value) {
2780
+ return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
2781
+ }
2782
+ function parseAtlasUnitValue(input, location, fieldKey) {
2783
+ const match = input.match(UNIT_PATTERN2);
2784
+ if (!match) {
2785
+ throw WorldOrbitError.fromLocation(`Invalid unit value "${input}"`, location);
2489
2786
  }
2490
- const ast = {
2491
- type: "document",
2492
- objects: objectNodes
2787
+ const unitValue = {
2788
+ value: Number(match[1]),
2789
+ unit: match[2] ?? null
2493
2790
  };
2494
- const normalizedObjects = normalizeDocument(ast).objects;
2495
- validateDocument({
2496
- format: "worldorbit",
2497
- version: "1.0",
2498
- system: null,
2499
- objects: normalizedObjects
2500
- });
2501
- const diagnostics = schemaVersion === "2.0-draft" && outputVersion === "2.0" ? [
2502
- {
2503
- code: "load.schema.deprecatedDraft",
2504
- severity: "warning",
2505
- source: "upgrade",
2506
- message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".'
2791
+ if (fieldKey) {
2792
+ const schema = getFieldSchema(fieldKey);
2793
+ if (schema?.unitFamily && !unitFamilyAllowsUnit(schema.unitFamily, unitValue.unit)) {
2794
+ throw WorldOrbitError.fromLocation(`Unit "${unitValue.unit ?? "none"}" is not valid for "${fieldKey}"`, location);
2507
2795
  }
2508
- ] : [];
2796
+ }
2797
+ return unitValue;
2798
+ }
2799
+ function tryParseAtlasUnitValue(input) {
2800
+ const match = input.match(UNIT_PATTERN2);
2801
+ if (!match) {
2802
+ return null;
2803
+ }
2509
2804
  return {
2510
- format: "worldorbit",
2511
- version: outputVersion,
2512
- sourceVersion: "1.0",
2513
- system,
2514
- objects: normalizedObjects,
2515
- diagnostics
2805
+ value: Number(match[1]),
2806
+ unit: match[2] ?? null
2516
2807
  };
2517
2808
  }
2518
- function assertDraftSchemaHeader(tokens, line) {
2519
- if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || tokens[1].value.toLowerCase() !== "2.0-draft" && tokens[1].value.toLowerCase() !== "2.0") {
2520
- throw new WorldOrbitError('Expected atlas header "schema 2.0" or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
2809
+ function parseAtlasNumber(input, key, location) {
2810
+ const value = Number(input);
2811
+ if (!Number.isFinite(value)) {
2812
+ throw WorldOrbitError.fromLocation(`Invalid numeric value "${input}" for "${key}"`, location);
2521
2813
  }
2522
- return tokens[1].value.toLowerCase() === "2.0-draft" ? "2.0-draft" : "2.0";
2814
+ return value;
2523
2815
  }
2524
- function startTopLevelSection(tokens, line, system, objectNodes, viewpointIds, annotationIds, flags) {
2525
- const keyword = tokens[0]?.value.toLowerCase();
2526
- switch (keyword) {
2527
- case "system":
2528
- if (system) {
2529
- throw new WorldOrbitError('Atlas section "system" may only appear once', line, tokens[0].column);
2530
- }
2531
- return startSystemSection(tokens, line);
2532
- case "defaults":
2533
- if (!system) {
2534
- throw new WorldOrbitError('Atlas section "defaults" requires a preceding system declaration', line, tokens[0].column);
2535
- }
2536
- if (flags.sawDefaults) {
2537
- throw new WorldOrbitError('Atlas section "defaults" may only appear once', line, tokens[0].column);
2538
- }
2539
- return {
2540
- kind: "defaults",
2541
- system,
2542
- seenFields: /* @__PURE__ */ new Set()
2543
- };
2544
- case "atlas":
2545
- if (!system) {
2546
- throw new WorldOrbitError('Atlas section "atlas" requires a preceding system declaration', line, tokens[0].column);
2547
- }
2548
- if (flags.sawAtlas) {
2549
- throw new WorldOrbitError('Atlas section "atlas" may only appear once', line, tokens[0].column);
2550
- }
2551
- return {
2552
- kind: "atlas",
2553
- system,
2554
- inMetadata: false,
2555
- metadataIndent: null
2556
- };
2557
- case "viewpoint":
2558
- if (!system) {
2559
- throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
2560
- }
2561
- return startViewpointSection(tokens, line, system, viewpointIds);
2562
- case "annotation":
2563
- if (!system) {
2564
- throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
2565
- }
2566
- return startAnnotationSection(tokens, line, system, annotationIds);
2567
- case "object":
2568
- return startObjectSection(tokens, line, objectNodes);
2569
- default:
2570
- throw new WorldOrbitError(`Unknown atlas section "${tokens[0]?.value ?? ""}"`, line, tokens[0]?.column ?? 1);
2816
+ function parseAtlasBoolean(input, key, location) {
2817
+ const parsed = BOOLEAN_VALUES2.get(input.toLowerCase());
2818
+ if (parsed === void 0) {
2819
+ throw WorldOrbitError.fromLocation(`Invalid boolean value "${input}" for "${key}"`, location);
2571
2820
  }
2821
+ return parsed;
2572
2822
  }
2573
- function startSystemSection(tokens, line) {
2574
- if (tokens.length !== 2) {
2575
- throw new WorldOrbitError("Invalid atlas system declaration", line, tokens[0]?.column ?? 1);
2823
+ function parseAtlasAtReference(target, location) {
2824
+ if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
2825
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
2576
2826
  }
2577
- const system = {
2578
- type: "system",
2579
- id: tokens[1].value,
2580
- title: null,
2581
- defaults: {
2582
- view: "topdown",
2583
- scale: null,
2584
- units: null,
2585
- preset: null,
2586
- theme: null
2587
- },
2588
- atlasMetadata: {},
2589
- viewpoints: [],
2590
- annotations: []
2591
- };
2592
- return {
2593
- kind: "system",
2594
- system,
2827
+ const pairedMatch = target.match(/^([A-Za-z0-9._-]+)-([A-Za-z0-9._-]+):(L[1-5])$/);
2828
+ if (pairedMatch) {
2829
+ return {
2830
+ kind: "lagrange",
2831
+ primary: pairedMatch[1],
2832
+ secondary: pairedMatch[2],
2833
+ point: pairedMatch[3]
2834
+ };
2835
+ }
2836
+ const simpleMatch = target.match(/^([A-Za-z0-9._-]+):(L[1-5])$/);
2837
+ if (simpleMatch) {
2838
+ return {
2839
+ kind: "lagrange",
2840
+ primary: simpleMatch[1],
2841
+ secondary: null,
2842
+ point: simpleMatch[2]
2843
+ };
2844
+ }
2845
+ if (/^[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
2846
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
2847
+ }
2848
+ const anchorMatch = target.match(/^([A-Za-z0-9._-]+):([A-Za-z0-9._-]+)$/);
2849
+ if (anchorMatch) {
2850
+ return {
2851
+ kind: "anchor",
2852
+ objectId: anchorMatch[1],
2853
+ anchor: anchorMatch[2]
2854
+ };
2855
+ }
2856
+ return {
2857
+ kind: "named",
2858
+ name: target
2859
+ };
2860
+ }
2861
+ function validateAtlasImageSource(value, location) {
2862
+ if (!value) {
2863
+ throw WorldOrbitError.fromLocation('Field "image" must not be empty', location);
2864
+ }
2865
+ if (value.startsWith("//")) {
2866
+ throw WorldOrbitError.fromLocation('Field "image" must use a relative path, root-relative path, or an http/https URL', location);
2867
+ }
2868
+ const schemeMatch = value.match(URL_SCHEME_PATTERN2);
2869
+ if (!schemeMatch) {
2870
+ return;
2871
+ }
2872
+ const scheme = schemeMatch[0].slice(0, -1).toLowerCase();
2873
+ if (scheme !== "http" && scheme !== "https") {
2874
+ throw WorldOrbitError.fromLocation(`Field "image" does not support the "${scheme}" scheme`, location);
2875
+ }
2876
+ }
2877
+ function normalizeLegacyScalarValue(key, values, location) {
2878
+ const schema = getFieldSchema(key);
2879
+ if (!schema) {
2880
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
2881
+ }
2882
+ if (schema.arity === "single" && values.length !== 1) {
2883
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
2884
+ }
2885
+ switch (schema.kind) {
2886
+ case "list":
2887
+ return values;
2888
+ case "boolean":
2889
+ return parseAtlasBoolean(singleAtlasValue(values, key, location), key, location);
2890
+ case "number":
2891
+ return parseAtlasNumber(singleAtlasValue(values, key, location), key, location);
2892
+ case "unit":
2893
+ return parseAtlasUnitValue(singleAtlasValue(values, key, location), location, key);
2894
+ case "string": {
2895
+ const value = values.join(" ").trim();
2896
+ if (key === "image") {
2897
+ validateAtlasImageSource(value, location);
2898
+ }
2899
+ return value;
2900
+ }
2901
+ }
2902
+ }
2903
+ function ensureAtlasFieldSupported(key, objectType, location) {
2904
+ const schema = getFieldSchema(key);
2905
+ if (!schema) {
2906
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
2907
+ }
2908
+ if (!schema.objectTypes.includes(objectType)) {
2909
+ throw WorldOrbitError.fromLocation(`Field "${key}" is not valid on "${objectType}"`, location);
2910
+ }
2911
+ }
2912
+ function singleAtlasValue(values, key, location) {
2913
+ if (values.length !== 1) {
2914
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
2915
+ }
2916
+ return values[0];
2917
+ }
2918
+
2919
+ // packages/core/dist/atlas-validate.js
2920
+ var SURFACE_TARGET_TYPES2 = /* @__PURE__ */ new Set(["star", "planet", "moon", "asteroid", "comet"]);
2921
+ var EARTH_MASSES_PER_SOLAR = 332946.0487;
2922
+ var JUPITER_MASSES_PER_SOLAR = 1047.3486;
2923
+ var AU_IN_KM2 = 1495978707e-1;
2924
+ var EARTH_RADIUS_IN_KM2 = 6371;
2925
+ var SOLAR_RADIUS_IN_KM2 = 695700;
2926
+ var LY_IN_AU2 = 63241.077;
2927
+ var PC_IN_AU2 = 206264.806;
2928
+ var KPC_IN_AU2 = 206264806;
2929
+ function collectAtlasDiagnostics(document2, sourceSchemaVersion) {
2930
+ const diagnostics = [];
2931
+ const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
2932
+ const groupIds = new Set(document2.groups.map((group) => group.id));
2933
+ const eventIds = new Set(document2.events.map((event) => event.id));
2934
+ if (!document2.system) {
2935
+ diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
2936
+ }
2937
+ const knownIds = /* @__PURE__ */ new Map();
2938
+ for (const [kind, ids] of [
2939
+ ["group", document2.groups.map((group) => group.id)],
2940
+ ["viewpoint", document2.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
2941
+ ["annotation", document2.system?.annotations.map((annotation) => annotation.id) ?? []],
2942
+ ["relation", document2.relations.map((relation) => relation.id)],
2943
+ ["event", document2.events.map((event) => event.id)],
2944
+ ["object", document2.objects.map((object) => object.id)]
2945
+ ]) {
2946
+ for (const id of ids) {
2947
+ const previous = knownIds.get(id);
2948
+ if (previous) {
2949
+ diagnostics.push(error("validate.id.duplicate", `Duplicate ${kind} id "${id}" already used by ${previous}.`));
2950
+ } else {
2951
+ knownIds.set(id, kind);
2952
+ }
2953
+ }
2954
+ }
2955
+ for (const relation of document2.relations) {
2956
+ validateRelation(relation, objectMap, diagnostics);
2957
+ }
2958
+ for (const viewpoint of document2.system?.viewpoints ?? []) {
2959
+ validateViewpoint(viewpoint.filter, viewpoint.events ?? [], groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpoint.id);
2960
+ }
2961
+ for (const object of document2.objects) {
2962
+ validateObject(object, document2.system, objectMap, groupIds, diagnostics);
2963
+ }
2964
+ for (const event of document2.events) {
2965
+ validateEvent(event, objectMap, diagnostics);
2966
+ }
2967
+ return diagnostics;
2968
+ }
2969
+ function validateRelation(relation, objectMap, diagnostics) {
2970
+ if (!relation.from) {
2971
+ diagnostics.push(error("validate.relation.from.required", `Relation "${relation.id}" is missing a "from" target.`));
2972
+ } else if (!objectMap.has(relation.from)) {
2973
+ diagnostics.push(error("validate.relation.from.unknown", `Unknown relation source "${relation.from}" on "${relation.id}".`));
2974
+ }
2975
+ if (!relation.to) {
2976
+ diagnostics.push(error("validate.relation.to.required", `Relation "${relation.id}" is missing a "to" target.`));
2977
+ } else if (!objectMap.has(relation.to)) {
2978
+ diagnostics.push(error("validate.relation.to.unknown", `Unknown relation target "${relation.to}" on "${relation.id}".`));
2979
+ }
2980
+ if (!relation.kind) {
2981
+ diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
2982
+ }
2983
+ }
2984
+ function validateViewpoint(filter, eventRefs, groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpointId) {
2985
+ if (sourceSchemaVersion === "2.1") {
2986
+ if (filter) {
2987
+ for (const groupId of filter.groupIds) {
2988
+ if (!groupIds.has(groupId)) {
2989
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.groups`));
2990
+ }
2991
+ }
2992
+ }
2993
+ for (const eventId of eventRefs) {
2994
+ if (!eventIds.has(eventId)) {
2995
+ diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.events`));
2996
+ }
2997
+ }
2998
+ }
2999
+ }
3000
+ function validateObject(object, system, objectMap, groupIds, diagnostics) {
3001
+ const placement = object.placement;
3002
+ const orbitPlacement = placement?.mode === "orbit" ? placement : null;
3003
+ const parentObject = placement?.mode === "orbit" ? objectMap.get(placement.target) ?? null : null;
3004
+ if (object.groups) {
3005
+ for (const groupId of object.groups) {
3006
+ if (!groupIds.has(groupId)) {
3007
+ diagnostics.push(warn("validate.group.unknown", `Unknown group "${groupId}" on "${object.id}".`, object.id, "groups"));
3008
+ }
3009
+ }
3010
+ }
3011
+ if (orbitPlacement) {
3012
+ if (!objectMap.has(orbitPlacement.target)) {
3013
+ diagnostics.push(error("validate.orbit.target.unknown", `Unknown placement target "${orbitPlacement.target}" on "${object.id}".`, object.id, "orbit"));
3014
+ }
3015
+ if (orbitPlacement.distance && orbitPlacement.semiMajor) {
3016
+ diagnostics.push(error("validate.orbit.distanceConflict", `Object "${object.id}" cannot declare both "distance" and "semiMajor".`, object.id, "distance"));
3017
+ }
3018
+ if (orbitPlacement.phase && !object.epoch && !system?.epoch) {
3019
+ diagnostics.push(warn("validate.phase.epochMissing", `Object "${object.id}" sets "phase" without an object or system epoch.`, object.id, "phase"));
3020
+ }
3021
+ if (orbitPlacement.inclination && !object.referencePlane && !system?.referencePlane) {
3022
+ diagnostics.push(warn("validate.inclination.referencePlaneMissing", `Object "${object.id}" sets "inclination" without an object or system reference plane.`, object.id, "inclination"));
3023
+ }
3024
+ if (orbitPlacement.period && !massInSolar(parentObject?.properties.mass)) {
3025
+ diagnostics.push(warn("validate.period.massMissing", `Object "${object.id}" sets "period" but its central mass cannot be derived.`, object.id, "period"));
3026
+ }
3027
+ }
3028
+ if (placement?.mode === "surface") {
3029
+ const target = objectMap.get(placement.target);
3030
+ if (!target) {
3031
+ diagnostics.push(error("validate.surface.target.unknown", `Unknown placement target "${placement.target}" on "${object.id}".`, object.id, "surface"));
3032
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
3033
+ diagnostics.push(error("validate.surface.target.invalid", `Surface target "${placement.target}" on "${object.id}" is not surface-capable.`, object.id, "surface"));
3034
+ }
3035
+ }
3036
+ if (placement?.mode === "at") {
3037
+ if (object.type !== "structure" && object.type !== "phenomenon") {
3038
+ diagnostics.push(error("validate.at.objectType", `Only structures and phenomena may use "at" placement; found "${object.type}" on "${object.id}".`, object.id, "at"));
3039
+ }
3040
+ if (!validateAtTarget(object, objectMap, diagnostics)) {
3041
+ diagnostics.push(error("validate.at.target.unknown", `Unknown at-reference target "${placement.target}" on "${object.id}".`, object.id, "at"));
3042
+ }
3043
+ }
3044
+ if (object.resonance) {
3045
+ const target = objectMap.get(object.resonance.targetObjectId);
3046
+ if (!target) {
3047
+ diagnostics.push(error("validate.resonance.target.unknown", `Unknown resonance target "${object.resonance.targetObjectId}" on "${object.id}".`, object.id, "resonance"));
3048
+ } else if (object.placement?.mode !== "orbit" || target.placement?.mode !== "orbit" || object.placement.target !== target.placement.target) {
3049
+ diagnostics.push(warn("validate.resonance.orbitMismatch", `Resonance target "${object.resonance.targetObjectId}" on "${object.id}" does not share a compatible orbital parent.`, object.id, "resonance"));
3050
+ }
3051
+ }
3052
+ for (const rule of object.deriveRules ?? []) {
3053
+ if (rule.field !== "period" || rule.strategy !== "kepler") {
3054
+ diagnostics.push(warn("validate.derive.unsupported", `Unsupported derive rule "${rule.field} ${rule.strategy}" on "${object.id}".`, object.id, "derive"));
3055
+ continue;
3056
+ }
3057
+ const derivedPeriodDays = keplerPeriodDays(object, parentObject);
3058
+ if (derivedPeriodDays === null) {
3059
+ diagnostics.push(warn("validate.derive.inputsMissing", `Object "${object.id}" requests "derive period kepler" but lacks enough input data.`, object.id, "derive"));
3060
+ continue;
3061
+ }
3062
+ if (!orbitPlacement?.period) {
3063
+ diagnostics.push(info("validate.derive.period.available", `Object "${object.id}" can derive a Kepler period of ${formatDays(derivedPeriodDays)}.`, object.id, "derive"));
3064
+ }
3065
+ }
3066
+ for (const rule of object.validationRules ?? []) {
3067
+ if (rule.rule !== "kepler") {
3068
+ diagnostics.push(warn("validate.rule.unsupported", `Unsupported validation rule "${rule.rule}" on "${object.id}".`, object.id, "validate"));
3069
+ continue;
3070
+ }
3071
+ const actualPeriodDays = durationInDays(orbitPlacement?.period);
3072
+ const derivedPeriodDays = keplerPeriodDays(object, parentObject);
3073
+ if (actualPeriodDays === null || derivedPeriodDays === null) {
3074
+ continue;
3075
+ }
3076
+ const toleranceDays = toleranceForField(object, "period");
3077
+ if (Math.abs(actualPeriodDays - derivedPeriodDays) > toleranceDays) {
3078
+ diagnostics.push(error("validate.kepler.mismatch", `Object "${object.id}" fails Kepler validation for "period".`, object.id, "validate"));
3079
+ }
3080
+ }
3081
+ }
3082
+ function validateEvent(event, objectMap, diagnostics) {
3083
+ const fieldPrefix = `event.${event.id}`;
3084
+ const referencedIds = /* @__PURE__ */ new Set();
3085
+ if (!event.kind.trim()) {
3086
+ diagnostics.push(error("validate.event.kind.required", `Event "${event.id}" is missing a "kind" value.`, void 0, `${fieldPrefix}.kind`));
3087
+ }
3088
+ if (!event.targetObjectId && event.participantObjectIds.length === 0) {
3089
+ diagnostics.push(error("validate.event.references.required", `Event "${event.id}" must define a "target" or at least one participant.`, void 0, `${fieldPrefix}.participants`));
3090
+ }
3091
+ if (event.targetObjectId) {
3092
+ referencedIds.add(event.targetObjectId);
3093
+ if (!objectMap.has(event.targetObjectId)) {
3094
+ diagnostics.push(error("validate.event.target.unknown", `Unknown event target "${event.targetObjectId}" on "${event.id}".`, void 0, `${fieldPrefix}.target`));
3095
+ }
3096
+ }
3097
+ const seenParticipants = /* @__PURE__ */ new Set();
3098
+ for (const participantId of event.participantObjectIds) {
3099
+ referencedIds.add(participantId);
3100
+ if (seenParticipants.has(participantId)) {
3101
+ diagnostics.push(warn("validate.event.participants.duplicate", `Event "${event.id}" repeats participant "${participantId}".`, void 0, `${fieldPrefix}.participants`));
3102
+ continue;
3103
+ }
3104
+ seenParticipants.add(participantId);
3105
+ if (!objectMap.has(participantId)) {
3106
+ diagnostics.push(error("validate.event.participants.unknown", `Unknown event participant "${participantId}" on "${event.id}".`, void 0, `${fieldPrefix}.participants`));
3107
+ }
3108
+ }
3109
+ if (event.targetObjectId && event.participantObjectIds.length > 0 && !event.participantObjectIds.includes(event.targetObjectId)) {
3110
+ diagnostics.push(warn("validate.event.target.notParticipant", `Event "${event.id}" defines a target outside its participants list.`, void 0, `${fieldPrefix}.target`));
3111
+ }
3112
+ if (event.positions.length === 0) {
3113
+ diagnostics.push(warn("validate.event.positions.missing", `Event "${event.id}" has no positions block and cannot drive a scene snapshot.`, void 0, `${fieldPrefix}.positions`));
3114
+ }
3115
+ if (/(?:^|[-_])(solar-eclipse|lunar-eclipse|transit|occultation)(?:$|[-_])/.test(event.kind) && referencedIds.size < 3) {
3116
+ 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`));
3117
+ }
3118
+ const poseIds = /* @__PURE__ */ new Set();
3119
+ for (const pose of event.positions) {
3120
+ const poseFieldPrefix = `${fieldPrefix}.pose.${pose.objectId}`;
3121
+ if (poseIds.has(pose.objectId)) {
3122
+ diagnostics.push(error("validate.event.pose.duplicate", `Event "${event.id}" defines "${pose.objectId}" more than once in positions.`, void 0, poseFieldPrefix));
3123
+ continue;
3124
+ }
3125
+ poseIds.add(pose.objectId);
3126
+ const object = objectMap.get(pose.objectId);
3127
+ if (!object) {
3128
+ diagnostics.push(error("validate.event.pose.object.unknown", `Unknown event pose object "${pose.objectId}" on "${event.id}".`, void 0, poseFieldPrefix));
3129
+ continue;
3130
+ }
3131
+ if (!referencedIds.has(pose.objectId)) {
3132
+ diagnostics.push(warn("validate.event.pose.unreferenced", `Event pose "${pose.objectId}" on "${event.id}" is not listed in target/participants.`, void 0, poseFieldPrefix));
3133
+ }
3134
+ validateEventPose(pose, object, objectMap, diagnostics, poseFieldPrefix, event.id);
3135
+ }
3136
+ }
3137
+ function validateEventPose(pose, object, objectMap, diagnostics, fieldPrefix, eventId) {
3138
+ const placement = pose.placement;
3139
+ if (!placement) {
3140
+ diagnostics.push(error("validate.event.pose.placement.required", `Event "${eventId}" pose "${pose.objectId}" is missing a placement mode.`, void 0, fieldPrefix));
3141
+ return;
3142
+ }
3143
+ if (placement.mode === "orbit") {
3144
+ if (!objectMap.has(placement.target)) {
3145
+ diagnostics.push(error("validate.event.pose.orbit.target.unknown", `Unknown event orbit target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.orbit`));
3146
+ }
3147
+ if (placement.distance && placement.semiMajor) {
3148
+ diagnostics.push(error("validate.event.pose.orbit.distanceConflict", `Event "${eventId}" pose "${pose.objectId}" cannot declare both "distance" and "semiMajor".`, void 0, `${fieldPrefix}.distance`));
3149
+ }
3150
+ return;
3151
+ }
3152
+ if (placement.mode === "surface") {
3153
+ const target = objectMap.get(placement.target);
3154
+ if (!target) {
3155
+ diagnostics.push(error("validate.event.pose.surface.target.unknown", `Unknown event surface target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.surface`));
3156
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
3157
+ 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`));
3158
+ }
3159
+ return;
3160
+ }
3161
+ if (placement.mode === "at") {
3162
+ if (object.type !== "structure" && object.type !== "phenomenon") {
3163
+ 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`));
3164
+ }
3165
+ const reference = placement.reference;
3166
+ if (reference.kind === "named" && !objectMap.has(reference.name)) {
3167
+ diagnostics.push(error("validate.event.pose.at.target.unknown", `Unknown event at-reference target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3168
+ } else if (reference.kind === "anchor" && !objectMap.has(reference.objectId)) {
3169
+ diagnostics.push(error("validate.event.pose.anchor.target.unknown", `Unknown event anchor target "${reference.objectId}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3170
+ } else if (reference.kind === "lagrange") {
3171
+ if (!objectMap.has(reference.primary)) {
3172
+ diagnostics.push(error("validate.event.pose.lagrange.primary.unknown", `Unknown event Lagrange target "${reference.primary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3173
+ } else if (reference.secondary && !objectMap.has(reference.secondary)) {
3174
+ diagnostics.push(error("validate.event.pose.lagrange.secondary.unknown", `Unknown event Lagrange target "${reference.secondary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3175
+ }
3176
+ }
3177
+ }
3178
+ }
3179
+ function validateAtTarget(object, objectMap, diagnostics) {
3180
+ const reference = object.placement?.mode === "at" ? object.placement.reference : null;
3181
+ if (!reference) {
3182
+ return true;
3183
+ }
3184
+ if (reference.kind === "named") {
3185
+ return objectMap.has(reference.name);
3186
+ }
3187
+ if (reference.kind === "anchor") {
3188
+ if (!objectMap.has(reference.objectId)) {
3189
+ diagnostics.push(error("validate.anchor.target.unknown", `Unknown anchor target "${reference.objectId}" on "${object.id}".`, object.id, "at"));
3190
+ return false;
3191
+ }
3192
+ return true;
3193
+ }
3194
+ if (!objectMap.has(reference.primary)) {
3195
+ diagnostics.push(error("validate.lagrange.primary.unknown", `Unknown Lagrange reference "${reference.primary}" on "${object.id}".`, object.id, "at"));
3196
+ return false;
3197
+ }
3198
+ if (reference.secondary && !objectMap.has(reference.secondary)) {
3199
+ diagnostics.push(error("validate.lagrange.secondary.unknown", `Unknown Lagrange reference "${reference.secondary}" on "${object.id}".`, object.id, "at"));
3200
+ return false;
3201
+ }
3202
+ return true;
3203
+ }
3204
+ function keplerPeriodDays(object, parentObject) {
3205
+ const placement = object.placement;
3206
+ if (!placement || placement.mode !== "orbit") {
3207
+ return null;
3208
+ }
3209
+ const semiMajorAu = distanceInAu(placement.semiMajor ?? placement.distance);
3210
+ const centralMassSolar = massInSolar(parentObject?.properties.mass);
3211
+ if (semiMajorAu === null || centralMassSolar === null || centralMassSolar <= 0) {
3212
+ return null;
3213
+ }
3214
+ const periodYears = Math.sqrt(semiMajorAu ** 3 / centralMassSolar);
3215
+ return periodYears * 365.25;
3216
+ }
3217
+ function distanceInAu(value) {
3218
+ if (!value)
3219
+ return null;
3220
+ switch (value.unit) {
3221
+ case null:
3222
+ case "au":
3223
+ return value.value;
3224
+ case "km":
3225
+ return value.value / AU_IN_KM2;
3226
+ case "m":
3227
+ return value.value / (AU_IN_KM2 * 1e3);
3228
+ case "ly":
3229
+ return value.value * LY_IN_AU2;
3230
+ case "pc":
3231
+ return value.value * PC_IN_AU2;
3232
+ case "kpc":
3233
+ return value.value * KPC_IN_AU2;
3234
+ case "re":
3235
+ return value.value * EARTH_RADIUS_IN_KM2 / AU_IN_KM2;
3236
+ case "sol":
3237
+ return value.value * SOLAR_RADIUS_IN_KM2 / AU_IN_KM2;
3238
+ default:
3239
+ return null;
3240
+ }
3241
+ }
3242
+ function massInSolar(value) {
3243
+ if (!value || typeof value !== "object" || !("value" in value)) {
3244
+ return null;
3245
+ }
3246
+ const unitValue = value;
3247
+ switch (unitValue.unit) {
3248
+ case null:
3249
+ case "sol":
3250
+ return unitValue.value;
3251
+ case "me":
3252
+ return unitValue.value / EARTH_MASSES_PER_SOLAR;
3253
+ case "mj":
3254
+ return unitValue.value / JUPITER_MASSES_PER_SOLAR;
3255
+ default:
3256
+ return null;
3257
+ }
3258
+ }
3259
+ function durationInDays(value) {
3260
+ if (!value)
3261
+ return null;
3262
+ switch (value.unit) {
3263
+ case null:
3264
+ case "d":
3265
+ return value.value;
3266
+ case "s":
3267
+ return value.value / 86400;
3268
+ case "min":
3269
+ return value.value / 1440;
3270
+ case "h":
3271
+ return value.value / 24;
3272
+ case "y":
3273
+ return value.value * 365.25;
3274
+ case "ky":
3275
+ return value.value * 365250;
3276
+ case "my":
3277
+ return value.value * 36525e4;
3278
+ case "gy":
3279
+ return value.value * 36525e7;
3280
+ default:
3281
+ return null;
3282
+ }
3283
+ }
3284
+ function toleranceForField(object, field) {
3285
+ const tolerance = object.tolerances?.find((entry) => entry.field === field)?.value;
3286
+ if (typeof tolerance === "number") {
3287
+ return tolerance;
3288
+ }
3289
+ if (tolerance && typeof tolerance === "object" && "value" in tolerance) {
3290
+ return durationInDays(tolerance) ?? 0;
3291
+ }
3292
+ return 0;
3293
+ }
3294
+ function formatDays(days) {
3295
+ return `${Math.round(days * 100) / 100}d`;
3296
+ }
3297
+ function error(code, message, objectId, field) {
3298
+ return { code, severity: "error", source: "validate", message, objectId, field };
3299
+ }
3300
+ function warn(code, message, objectId, field) {
3301
+ return { code, severity: "warning", source: "validate", message, objectId, field };
3302
+ }
3303
+ function info(code, message, objectId, field) {
3304
+ return { code, severity: "info", source: "validate", message, objectId, field };
3305
+ }
3306
+
3307
+ // packages/core/dist/draft-parse.js
3308
+ var STRUCTURED_TYPED_BLOCKS = /* @__PURE__ */ new Set([
3309
+ "climate",
3310
+ "habitability",
3311
+ "settlement"
3312
+ ]);
3313
+ var DRAFT_OBJECT_FIELD_SPECS = /* @__PURE__ */ new Map();
3314
+ for (const key of [
3315
+ "orbit",
3316
+ "distance",
3317
+ "semiMajor",
3318
+ "eccentricity",
3319
+ "period",
3320
+ "angle",
3321
+ "inclination",
3322
+ "phase",
3323
+ "at",
3324
+ "surface",
3325
+ "free",
3326
+ "kind",
3327
+ "class",
3328
+ "culture",
3329
+ "tags",
3330
+ "color",
3331
+ "image",
3332
+ "hidden",
3333
+ "radius",
3334
+ "mass",
3335
+ "density",
3336
+ "gravity",
3337
+ "temperature",
3338
+ "albedo",
3339
+ "atmosphere",
3340
+ "inner",
3341
+ "outer",
3342
+ "on",
3343
+ "source",
3344
+ "cycle"
3345
+ ]) {
3346
+ const schema = getFieldSchema(key);
3347
+ if (schema) {
3348
+ DRAFT_OBJECT_FIELD_SPECS.set(key, {
3349
+ key,
3350
+ version: "2.0",
3351
+ inlineMode: schema.arity === "multiple" ? "multiple" : "single",
3352
+ allowRepeat: false,
3353
+ legacySchema: schema
3354
+ });
3355
+ }
3356
+ }
3357
+ for (const spec of [
3358
+ { key: "groups", inlineMode: "multiple", allowRepeat: false },
3359
+ { key: "epoch", inlineMode: "single", allowRepeat: false },
3360
+ { key: "referencePlane", inlineMode: "single", allowRepeat: false },
3361
+ { key: "tidalLock", inlineMode: "single", allowRepeat: false },
3362
+ { key: "renderLabel", inlineMode: "single", allowRepeat: false },
3363
+ { key: "renderOrbit", inlineMode: "single", allowRepeat: false },
3364
+ { key: "renderPriority", inlineMode: "single", allowRepeat: false },
3365
+ { key: "resonance", inlineMode: "pair", allowRepeat: false },
3366
+ { key: "derive", inlineMode: "pair", allowRepeat: true },
3367
+ { key: "validate", inlineMode: "single", allowRepeat: true },
3368
+ { key: "locked", inlineMode: "multiple", allowRepeat: false },
3369
+ { key: "tolerance", inlineMode: "pair", allowRepeat: true }
3370
+ ]) {
3371
+ DRAFT_OBJECT_FIELD_SPECS.set(spec.key, {
3372
+ key: spec.key,
3373
+ version: "2.1",
3374
+ inlineMode: spec.inlineMode,
3375
+ allowRepeat: spec.allowRepeat
3376
+ });
3377
+ }
3378
+ var DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
3379
+ var EVENT_POSE_FIELD_KEYS = /* @__PURE__ */ new Set([
3380
+ "orbit",
3381
+ "distance",
3382
+ "semiMajor",
3383
+ "eccentricity",
3384
+ "period",
3385
+ "angle",
3386
+ "inclination",
3387
+ "phase",
3388
+ "at",
3389
+ "surface",
3390
+ "free",
3391
+ "inner",
3392
+ "outer"
3393
+ ]);
3394
+ function parseWorldOrbitAtlas(source) {
3395
+ return parseAtlasSource(source);
3396
+ }
3397
+ function parseAtlasSource(source, forcedOutputVersion) {
3398
+ const prepared = preprocessAtlasSource(source);
3399
+ const lines = prepared.source.split(/\r?\n/);
3400
+ const diagnostics = [];
3401
+ let sawSchemaHeader = false;
3402
+ let sourceSchemaVersion = "2.0";
3403
+ let system = null;
3404
+ let section = null;
3405
+ const objectNodes = [];
3406
+ const groups = [];
3407
+ const relations = [];
3408
+ const events = [];
3409
+ const eventPoseNodes = /* @__PURE__ */ new Map();
3410
+ let sawDefaults = false;
3411
+ let sawAtlas = false;
3412
+ const viewpointIds = /* @__PURE__ */ new Set();
3413
+ const annotationIds = /* @__PURE__ */ new Set();
3414
+ const groupIds = /* @__PURE__ */ new Set();
3415
+ const relationIds = /* @__PURE__ */ new Set();
3416
+ const eventIds = /* @__PURE__ */ new Set();
3417
+ for (let index = 0; index < lines.length; index++) {
3418
+ const rawLine = lines[index];
3419
+ const lineNumber = index + 1;
3420
+ if (!rawLine.trim()) {
3421
+ continue;
3422
+ }
3423
+ const indent = getIndent(rawLine);
3424
+ const tokens = tokenizeLineDetailed(rawLine.slice(indent), {
3425
+ line: lineNumber,
3426
+ columnOffset: indent
3427
+ });
3428
+ if (tokens.length === 0) {
3429
+ continue;
3430
+ }
3431
+ if (!sawSchemaHeader) {
3432
+ sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
3433
+ sawSchemaHeader = true;
3434
+ if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
3435
+ diagnostics.push({
3436
+ code: "parse.schema21.commentCompatibility",
3437
+ severity: "warning",
3438
+ source: "parse",
3439
+ message: `Comments require schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
3440
+ line: prepared.comments[0].line,
3441
+ column: prepared.comments[0].column
3442
+ });
3443
+ }
3444
+ continue;
3445
+ }
3446
+ if (indent === 0) {
3447
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, { sawDefaults, sawAtlas });
3448
+ if (section.kind === "system") {
3449
+ system = section.system;
3450
+ } else if (section.kind === "defaults") {
3451
+ sawDefaults = true;
3452
+ } else if (section.kind === "atlas") {
3453
+ sawAtlas = true;
3454
+ }
3455
+ continue;
3456
+ }
3457
+ if (!section) {
3458
+ throw new WorldOrbitError("Indented line without parent atlas section", lineNumber, indent + 1);
3459
+ }
3460
+ handleSectionLine(section, indent, tokens, lineNumber);
3461
+ }
3462
+ if (!sawSchemaHeader) {
3463
+ throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
3464
+ }
3465
+ const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
3466
+ const normalizedEvents = events.map((event) => normalizeDraftEvent(event, eventPoseNodes.get(event.id) ?? []));
3467
+ const outputVersion = forcedOutputVersion ?? (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
3468
+ const baseDocument = {
3469
+ format: "worldorbit",
3470
+ sourceVersion: "1.0",
3471
+ system,
3472
+ groups,
3473
+ relations,
3474
+ events: normalizedEvents,
3475
+ objects,
3476
+ diagnostics
3477
+ };
3478
+ if (outputVersion === "2.0-draft") {
3479
+ const document3 = {
3480
+ ...baseDocument,
3481
+ version: "2.0-draft",
3482
+ schemaVersion: "2.0-draft"
3483
+ };
3484
+ document3.diagnostics.push(...collectAtlasDiagnostics(document3, sourceSchemaVersion));
3485
+ return document3;
3486
+ }
3487
+ const document2 = {
3488
+ ...baseDocument,
3489
+ version: outputVersion,
3490
+ schemaVersion: outputVersion
3491
+ };
3492
+ if (sourceSchemaVersion === "2.0-draft") {
3493
+ document2.diagnostics.push({
3494
+ code: "load.schema.deprecatedDraft",
3495
+ severity: "warning",
3496
+ source: "upgrade",
3497
+ message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".'
3498
+ });
3499
+ }
3500
+ document2.diagnostics.push(...collectAtlasDiagnostics(document2, sourceSchemaVersion));
3501
+ return document2;
3502
+ }
3503
+ function assertDraftSchemaHeader(tokens, line) {
3504
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
3505
+ throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
3506
+ }
3507
+ const version = tokens[1].value.toLowerCase();
3508
+ return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
3509
+ }
3510
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, flags) {
3511
+ const keyword = tokens[0]?.value.toLowerCase();
3512
+ switch (keyword) {
3513
+ case "system":
3514
+ if (system) {
3515
+ throw new WorldOrbitError('Atlas section "system" may only appear once', line, tokens[0].column);
3516
+ }
3517
+ return startSystemSection(tokens, line, sourceSchemaVersion, diagnostics);
3518
+ case "defaults":
3519
+ if (!system) {
3520
+ throw new WorldOrbitError('Atlas section "defaults" requires a preceding system declaration', line, tokens[0].column);
3521
+ }
3522
+ if (flags.sawDefaults) {
3523
+ throw new WorldOrbitError('Atlas section "defaults" may only appear once', line, tokens[0].column);
3524
+ }
3525
+ return {
3526
+ kind: "defaults",
3527
+ system,
3528
+ seenFields: /* @__PURE__ */ new Set()
3529
+ };
3530
+ case "atlas":
3531
+ if (!system) {
3532
+ throw new WorldOrbitError('Atlas section "atlas" requires a preceding system declaration', line, tokens[0].column);
3533
+ }
3534
+ if (flags.sawAtlas) {
3535
+ throw new WorldOrbitError('Atlas section "atlas" may only appear once', line, tokens[0].column);
3536
+ }
3537
+ return {
3538
+ kind: "atlas",
3539
+ system,
3540
+ inMetadata: false,
3541
+ metadataIndent: null
3542
+ };
3543
+ case "viewpoint":
3544
+ if (!system) {
3545
+ throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
3546
+ }
3547
+ return startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics);
3548
+ case "annotation":
3549
+ if (!system) {
3550
+ throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
3551
+ }
3552
+ return startAnnotationSection(tokens, line, system, annotationIds);
3553
+ case "group":
3554
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "group", { line, column: tokens[0].column });
3555
+ return startGroupSection(tokens, line, groups, groupIds);
3556
+ case "relation":
3557
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
3558
+ return startRelationSection(tokens, line, relations, relationIds);
3559
+ case "event":
3560
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "event", { line, column: tokens[0].column });
3561
+ return startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics);
3562
+ case "object":
3563
+ return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
3564
+ default:
3565
+ throw new WorldOrbitError(`Unknown atlas section "${tokens[0]?.value ?? ""}"`, line, tokens[0]?.column ?? 1);
3566
+ }
3567
+ }
3568
+ function startSystemSection(tokens, line, sourceSchemaVersion, diagnostics) {
3569
+ if (tokens.length !== 2) {
3570
+ throw new WorldOrbitError("Invalid atlas system declaration", line, tokens[0]?.column ?? 1);
3571
+ }
3572
+ const system = {
3573
+ type: "system",
3574
+ id: tokens[1].value,
3575
+ title: null,
3576
+ description: null,
3577
+ epoch: null,
3578
+ referencePlane: null,
3579
+ defaults: {
3580
+ view: "topdown",
3581
+ scale: null,
3582
+ units: null,
3583
+ preset: null,
3584
+ theme: null
3585
+ },
3586
+ atlasMetadata: {},
3587
+ viewpoints: [],
3588
+ annotations: []
3589
+ };
3590
+ return {
3591
+ kind: "system",
3592
+ system,
3593
+ sourceSchemaVersion,
3594
+ diagnostics,
2595
3595
  seenFields: /* @__PURE__ */ new Set()
2596
3596
  };
2597
3597
  }
2598
- function startViewpointSection(tokens, line, system, viewpointIds) {
3598
+ function startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics) {
2599
3599
  if (tokens.length !== 2) {
2600
3600
  throw new WorldOrbitError("Invalid viewpoint declaration", line, tokens[0]?.column ?? 1);
2601
3601
  }
@@ -2612,6 +3612,7 @@ var WorldOrbit = (() => {
2612
3612
  summary: "",
2613
3613
  focusObjectId: null,
2614
3614
  selectedObjectId: null,
3615
+ events: [],
2615
3616
  projection: system.defaults.view,
2616
3617
  preset: system.defaults.preset,
2617
3618
  zoom: null,
@@ -2624,6 +3625,8 @@ var WorldOrbit = (() => {
2624
3625
  return {
2625
3626
  kind: "viewpoint",
2626
3627
  viewpoint,
3628
+ sourceSchemaVersion,
3629
+ diagnostics,
2627
3630
  seenFields: /* @__PURE__ */ new Set(),
2628
3631
  inFilter: false,
2629
3632
  filterIndent: null,
@@ -2657,7 +3660,107 @@ var WorldOrbit = (() => {
2657
3660
  seenFields: /* @__PURE__ */ new Set()
2658
3661
  };
2659
3662
  }
2660
- function startObjectSection(tokens, line, objectNodes) {
3663
+ function startGroupSection(tokens, line, groups, groupIds) {
3664
+ if (tokens.length !== 2) {
3665
+ throw new WorldOrbitError("Invalid group declaration", line, tokens[0]?.column ?? 1);
3666
+ }
3667
+ const id = normalizeIdentifier(tokens[1].value);
3668
+ if (!id) {
3669
+ throw new WorldOrbitError("Group id must not be empty", line, tokens[1].column);
3670
+ }
3671
+ if (groupIds.has(id)) {
3672
+ throw new WorldOrbitError(`Duplicate group id "${id}"`, line, tokens[1].column);
3673
+ }
3674
+ const group = {
3675
+ id,
3676
+ label: humanizeIdentifier2(id),
3677
+ summary: "",
3678
+ color: null,
3679
+ tags: [],
3680
+ hidden: false
3681
+ };
3682
+ groups.push(group);
3683
+ groupIds.add(id);
3684
+ return {
3685
+ kind: "group",
3686
+ group,
3687
+ seenFields: /* @__PURE__ */ new Set()
3688
+ };
3689
+ }
3690
+ function startRelationSection(tokens, line, relations, relationIds) {
3691
+ if (tokens.length !== 2) {
3692
+ throw new WorldOrbitError("Invalid relation declaration", line, tokens[0]?.column ?? 1);
3693
+ }
3694
+ const id = normalizeIdentifier(tokens[1].value);
3695
+ if (!id) {
3696
+ throw new WorldOrbitError("Relation id must not be empty", line, tokens[1].column);
3697
+ }
3698
+ if (relationIds.has(id)) {
3699
+ throw new WorldOrbitError(`Duplicate relation id "${id}"`, line, tokens[1].column);
3700
+ }
3701
+ const relation = {
3702
+ id,
3703
+ from: "",
3704
+ to: "",
3705
+ kind: "",
3706
+ label: null,
3707
+ summary: null,
3708
+ tags: [],
3709
+ color: null,
3710
+ hidden: false
3711
+ };
3712
+ relations.push(relation);
3713
+ relationIds.add(id);
3714
+ return {
3715
+ kind: "relation",
3716
+ relation,
3717
+ seenFields: /* @__PURE__ */ new Set()
3718
+ };
3719
+ }
3720
+ function startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics) {
3721
+ if (tokens.length !== 2) {
3722
+ throw new WorldOrbitError("Invalid event declaration", line, tokens[0]?.column ?? 1);
3723
+ }
3724
+ const id = normalizeIdentifier(tokens[1].value);
3725
+ if (!id) {
3726
+ throw new WorldOrbitError("Event id must not be empty", line, tokens[1].column);
3727
+ }
3728
+ if (eventIds.has(id)) {
3729
+ throw new WorldOrbitError(`Duplicate event id "${id}"`, line, tokens[1].column);
3730
+ }
3731
+ const event = {
3732
+ id,
3733
+ kind: "",
3734
+ label: humanizeIdentifier2(id),
3735
+ summary: null,
3736
+ targetObjectId: null,
3737
+ participantObjectIds: [],
3738
+ timing: null,
3739
+ visibility: null,
3740
+ tags: [],
3741
+ color: null,
3742
+ hidden: false,
3743
+ positions: []
3744
+ };
3745
+ const rawPoses = [];
3746
+ events.push(event);
3747
+ eventPoseNodes.set(id, rawPoses);
3748
+ eventIds.add(id);
3749
+ return {
3750
+ kind: "event",
3751
+ event,
3752
+ sourceSchemaVersion,
3753
+ diagnostics,
3754
+ seenFields: /* @__PURE__ */ new Set(),
3755
+ rawPoses,
3756
+ inPositions: false,
3757
+ positionsIndent: null,
3758
+ activePose: null,
3759
+ poseIndent: null,
3760
+ activePoseSeenFields: /* @__PURE__ */ new Set()
3761
+ };
3762
+ }
3763
+ function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
2661
3764
  if (tokens.length < 3) {
2662
3765
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
2663
3766
  }
@@ -2668,12 +3771,11 @@ var WorldOrbit = (() => {
2668
3771
  throw new WorldOrbitError(`Unknown object type "${objectTypeToken.value}"`, line, objectTypeToken.column);
2669
3772
  }
2670
3773
  const objectNode = {
2671
- type: "object",
2672
3774
  objectType,
2673
- name: idToken.value,
2674
- inlineFields: parseInlineFields2(tokens.slice(3), line),
2675
- blockFields: [],
3775
+ id: idToken.value,
3776
+ fields: parseInlineObjectFields(tokens.slice(3), line, objectType, sourceSchemaVersion, diagnostics),
2676
3777
  infoEntries: [],
3778
+ typedBlockEntries: {},
2677
3779
  location: {
2678
3780
  line,
2679
3781
  column: objectTypeToken.column
@@ -2683,8 +3785,12 @@ var WorldOrbit = (() => {
2683
3785
  return {
2684
3786
  kind: "object",
2685
3787
  objectNode,
2686
- inInfoBlock: false,
2687
- infoIndent: null
3788
+ sourceSchemaVersion,
3789
+ diagnostics,
3790
+ activeBlock: null,
3791
+ blockIndent: null,
3792
+ seenInfoKeys: /* @__PURE__ */ new Set(),
3793
+ seenTypedBlockKeys: {}
2688
3794
  };
2689
3795
  }
2690
3796
  function handleSectionLine(section, indent, tokens, line) {
@@ -2704,6 +3810,15 @@ var WorldOrbit = (() => {
2704
3810
  case "annotation":
2705
3811
  applyAnnotationField(section, tokens, line);
2706
3812
  return;
3813
+ case "group":
3814
+ applyGroupField(section, tokens, line);
3815
+ return;
3816
+ case "relation":
3817
+ applyRelationField(section, tokens, line);
3818
+ return;
3819
+ case "event":
3820
+ applyEventField(section, indent, tokens, line);
3821
+ return;
2707
3822
  case "object":
2708
3823
  applyObjectField(section, indent, tokens, line);
2709
3824
  return;
@@ -2711,10 +3826,35 @@ var WorldOrbit = (() => {
2711
3826
  }
2712
3827
  function applySystemField(section, tokens, line) {
2713
3828
  const key = requireUniqueField(tokens, section.seenFields, line);
2714
- if (key !== "title") {
2715
- throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
3829
+ const value = joinFieldValue(tokens, line);
3830
+ switch (key) {
3831
+ case "title":
3832
+ section.system.title = value;
3833
+ return;
3834
+ case "description":
3835
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
3836
+ line,
3837
+ column: tokens[0].column
3838
+ });
3839
+ section.system.description = value;
3840
+ return;
3841
+ case "epoch":
3842
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
3843
+ line,
3844
+ column: tokens[0].column
3845
+ });
3846
+ section.system.epoch = value;
3847
+ return;
3848
+ case "referenceplane":
3849
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "referencePlane", {
3850
+ line,
3851
+ column: tokens[0].column
3852
+ });
3853
+ section.system.referencePlane = value;
3854
+ return;
3855
+ default:
3856
+ throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
2716
3857
  }
2717
- section.system.title = joinFieldValue(tokens, line);
2718
3858
  }
2719
3859
  function applyDefaultsField(section, tokens, line) {
2720
3860
  const key = requireUniqueField(tokens, section.seenFields, line);
@@ -2745,14 +3885,11 @@ var WorldOrbit = (() => {
2745
3885
  section.metadataIndent = null;
2746
3886
  }
2747
3887
  if (section.inMetadata) {
2748
- if (tokens.length < 2) {
2749
- throw new WorldOrbitError("Invalid atlas metadata entry", line, tokens[0]?.column ?? 1);
2750
- }
2751
- const key = tokens[0].value;
2752
- if (key in section.system.atlasMetadata) {
2753
- throw new WorldOrbitError(`Duplicate atlas metadata key "${key}"`, line, tokens[0].column);
3888
+ const entry = parseInfoLikeEntry(tokens, line, "Invalid atlas metadata entry");
3889
+ if (entry.key in section.system.atlasMetadata) {
3890
+ throw new WorldOrbitError(`Duplicate atlas metadata key "${entry.key}"`, line, tokens[0].column);
2754
3891
  }
2755
- section.system.atlasMetadata[key] = joinFieldValue(tokens, line);
3892
+ section.system.atlasMetadata[entry.key] = entry.value;
2756
3893
  return;
2757
3894
  }
2758
3895
  if (tokens.length === 1 && tokens[0].value.toLowerCase() === "metadata") {
@@ -2808,7 +3945,14 @@ var WorldOrbit = (() => {
2808
3945
  section.viewpoint.rotationDeg = parseFiniteNumber2(value, line, tokens[0].column, "rotation");
2809
3946
  return;
2810
3947
  case "layers":
2811
- section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line);
3948
+ section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line, section.sourceSchemaVersion, section.diagnostics);
3949
+ return;
3950
+ case "events":
3951
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.events", {
3952
+ line,
3953
+ column: tokens[0].column
3954
+ });
3955
+ section.viewpoint.events = parseTokenList(tokens.slice(1), line, "events");
2812
3956
  return;
2813
3957
  default:
2814
3958
  throw new WorldOrbitError(`Unknown viewpoint field "${tokens[0].value}"`, line, tokens[0].column);
@@ -2833,42 +3977,223 @@ var WorldOrbit = (() => {
2833
3977
  default:
2834
3978
  throw new WorldOrbitError(`Unknown viewpoint filter field "${tokens[0].value}"`, line, tokens[0].column);
2835
3979
  }
2836
- section.viewpoint.filter = filter;
2837
- }
2838
- function applyAnnotationField(section, tokens, line) {
3980
+ section.viewpoint.filter = filter;
3981
+ }
3982
+ function applyAnnotationField(section, tokens, line) {
3983
+ const key = requireUniqueField(tokens, section.seenFields, line);
3984
+ switch (key) {
3985
+ case "label":
3986
+ section.annotation.label = joinFieldValue(tokens, line);
3987
+ return;
3988
+ case "target":
3989
+ section.annotation.targetObjectId = joinFieldValue(tokens, line);
3990
+ return;
3991
+ case "body":
3992
+ section.annotation.body = joinFieldValue(tokens, line);
3993
+ return;
3994
+ case "tags":
3995
+ section.annotation.tags = parseTokenList(tokens.slice(1), line, "tags");
3996
+ return;
3997
+ default:
3998
+ throw new WorldOrbitError(`Unknown annotation field "${tokens[0].value}"`, line, tokens[0].column);
3999
+ }
4000
+ }
4001
+ function applyGroupField(section, tokens, line) {
4002
+ const key = requireUniqueField(tokens, section.seenFields, line);
4003
+ switch (key) {
4004
+ case "label":
4005
+ section.group.label = joinFieldValue(tokens, line);
4006
+ return;
4007
+ case "summary":
4008
+ section.group.summary = joinFieldValue(tokens, line);
4009
+ return;
4010
+ case "color":
4011
+ section.group.color = joinFieldValue(tokens, line);
4012
+ return;
4013
+ case "tags":
4014
+ section.group.tags = parseTokenList(tokens.slice(1), line, "tags");
4015
+ return;
4016
+ case "hidden":
4017
+ section.group.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4018
+ line,
4019
+ column: tokens[0].column
4020
+ });
4021
+ return;
4022
+ default:
4023
+ throw new WorldOrbitError(`Unknown group field "${tokens[0].value}"`, line, tokens[0].column);
4024
+ }
4025
+ }
4026
+ function applyRelationField(section, tokens, line) {
4027
+ const key = requireUniqueField(tokens, section.seenFields, line);
4028
+ switch (key) {
4029
+ case "from":
4030
+ section.relation.from = joinFieldValue(tokens, line);
4031
+ return;
4032
+ case "to":
4033
+ section.relation.to = joinFieldValue(tokens, line);
4034
+ return;
4035
+ case "kind":
4036
+ section.relation.kind = joinFieldValue(tokens, line);
4037
+ return;
4038
+ case "label":
4039
+ section.relation.label = joinFieldValue(tokens, line);
4040
+ return;
4041
+ case "summary":
4042
+ section.relation.summary = joinFieldValue(tokens, line);
4043
+ return;
4044
+ case "tags":
4045
+ section.relation.tags = parseTokenList(tokens.slice(1), line, "tags");
4046
+ return;
4047
+ case "color":
4048
+ section.relation.color = joinFieldValue(tokens, line);
4049
+ return;
4050
+ case "hidden":
4051
+ section.relation.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4052
+ line,
4053
+ column: tokens[0].column
4054
+ });
4055
+ return;
4056
+ default:
4057
+ throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
4058
+ }
4059
+ }
4060
+ function applyEventField(section, indent, tokens, line) {
4061
+ if (section.activePose && indent <= (section.poseIndent ?? 0)) {
4062
+ section.activePose = null;
4063
+ section.poseIndent = null;
4064
+ section.activePoseSeenFields.clear();
4065
+ }
4066
+ if (!section.activePose && section.inPositions && indent <= (section.positionsIndent ?? 0)) {
4067
+ section.inPositions = false;
4068
+ section.positionsIndent = null;
4069
+ }
4070
+ if (section.activePose) {
4071
+ section.activePose.fields.push(parseEventPoseField(tokens, line, section.activePoseSeenFields));
4072
+ return;
4073
+ }
4074
+ if (section.inPositions) {
4075
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "pose") {
4076
+ throw new WorldOrbitError(`Unknown event positions field "${tokens[0].value}"`, line, tokens[0]?.column ?? 1);
4077
+ }
4078
+ const objectId = tokens[1].value;
4079
+ if (!objectId.trim()) {
4080
+ throw new WorldOrbitError("Event pose object id must not be empty", line, tokens[1].column);
4081
+ }
4082
+ const rawPose = {
4083
+ objectId,
4084
+ fields: [],
4085
+ location: { line, column: tokens[0].column }
4086
+ };
4087
+ section.rawPoses.push(rawPose);
4088
+ section.activePose = rawPose;
4089
+ section.poseIndent = indent;
4090
+ section.activePoseSeenFields = /* @__PURE__ */ new Set();
4091
+ return;
4092
+ }
4093
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "positions") {
4094
+ if (section.seenFields.has("positions")) {
4095
+ throw new WorldOrbitError('Duplicate event field "positions"', line, tokens[0].column);
4096
+ }
4097
+ section.seenFields.add("positions");
4098
+ section.inPositions = true;
4099
+ section.positionsIndent = indent;
4100
+ return;
4101
+ }
2839
4102
  const key = requireUniqueField(tokens, section.seenFields, line);
2840
4103
  switch (key) {
4104
+ case "kind":
4105
+ section.event.kind = joinFieldValue(tokens, line);
4106
+ return;
2841
4107
  case "label":
2842
- section.annotation.label = joinFieldValue(tokens, line);
4108
+ section.event.label = joinFieldValue(tokens, line);
4109
+ return;
4110
+ case "summary":
4111
+ section.event.summary = joinFieldValue(tokens, line);
2843
4112
  return;
2844
4113
  case "target":
2845
- section.annotation.targetObjectId = joinFieldValue(tokens, line);
4114
+ section.event.targetObjectId = joinFieldValue(tokens, line);
2846
4115
  return;
2847
- case "body":
2848
- section.annotation.body = joinFieldValue(tokens, line);
4116
+ case "participants":
4117
+ section.event.participantObjectIds = parseTokenList(tokens.slice(1), line, "participants");
4118
+ return;
4119
+ case "timing":
4120
+ section.event.timing = joinFieldValue(tokens, line);
4121
+ return;
4122
+ case "visibility":
4123
+ section.event.visibility = joinFieldValue(tokens, line);
2849
4124
  return;
2850
4125
  case "tags":
2851
- section.annotation.tags = parseTokenList(tokens.slice(1), line, "tags");
4126
+ section.event.tags = parseTokenList(tokens.slice(1), line, "tags");
4127
+ return;
4128
+ case "color":
4129
+ section.event.color = joinFieldValue(tokens, line);
4130
+ return;
4131
+ case "hidden":
4132
+ section.event.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4133
+ line,
4134
+ column: tokens[0].column
4135
+ });
2852
4136
  return;
2853
4137
  default:
2854
- throw new WorldOrbitError(`Unknown annotation field "${tokens[0].value}"`, line, tokens[0].column);
4138
+ throw new WorldOrbitError(`Unknown event field "${tokens[0].value}"`, line, tokens[0].column);
2855
4139
  }
2856
4140
  }
2857
- function applyObjectField(section, indent, tokens, line) {
2858
- if (tokens.length === 1 && tokens[0].value === "info") {
2859
- section.inInfoBlock = true;
2860
- section.infoIndent = indent;
2861
- return;
4141
+ function parseEventPoseField(tokens, line, seenFields) {
4142
+ if (tokens.length < 2) {
4143
+ throw new WorldOrbitError("Invalid event pose field line", line, tokens[0]?.column ?? 1);
4144
+ }
4145
+ const key = tokens[0].value;
4146
+ if (!EVENT_POSE_FIELD_KEYS.has(key)) {
4147
+ throw new WorldOrbitError(`Unknown event pose field "${key}"`, line, tokens[0].column);
2862
4148
  }
2863
- if (section.inInfoBlock && indent <= (section.infoIndent ?? 0)) {
2864
- section.inInfoBlock = false;
2865
- section.infoIndent = null;
4149
+ if (seenFields.has(key)) {
4150
+ throw new WorldOrbitError(`Duplicate event pose field "${key}"`, line, tokens[0].column);
4151
+ }
4152
+ seenFields.add(key);
4153
+ return {
4154
+ type: "field",
4155
+ key,
4156
+ values: tokens.slice(1).map((token) => token.value),
4157
+ location: { line, column: tokens[0].column }
4158
+ };
4159
+ }
4160
+ function applyObjectField(section, indent, tokens, line) {
4161
+ if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
4162
+ section.activeBlock = null;
4163
+ section.blockIndent = null;
4164
+ }
4165
+ if (tokens.length === 1) {
4166
+ const blockName = tokens[0].value.toLowerCase();
4167
+ if (blockName === "info" || STRUCTURED_TYPED_BLOCKS.has(blockName)) {
4168
+ if (blockName !== "info") {
4169
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, blockName, { line, column: tokens[0].column });
4170
+ }
4171
+ section.activeBlock = blockName;
4172
+ section.blockIndent = indent;
4173
+ return;
4174
+ }
2866
4175
  }
2867
- if (section.inInfoBlock) {
2868
- section.objectNode.infoEntries.push(parseInfoEntry2(tokens, line));
4176
+ if (section.activeBlock) {
4177
+ const entry = parseInfoLikeEntry(tokens, line, `Invalid ${section.activeBlock} entry`);
4178
+ if (section.activeBlock === "info") {
4179
+ if (section.seenInfoKeys.has(entry.key)) {
4180
+ throw new WorldOrbitError(`Duplicate info key "${entry.key}"`, line, tokens[0].column);
4181
+ }
4182
+ section.seenInfoKeys.add(entry.key);
4183
+ section.objectNode.infoEntries.push(entry);
4184
+ return;
4185
+ }
4186
+ const typedBlock = section.activeBlock;
4187
+ const seenKeys = section.seenTypedBlockKeys[typedBlock] ?? (section.seenTypedBlockKeys[typedBlock] = /* @__PURE__ */ new Set());
4188
+ if (seenKeys.has(entry.key)) {
4189
+ throw new WorldOrbitError(`Duplicate ${typedBlock} key "${entry.key}"`, line, tokens[0].column);
4190
+ }
4191
+ seenKeys.add(entry.key);
4192
+ const entries = section.objectNode.typedBlockEntries[typedBlock] ?? (section.objectNode.typedBlockEntries[typedBlock] = []);
4193
+ entries.push(entry);
2869
4194
  return;
2870
4195
  }
2871
- section.objectNode.blockFields.push(parseField2(tokens, line));
4196
+ section.objectNode.fields.push(parseObjectField(tokens, line, section.objectNode.objectType, section.sourceSchemaVersion, section.diagnostics));
2872
4197
  }
2873
4198
  function requireUniqueField(tokens, seenFields, line) {
2874
4199
  if (tokens.length < 2) {
@@ -2888,50 +4213,46 @@ var WorldOrbit = (() => {
2888
4213
  return tokens.slice(1).map((token) => token.value).join(" ").trim();
2889
4214
  }
2890
4215
  function parseObjectTypeTokens(tokens, line) {
2891
- if (tokens.length === 0) {
2892
- throw new WorldOrbitError("Missing value for atlas field", line);
2893
- }
2894
- return tokens.map((token) => {
2895
- const value = token.value;
2896
- if (value !== "star" && value !== "planet" && value !== "moon" && value !== "belt" && value !== "asteroid" && value !== "comet" && value !== "ring" && value !== "structure" && value !== "phenomenon") {
2897
- throw new WorldOrbitError(`Unknown viewpoint object type "${token.value}"`, line, token.column);
2898
- }
2899
- return value;
2900
- });
4216
+ 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");
2901
4217
  }
2902
- function parseTokenList(tokens, line, field) {
2903
- if (tokens.length === 0) {
2904
- throw new WorldOrbitError(`Missing value for field "${field}"`, line);
4218
+ function parseLayerTokens(tokens, line, sourceSchemaVersion, diagnostics) {
4219
+ const layers = {};
4220
+ for (const token of parseTokenList(tokens, line, "layers")) {
4221
+ const enabled = !token.startsWith("-") && !token.startsWith("!");
4222
+ const raw = token.replace(/^[-!]+/, "").toLowerCase();
4223
+ if (raw === "orbits") {
4224
+ layers["orbits-back"] = enabled;
4225
+ layers["orbits-front"] = enabled;
4226
+ continue;
4227
+ }
4228
+ if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "events" || raw === "objects" || raw === "labels" || raw === "metadata") {
4229
+ if (raw === "events" && sourceSchemaVersion && diagnostics) {
4230
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "layers.events", {
4231
+ line,
4232
+ column: tokens[0]?.column ?? 1
4233
+ });
4234
+ }
4235
+ layers[raw] = enabled;
4236
+ }
2905
4237
  }
2906
- return tokens.map((token) => token.value);
4238
+ return layers;
2907
4239
  }
2908
- function parseLayerTokens(tokens, line) {
4240
+ function parseTokenList(tokens, line, fieldName) {
2909
4241
  if (tokens.length === 0) {
2910
- throw new WorldOrbitError('Missing value for field "layers"', line);
4242
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, 1);
2911
4243
  }
2912
- const next = {};
2913
- for (const token of tokens) {
2914
- const enabled = !token.value.startsWith("-") && !token.value.startsWith("!");
2915
- const rawLayer = token.value.replace(/^[-!]+/, "").toLowerCase();
2916
- if (rawLayer === "orbits") {
2917
- next["orbits-back"] = enabled;
2918
- next["orbits-front"] = enabled;
2919
- continue;
2920
- }
2921
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
2922
- next[rawLayer] = enabled;
2923
- continue;
2924
- }
2925
- throw new WorldOrbitError(`Unknown layer token "${token.value}"`, line, token.column);
4244
+ const values = tokens.map((token) => token.value).filter(Boolean);
4245
+ if (values.length === 0) {
4246
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, tokens[0]?.column ?? 1);
2926
4247
  }
2927
- return next;
4248
+ return values;
2928
4249
  }
2929
4250
  function parseProjectionValue(value, line, column) {
2930
4251
  const normalized = value.toLowerCase();
2931
- if (normalized === "topdown" || normalized === "isometric") {
2932
- return normalized;
4252
+ if (normalized !== "topdown" && normalized !== "isometric") {
4253
+ throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
2933
4254
  }
2934
- throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
4255
+ return normalized;
2935
4256
  }
2936
4257
  function parsePresetValue(value, line, column) {
2937
4258
  const normalized = value.toLowerCase();
@@ -2941,16 +4262,16 @@ var WorldOrbit = (() => {
2941
4262
  throw new WorldOrbitError(`Unknown render preset "${value}"`, line, column);
2942
4263
  }
2943
4264
  function parsePositiveNumber2(value, line, column, field) {
2944
- const parsed = Number(value);
2945
- if (!Number.isFinite(parsed) || parsed <= 0) {
2946
- throw new WorldOrbitError(`Field "${field}" expects a positive number`, line, column);
4265
+ const parsed = parseFiniteNumber2(value, line, column, field);
4266
+ if (parsed <= 0) {
4267
+ throw new WorldOrbitError(`Field "${field}" must be greater than zero`, line, column);
2947
4268
  }
2948
4269
  return parsed;
2949
4270
  }
2950
4271
  function parseFiniteNumber2(value, line, column, field) {
2951
4272
  const parsed = Number(value);
2952
4273
  if (!Number.isFinite(parsed)) {
2953
- throw new WorldOrbitError(`Field "${field}" expects a finite number`, line, column);
4274
+ throw new WorldOrbitError(`Invalid numeric value "${value}" for "${field}"`, line, column);
2954
4275
  }
2955
4276
  return parsed;
2956
4277
  }
@@ -2962,28 +4283,43 @@ var WorldOrbit = (() => {
2962
4283
  groupIds: []
2963
4284
  };
2964
4285
  }
2965
- function parseInlineFields2(tokens, line) {
4286
+ function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
2966
4287
  const fields = [];
2967
4288
  let index = 0;
2968
4289
  while (index < tokens.length) {
2969
4290
  const keyToken = tokens[index];
2970
- const schema = getFieldSchema(keyToken.value);
2971
- if (!schema) {
4291
+ const spec = getDraftObjectFieldSpec(keyToken.value);
4292
+ if (!spec) {
2972
4293
  throw new WorldOrbitError(`Unknown field "${keyToken.value}"`, line, keyToken.column);
2973
4294
  }
4295
+ if (spec.version === "2.1") {
4296
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, keyToken.value, {
4297
+ line,
4298
+ column: keyToken.column
4299
+ });
4300
+ }
2974
4301
  index++;
2975
4302
  const valueTokens = [];
2976
- if (schema.arity === "multiple") {
2977
- while (index < tokens.length && !isKnownFieldKey(tokens[index].value)) {
2978
- valueTokens.push(tokens[index]);
2979
- index++;
2980
- }
2981
- } else {
4303
+ if (spec.inlineMode === "single") {
2982
4304
  const nextToken = tokens[index];
2983
4305
  if (nextToken) {
2984
4306
  valueTokens.push(nextToken);
2985
4307
  index++;
2986
4308
  }
4309
+ } else if (spec.inlineMode === "pair") {
4310
+ for (let count = 0; count < 2; count++) {
4311
+ const nextToken = tokens[index];
4312
+ if (!nextToken) {
4313
+ break;
4314
+ }
4315
+ valueTokens.push(nextToken);
4316
+ index++;
4317
+ }
4318
+ } else {
4319
+ while (index < tokens.length && !DRAFT_OBJECT_FIELD_KEYS.has(tokens[index].value)) {
4320
+ valueTokens.push(tokens[index]);
4321
+ index++;
4322
+ }
2987
4323
  }
2988
4324
  if (valueTokens.length === 0) {
2989
4325
  throw new WorldOrbitError(`Missing value for field "${keyToken.value}"`, line, keyToken.column);
@@ -2995,25 +4331,35 @@ var WorldOrbit = (() => {
2995
4331
  location: { line, column: keyToken.column }
2996
4332
  });
2997
4333
  }
4334
+ validateDraftObjectFieldCompatibility(fields, objectType);
2998
4335
  return fields;
2999
4336
  }
3000
- function parseField2(tokens, line) {
4337
+ function parseObjectField(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
3001
4338
  if (tokens.length < 2) {
3002
4339
  throw new WorldOrbitError("Invalid field line", line, tokens[0]?.column ?? 1);
3003
4340
  }
3004
- if (!getFieldSchema(tokens[0].value)) {
4341
+ const spec = getDraftObjectFieldSpec(tokens[0].value);
4342
+ if (!spec) {
3005
4343
  throw new WorldOrbitError(`Unknown field "${tokens[0].value}"`, line, tokens[0].column);
3006
4344
  }
3007
- return {
4345
+ if (spec.version === "2.1") {
4346
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, tokens[0].value, {
4347
+ line,
4348
+ column: tokens[0].column
4349
+ });
4350
+ }
4351
+ const field = {
3008
4352
  type: "field",
3009
4353
  key: tokens[0].value,
3010
4354
  values: tokens.slice(1).map((token) => token.value),
3011
4355
  location: { line, column: tokens[0].column }
3012
4356
  };
4357
+ validateDraftObjectFieldCompatibility([field], objectType);
4358
+ return field;
3013
4359
  }
3014
- function parseInfoEntry2(tokens, line) {
4360
+ function parseInfoLikeEntry(tokens, line, errorMessage) {
3015
4361
  if (tokens.length < 2) {
3016
- throw new WorldOrbitError("Invalid info entry", line, tokens[0]?.column ?? 1);
4362
+ throw new WorldOrbitError(errorMessage, line, tokens[0]?.column ?? 1);
3017
4363
  }
3018
4364
  return {
3019
4365
  type: "info-entry",
@@ -3022,18 +4368,366 @@ var WorldOrbit = (() => {
3022
4368
  location: { line, column: tokens[0].column }
3023
4369
  };
3024
4370
  }
3025
- function normalizeIdentifier(value) {
3026
- return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
4371
+ function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
4372
+ const fieldMap = collectDraftFields(node.fields);
4373
+ const placement = extractPlacementFromFieldMap(fieldMap);
4374
+ const properties = normalizeDraftProperties(node.objectType, fieldMap);
4375
+ const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
4376
+ const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
4377
+ const referencePlane = parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0]);
4378
+ const tidalLock = fieldMap.has("tidalLock") ? parseAtlasBoolean(singleFieldValue2(fieldMap.get("tidalLock")[0]), "tidalLock", fieldMap.get("tidalLock")[0].location) : void 0;
4379
+ const resonance = fieldMap.has("resonance") ? parseResonanceField(fieldMap.get("resonance")[0]) : void 0;
4380
+ const renderHints = extractRenderHints(fieldMap);
4381
+ const deriveRules = fieldMap.get("derive")?.map((field) => parseDeriveField(field));
4382
+ const validationRules = fieldMap.get("validate")?.map((field) => ({
4383
+ rule: singleFieldValue2(field)
4384
+ }));
4385
+ const lockedFields = fieldMap.has("locked") ? [...new Set(fieldMap.get("locked").flatMap((field) => field.values))] : void 0;
4386
+ const tolerances = fieldMap.get("tolerance")?.map((field) => parseToleranceField(field));
4387
+ const typedBlocks = normalizeTypedBlocks(node.typedBlockEntries);
4388
+ const info2 = normalizeInfoEntries(node.infoEntries, "info");
4389
+ const object = {
4390
+ type: node.objectType,
4391
+ id: node.id,
4392
+ properties,
4393
+ placement,
4394
+ info: info2
4395
+ };
4396
+ if (groups.length > 0)
4397
+ object.groups = groups;
4398
+ if (epoch)
4399
+ object.epoch = epoch;
4400
+ if (referencePlane)
4401
+ object.referencePlane = referencePlane;
4402
+ if (tidalLock !== void 0)
4403
+ object.tidalLock = tidalLock;
4404
+ if (resonance)
4405
+ object.resonance = resonance;
4406
+ if (renderHints)
4407
+ object.renderHints = renderHints;
4408
+ if (deriveRules?.length)
4409
+ object.deriveRules = deriveRules;
4410
+ if (validationRules?.length)
4411
+ object.validationRules = validationRules;
4412
+ if (lockedFields?.length)
4413
+ object.lockedFields = lockedFields;
4414
+ if (tolerances?.length)
4415
+ object.tolerances = tolerances;
4416
+ if (typedBlocks && Object.keys(typedBlocks).length > 0)
4417
+ object.typedBlocks = typedBlocks;
4418
+ if (sourceSchemaVersion !== "2.1") {
4419
+ 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) {
4420
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
4421
+ }
4422
+ }
4423
+ return object;
3027
4424
  }
3028
- function humanizeIdentifier2(value) {
3029
- return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
4425
+ function normalizeDraftEvent(event, rawPoses) {
4426
+ return {
4427
+ ...event,
4428
+ participantObjectIds: [...new Set(event.participantObjectIds)],
4429
+ tags: [...new Set(event.tags)],
4430
+ positions: rawPoses.map((pose) => normalizeDraftEventPose(pose))
4431
+ };
4432
+ }
4433
+ function normalizeDraftEventPose(rawPose) {
4434
+ const fieldMap = collectDraftFields(rawPose.fields);
4435
+ const placement = extractPlacementFromFieldMap(fieldMap);
4436
+ return {
4437
+ objectId: rawPose.objectId,
4438
+ placement,
4439
+ inner: parseOptionalUnitField(fieldMap.get("inner")?.[0], "inner"),
4440
+ outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer")
4441
+ };
4442
+ }
4443
+ function collectDraftFields(fields) {
4444
+ const grouped = /* @__PURE__ */ new Map();
4445
+ for (const field of fields) {
4446
+ const spec = getDraftObjectFieldSpec(field.key);
4447
+ if (!spec) {
4448
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
4449
+ }
4450
+ if (!spec.allowRepeat && grouped.has(field.key)) {
4451
+ throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
4452
+ }
4453
+ const existing = grouped.get(field.key) ?? [];
4454
+ existing.push(field);
4455
+ grouped.set(field.key, existing);
4456
+ }
4457
+ return grouped;
4458
+ }
4459
+ function extractPlacementFromFieldMap(fieldMap) {
4460
+ const orbitField = fieldMap.get("orbit")?.[0];
4461
+ const atField = fieldMap.get("at")?.[0];
4462
+ const surfaceField = fieldMap.get("surface")?.[0];
4463
+ const freeField = fieldMap.get("free")?.[0];
4464
+ const count = [orbitField, atField, surfaceField, freeField].filter(Boolean).length;
4465
+ if (count > 1) {
4466
+ const conflictingField = orbitField ?? atField ?? surfaceField ?? freeField;
4467
+ throw WorldOrbitError.fromLocation("Object has multiple placement modes", conflictingField?.location);
4468
+ }
4469
+ if (orbitField) {
4470
+ return {
4471
+ mode: "orbit",
4472
+ target: singleFieldValue2(orbitField),
4473
+ distance: parseOptionalUnitField(fieldMap.get("distance")?.[0], "distance"),
4474
+ semiMajor: parseOptionalUnitField(fieldMap.get("semiMajor")?.[0], "semiMajor"),
4475
+ eccentricity: parseOptionalNumberField(fieldMap.get("eccentricity")?.[0], "eccentricity"),
4476
+ period: parseOptionalUnitField(fieldMap.get("period")?.[0], "period"),
4477
+ angle: parseOptionalUnitField(fieldMap.get("angle")?.[0], "angle"),
4478
+ inclination: parseOptionalUnitField(fieldMap.get("inclination")?.[0], "inclination"),
4479
+ phase: parseOptionalUnitField(fieldMap.get("phase")?.[0], "phase")
4480
+ };
4481
+ }
4482
+ if (atField) {
4483
+ const target = singleFieldValue2(atField);
4484
+ return {
4485
+ mode: "at",
4486
+ target,
4487
+ reference: parseAtlasAtReference(target, atField.location)
4488
+ };
4489
+ }
4490
+ if (surfaceField) {
4491
+ return {
4492
+ mode: "surface",
4493
+ target: singleFieldValue2(surfaceField)
4494
+ };
4495
+ }
4496
+ if (freeField) {
4497
+ const raw = singleFieldValue2(freeField);
4498
+ const distance = tryParseAtlasUnitValue(raw);
4499
+ return {
4500
+ mode: "free",
4501
+ distance: distance ?? void 0,
4502
+ descriptor: distance ? void 0 : raw
4503
+ };
4504
+ }
4505
+ return null;
4506
+ }
4507
+ function normalizeDraftProperties(objectType, fieldMap) {
4508
+ const properties = {};
4509
+ for (const [key, fields] of fieldMap.entries()) {
4510
+ const field = fields[0];
4511
+ const spec = getDraftObjectFieldSpec(key);
4512
+ if (!field || !spec?.legacySchema || spec.legacySchema.placement) {
4513
+ continue;
4514
+ }
4515
+ ensureAtlasFieldSupported(key, objectType, field.location);
4516
+ properties[key] = normalizeLegacyScalarValue(key, field.values, field.location);
4517
+ }
4518
+ return properties;
4519
+ }
4520
+ function normalizeInfoEntries(entries, label) {
4521
+ const normalized = {};
4522
+ for (const entry of entries) {
4523
+ if (entry.key in normalized) {
4524
+ throw WorldOrbitError.fromLocation(`Duplicate ${label} key "${entry.key}"`, entry.location);
4525
+ }
4526
+ normalized[entry.key] = entry.value;
4527
+ }
4528
+ return normalized;
4529
+ }
4530
+ function normalizeTypedBlocks(typedBlockEntries) {
4531
+ const typedBlocks = {};
4532
+ for (const blockName of Object.keys(typedBlockEntries)) {
4533
+ const entries = typedBlockEntries[blockName];
4534
+ if (entries?.length) {
4535
+ typedBlocks[blockName] = normalizeInfoEntries(entries, blockName);
4536
+ }
4537
+ }
4538
+ return typedBlocks;
4539
+ }
4540
+ function extractRenderHints(fieldMap) {
4541
+ const renderHints = {};
4542
+ const renderLabelField = fieldMap.get("renderLabel")?.[0];
4543
+ const renderOrbitField = fieldMap.get("renderOrbit")?.[0];
4544
+ const renderPriorityField = fieldMap.get("renderPriority")?.[0];
4545
+ if (renderLabelField) {
4546
+ renderHints.renderLabel = parseAtlasBoolean(singleFieldValue2(renderLabelField), "renderLabel", renderLabelField.location);
4547
+ }
4548
+ if (renderOrbitField) {
4549
+ renderHints.renderOrbit = parseAtlasBoolean(singleFieldValue2(renderOrbitField), "renderOrbit", renderOrbitField.location);
4550
+ }
4551
+ if (renderPriorityField) {
4552
+ renderHints.renderPriority = parseAtlasNumber(singleFieldValue2(renderPriorityField), "renderPriority", renderPriorityField.location);
4553
+ }
4554
+ return Object.keys(renderHints).length > 0 ? renderHints : void 0;
4555
+ }
4556
+ function parseResonanceField(field) {
4557
+ if (field.values.length !== 2) {
4558
+ throw WorldOrbitError.fromLocation('Field "resonance" expects "<targetObjectId> <ratio>"', field.location);
4559
+ }
4560
+ const ratio = field.values[1];
4561
+ if (!/^\d+:\d+$/.test(ratio)) {
4562
+ throw WorldOrbitError.fromLocation(`Invalid resonance ratio "${ratio}"`, field.location);
4563
+ }
4564
+ return {
4565
+ targetObjectId: field.values[0],
4566
+ ratio
4567
+ };
4568
+ }
4569
+ function parseDeriveField(field) {
4570
+ if (field.values.length !== 2) {
4571
+ throw WorldOrbitError.fromLocation('Field "derive" expects "<field> <strategy>"', field.location);
4572
+ }
4573
+ return {
4574
+ field: field.values[0],
4575
+ strategy: field.values[1]
4576
+ };
4577
+ }
4578
+ function parseToleranceField(field) {
4579
+ if (field.values.length !== 2) {
4580
+ throw WorldOrbitError.fromLocation('Field "tolerance" expects "<field> <value>"', field.location);
4581
+ }
4582
+ const rawValue = field.values[1];
4583
+ const unitValue = tryParseAtlasUnitValue(rawValue);
4584
+ const numericValue2 = Number(rawValue);
4585
+ return {
4586
+ field: field.values[0],
4587
+ value: unitValue ?? (Number.isFinite(numericValue2) ? numericValue2 : rawValue)
4588
+ };
4589
+ }
4590
+ function parseOptionalTokenList(field) {
4591
+ return field ? [...new Set(field.values)] : [];
4592
+ }
4593
+ function parseOptionalJoinedValue(field) {
4594
+ if (!field) {
4595
+ return null;
4596
+ }
4597
+ return field.values.join(" ").trim() || null;
4598
+ }
4599
+ function parseOptionalUnitField(field, key) {
4600
+ return field ? parseAtlasUnitValue(singleFieldValue2(field), field.location, key) : void 0;
4601
+ }
4602
+ function parseOptionalNumberField(field, key) {
4603
+ return field ? parseAtlasNumber(singleFieldValue2(field), key, field.location) : void 0;
4604
+ }
4605
+ function singleFieldValue2(field) {
4606
+ return singleAtlasValue(field.values, field.key, field.location);
4607
+ }
4608
+ function getDraftObjectFieldSpec(key) {
4609
+ return DRAFT_OBJECT_FIELD_SPECS.get(key);
4610
+ }
4611
+ function validateDraftObjectFieldCompatibility(fields, objectType) {
4612
+ for (const field of fields) {
4613
+ const spec = getDraftObjectFieldSpec(field.key);
4614
+ if (!spec) {
4615
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
4616
+ }
4617
+ if (spec.legacySchema) {
4618
+ ensureAtlasFieldSupported(field.key, objectType, field.location);
4619
+ continue;
4620
+ }
4621
+ if ((field.key === "renderLabel" || field.key === "renderOrbit" || field.key === "tidalLock") && field.values.length !== 1) {
4622
+ throw WorldOrbitError.fromLocation(`Field "${field.key}" expects exactly one value`, field.location);
4623
+ }
4624
+ }
4625
+ }
4626
+ function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
4627
+ if (sourceSchemaVersion === "2.1") {
4628
+ return;
4629
+ }
4630
+ diagnostics.push({
4631
+ code: "parse.schema21.featureCompatibility",
4632
+ severity: "warning",
4633
+ source: "parse",
4634
+ message: `Feature "${featureName}" requires schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
4635
+ line: location.line,
4636
+ column: location.column
4637
+ });
4638
+ }
4639
+ function preprocessAtlasSource(source) {
4640
+ const chars = [...source];
4641
+ const comments = [];
4642
+ let inString = false;
4643
+ let inBlockComment = false;
4644
+ let blockCommentStart = null;
4645
+ let line = 1;
4646
+ let column = 1;
4647
+ for (let index = 0; index < chars.length; index++) {
4648
+ const ch = chars[index];
4649
+ const next = chars[index + 1];
4650
+ if (inBlockComment) {
4651
+ if (ch === "*" && next === "/") {
4652
+ chars[index] = " ";
4653
+ chars[index + 1] = " ";
4654
+ inBlockComment = false;
4655
+ blockCommentStart = null;
4656
+ index++;
4657
+ column += 2;
4658
+ continue;
4659
+ }
4660
+ if (ch !== "\n" && ch !== "\r") {
4661
+ chars[index] = " ";
4662
+ }
4663
+ if (ch === "\n") {
4664
+ line++;
4665
+ column = 1;
4666
+ } else {
4667
+ column++;
4668
+ }
4669
+ continue;
4670
+ }
4671
+ if (!inString && ch === "/" && next === "*") {
4672
+ comments.push({ kind: "block", line, column });
4673
+ chars[index] = " ";
4674
+ chars[index + 1] = " ";
4675
+ inBlockComment = true;
4676
+ blockCommentStart = { line, column };
4677
+ index++;
4678
+ column += 2;
4679
+ continue;
4680
+ }
4681
+ if (!inString && ch === "#" && !isHexColorLiteral(chars, index)) {
4682
+ comments.push({ kind: "line", line, column });
4683
+ chars[index] = " ";
4684
+ let inner = index + 1;
4685
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
4686
+ chars[inner] = " ";
4687
+ inner++;
4688
+ }
4689
+ column += inner - index;
4690
+ index = inner - 1;
4691
+ continue;
4692
+ }
4693
+ if (ch === '"' && chars[index - 1] !== "\\") {
4694
+ inString = !inString;
4695
+ }
4696
+ if (ch === "\n") {
4697
+ line++;
4698
+ column = 1;
4699
+ } else {
4700
+ column++;
4701
+ }
4702
+ }
4703
+ if (inBlockComment) {
4704
+ throw WorldOrbitError.fromLocation("Unclosed block comment", blockCommentStart ?? void 0);
4705
+ }
4706
+ return {
4707
+ source: chars.join(""),
4708
+ comments
4709
+ };
4710
+ }
4711
+ function isHexColorLiteral(chars, start) {
4712
+ let index = start + 1;
4713
+ let length = 0;
4714
+ while (index < chars.length && /[0-9a-f]/i.test(chars[index] ?? "")) {
4715
+ index++;
4716
+ length++;
4717
+ }
4718
+ if (![3, 4, 6, 8].includes(length)) {
4719
+ return false;
4720
+ }
4721
+ const next = chars[index];
4722
+ return next === void 0 || next === " " || next === " " || next === "\r" || next === "\n";
3030
4723
  }
3031
4724
 
3032
4725
  // packages/core/dist/load.js
3033
- var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0)?$/i;
4726
+ var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1)?$/i;
4727
+ var ATLAS_SCHEMA_21_PATTERN = /^schema\s+2\.1$/i;
3034
4728
  var LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
3035
4729
  function detectWorldOrbitSchemaVersion(source) {
3036
- for (const line of source.split(/\r?\n/)) {
4730
+ for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
3037
4731
  const trimmed = line.trim();
3038
4732
  if (!trimmed) {
3039
4733
  continue;
@@ -3041,6 +4735,9 @@ var WorldOrbit = (() => {
3041
4735
  if (LEGACY_DRAFT_SCHEMA_PATTERN.test(trimmed)) {
3042
4736
  return "2.0-draft";
3043
4737
  }
4738
+ if (ATLAS_SCHEMA_21_PATTERN.test(trimmed)) {
4739
+ return "2.1";
4740
+ }
3044
4741
  if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
3045
4742
  return "2.0";
3046
4743
  }
@@ -3048,6 +4745,49 @@ var WorldOrbit = (() => {
3048
4745
  }
3049
4746
  return "1.0";
3050
4747
  }
4748
+ function stripCommentsForSchemaDetection(source) {
4749
+ const chars = [...source];
4750
+ let inString = false;
4751
+ let inBlockComment = false;
4752
+ for (let index = 0; index < chars.length; index++) {
4753
+ const ch = chars[index];
4754
+ const next = chars[index + 1];
4755
+ if (inBlockComment) {
4756
+ if (ch === "*" && next === "/") {
4757
+ chars[index] = " ";
4758
+ chars[index + 1] = " ";
4759
+ inBlockComment = false;
4760
+ index++;
4761
+ continue;
4762
+ }
4763
+ if (ch !== "\n" && ch !== "\r") {
4764
+ chars[index] = " ";
4765
+ }
4766
+ continue;
4767
+ }
4768
+ if (!inString && ch === "/" && next === "*") {
4769
+ chars[index] = " ";
4770
+ chars[index + 1] = " ";
4771
+ inBlockComment = true;
4772
+ index++;
4773
+ continue;
4774
+ }
4775
+ if (!inString && ch === "#") {
4776
+ chars[index] = " ";
4777
+ let inner = index + 1;
4778
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
4779
+ chars[inner] = " ";
4780
+ inner++;
4781
+ }
4782
+ index = inner - 1;
4783
+ continue;
4784
+ }
4785
+ if (ch === '"' && chars[index - 1] !== "\\") {
4786
+ inString = !inString;
4787
+ }
4788
+ }
4789
+ return chars.join("");
4790
+ }
3051
4791
  function loadWorldOrbitSource(source) {
3052
4792
  const result = loadWorldOrbitSourceWithDiagnostics(source);
3053
4793
  if (!result.ok || !result.value) {
@@ -3058,36 +4798,36 @@ var WorldOrbit = (() => {
3058
4798
  }
3059
4799
  function loadWorldOrbitSourceWithDiagnostics(source) {
3060
4800
  const schemaVersion = detectWorldOrbitSchemaVersion(source);
3061
- if (schemaVersion === "2.0" || schemaVersion === "2.0-draft") {
4801
+ if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1") {
3062
4802
  return loadAtlasSourceWithDiagnostics(source, schemaVersion);
3063
4803
  }
3064
4804
  let ast;
3065
4805
  try {
3066
4806
  ast = parseWorldOrbit(source);
3067
- } catch (error) {
4807
+ } catch (error2) {
3068
4808
  return {
3069
4809
  ok: false,
3070
4810
  value: null,
3071
- diagnostics: [diagnosticFromError(error, "parse")]
4811
+ diagnostics: [diagnosticFromError(error2, "parse")]
3072
4812
  };
3073
4813
  }
3074
4814
  let document2;
3075
4815
  try {
3076
4816
  document2 = normalizeDocument(ast);
3077
- } catch (error) {
4817
+ } catch (error2) {
3078
4818
  return {
3079
4819
  ok: false,
3080
4820
  value: null,
3081
- diagnostics: [diagnosticFromError(error, "normalize")]
4821
+ diagnostics: [diagnosticFromError(error2, "normalize")]
3082
4822
  };
3083
4823
  }
3084
4824
  try {
3085
4825
  validateDocument(document2);
3086
- } catch (error) {
4826
+ } catch (error2) {
3087
4827
  return {
3088
4828
  ok: false,
3089
4829
  value: null,
3090
- diagnostics: [diagnosticFromError(error, "validate")]
4830
+ diagnostics: [diagnosticFromError(error2, "validate")]
3091
4831
  };
3092
4832
  }
3093
4833
  return {
@@ -3107,30 +4847,29 @@ var WorldOrbit = (() => {
3107
4847
  let atlasDocument;
3108
4848
  try {
3109
4849
  atlasDocument = parseWorldOrbitAtlas(source);
3110
- } catch (error) {
4850
+ } catch (error2) {
3111
4851
  return {
3112
4852
  ok: false,
3113
4853
  value: null,
3114
- diagnostics: [diagnosticFromError(error, "parse", "load.atlas.failed")]
4854
+ diagnostics: [diagnosticFromError(error2, "parse", "load.atlas.failed")]
3115
4855
  };
3116
4856
  }
3117
- let document2;
3118
- try {
3119
- document2 = materializeAtlasDocument(atlasDocument);
3120
- } catch (error) {
4857
+ const atlasDiagnostics = [...atlasDocument.diagnostics];
4858
+ if (atlasDiagnostics.some((diagnostic) => diagnostic.severity === "error")) {
3121
4859
  return {
3122
4860
  ok: false,
3123
4861
  value: null,
3124
- diagnostics: [diagnosticFromError(error, "normalize", "load.atlas.materialize.failed")]
4862
+ diagnostics: atlasDiagnostics
3125
4863
  };
3126
4864
  }
4865
+ let document2;
3127
4866
  try {
3128
- validateDocument(document2);
3129
- } catch (error) {
4867
+ document2 = materializeAtlasDocument(atlasDocument);
4868
+ } catch (error2) {
3130
4869
  return {
3131
4870
  ok: false,
3132
4871
  value: null,
3133
- diagnostics: [diagnosticFromError(error, "validate", "load.atlas.validate.failed")]
4872
+ diagnostics: [diagnosticFromError(error2, "normalize", "load.atlas.materialize.failed")]
3134
4873
  };
3135
4874
  }
3136
4875
  const loaded = {
@@ -3139,12 +4878,12 @@ var WorldOrbit = (() => {
3139
4878
  document: document2,
3140
4879
  atlasDocument,
3141
4880
  draftDocument: atlasDocument,
3142
- diagnostics: [...atlasDocument.diagnostics]
4881
+ diagnostics: atlasDiagnostics
3143
4882
  };
3144
4883
  return {
3145
4884
  ok: true,
3146
4885
  value: loaded,
3147
- diagnostics: [...atlasDocument.diagnostics]
4886
+ diagnostics: atlasDiagnostics
3148
4887
  };
3149
4888
  }
3150
4889
 
@@ -3152,6 +4891,8 @@ var WorldOrbit = (() => {
3152
4891
  var DEFAULT_LAYERS = {
3153
4892
  background: true,
3154
4893
  guides: true,
4894
+ relations: true,
4895
+ events: true,
3155
4896
  orbits: true,
3156
4897
  objects: true,
3157
4898
  labels: true,
@@ -3166,6 +4907,7 @@ var WorldOrbit = (() => {
3166
4907
  backgroundGlow: "rgba(240, 180, 100, 0.18)",
3167
4908
  panel: "rgba(7, 17, 27, 0.9)",
3168
4909
  panelLine: "rgba(168, 207, 242, 0.18)",
4910
+ relation: "rgba(240, 180, 100, 0.42)",
3169
4911
  orbit: "rgba(163, 209, 255, 0.24)",
3170
4912
  orbitBand: "rgba(255, 190, 120, 0.28)",
3171
4913
  guide: "rgba(255, 255, 255, 0.04)",
@@ -3188,6 +4930,7 @@ var WorldOrbit = (() => {
3188
4930
  backgroundGlow: "rgba(120, 255, 215, 0.16)",
3189
4931
  panel: "rgba(7, 20, 30, 0.9)",
3190
4932
  panelLine: "rgba(120, 255, 215, 0.16)",
4933
+ relation: "rgba(156, 231, 255, 0.42)",
3191
4934
  orbit: "rgba(120, 255, 215, 0.2)",
3192
4935
  orbitBand: "rgba(137, 185, 255, 0.24)",
3193
4936
  guide: "rgba(255, 255, 255, 0.035)",
@@ -3210,6 +4953,7 @@ var WorldOrbit = (() => {
3210
4953
  backgroundGlow: "rgba(255, 127, 95, 0.18)",
3211
4954
  panel: "rgba(24, 9, 13, 0.9)",
3212
4955
  panelLine: "rgba(255, 166, 149, 0.16)",
4956
+ relation: "rgba(255, 178, 125, 0.42)",
3213
4957
  orbit: "rgba(255, 188, 164, 0.22)",
3214
4958
  orbitBand: "rgba(255, 214, 139, 0.24)",
3215
4959
  guide: "rgba(255, 255, 255, 0.03)",
@@ -3291,7 +5035,11 @@ var WorldOrbit = (() => {
3291
5035
  return false;
3292
5036
  }
3293
5037
  if (filter.groupIds?.length && (!object.groupId || !filter.groupIds.includes(object.groupId))) {
3294
- return false;
5038
+ const hasSemanticMatch = object.semanticGroupIds.length > 0 && filter.groupIds.some((groupId) => object.semanticGroupIds.includes(groupId));
5039
+ const hasLegacyMatch = Boolean(object.groupId && filter.groupIds.includes(object.groupId));
5040
+ if (!hasSemanticMatch && !hasLegacyMatch) {
5041
+ return false;
5042
+ }
3295
5043
  }
3296
5044
  if (filter.tags?.length) {
3297
5045
  const objectTags = Array.isArray(object.object.properties.tags) ? object.object.properties.tags.filter((entry) => typeof entry === "string") : [];
@@ -3347,6 +5095,8 @@ var WorldOrbit = (() => {
3347
5095
  const imageDefinitions = buildImageDefinitions(visibleObjects);
3348
5096
  const orbitMarkup = layers.orbits ? renderOrbitLayer(scene, visibleObjectIds, layers.structures) : { back: "", front: "" };
3349
5097
  const leaderMarkup = layers.guides ? scene.leaders.filter((leader) => !leader.hidden).filter((leader) => visibleObjectIds.has(leader.objectId)).filter((leader) => layers.structures || !isStructureLike(leader.object)).map((leader) => `<line class="wo-leader wo-leader-${leader.mode}" x1="${leader.x1}" y1="${leader.y1}" x2="${leader.x2}" y2="${leader.y2}" data-render-id="${escapeXml(leader.renderId)}" data-group-id="${escapeAttribute(leader.groupId ?? "")}" />`).join("") : "";
5098
+ const relationMarkup = layers.relations ? scene.relations.filter((relation) => !relation.hidden).filter((relation) => visibleObjectIds.has(relation.fromObjectId) && visibleObjectIds.has(relation.toObjectId)).map((relation) => `<line class="wo-relation" x1="${relation.x1}" y1="${relation.y1}" x2="${relation.x2}" y2="${relation.y2}" data-render-id="${escapeXml(relation.renderId)}" data-relation-id="${escapeAttribute(relation.relationId)}" />`).join("") : "";
5099
+ const eventMarkup = layers.events ? scene.events.filter((event) => !event.hidden).map((event) => renderSceneEventOverlay(scene, event, visibleObjectIds, theme)).join("") : "";
3350
5100
  const objectMarkup = layers.objects ? visibleObjects.map((object) => renderSceneObject(object, options.selectedObjectId ?? null, theme)).join("") : "";
3351
5101
  const labelMarkup = layers.labels ? visibleLabels.map((label) => renderSceneLabel(scene, label, options.selectedObjectId ?? null)).join("") : "";
3352
5102
  const metadataMarkup = layers.metadata ? `<text class="wo-title" x="56" y="64">${escapeXml(scene.title)}</text>
@@ -3381,6 +5131,10 @@ var WorldOrbit = (() => {
3381
5131
  .wo-orbit-back { opacity: 0.38; stroke-dasharray: 8 6; }
3382
5132
  .wo-orbit-front { opacity: 0.9; }
3383
5133
  .wo-orbit-band { stroke: ${theme.orbitBand}; stroke-linecap: round; }
5134
+ .wo-relation { stroke: ${theme.relation}; stroke-width: 2; stroke-dasharray: 10 6; }
5135
+ .wo-event-line { stroke: ${theme.accent}; stroke-width: 1.6; stroke-dasharray: 5 5; opacity: 0.72; }
5136
+ .wo-event-node { fill: ${theme.accent}; stroke: ${theme.selected}; stroke-width: 1.4; opacity: 0.92; }
5137
+ .wo-event-label { fill: ${theme.accent}; font-family: ${theme.fontFamily}; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
3384
5138
  .wo-leader { stroke: ${theme.leader}; stroke-width: 1.5; stroke-dasharray: 6 5; }
3385
5139
  .wo-label { fill: ${theme.ink}; font-family: ${theme.fontFamily}; font-weight: 600; letter-spacing: 0.02em; }
3386
5140
  .wo-label-secondary { fill: ${theme.muted}; font-family: ${theme.fontFamily}; font-weight: 500; }
@@ -3414,6 +5168,8 @@ var WorldOrbit = (() => {
3414
5168
  <g data-worldorbit-world-content="true">
3415
5169
  ${layers.orbits ? `<g data-layer-id="orbits-back">${orbitMarkup.back}</g>` : ""}
3416
5170
  ${layers.guides ? `<g data-layer-id="guides">${leaderMarkup}</g>` : ""}
5171
+ ${layers.relations ? `<g data-layer-id="relations">${relationMarkup}</g>` : ""}
5172
+ ${layers.events ? `<g data-layer-id="events">${eventMarkup}</g>` : ""}
3417
5173
  ${layers.objects ? `<g data-layer-id="objects">${objectMarkup}</g>` : ""}
3418
5174
  ${layers.orbits ? `<g data-layer-id="orbits-front">${orbitMarkup.front}</g>` : ""}
3419
5175
  ${layers.labels ? `<g data-layer-id="labels">${labelMarkup}</g>` : ""}
@@ -3421,6 +5177,20 @@ var WorldOrbit = (() => {
3421
5177
  </g>
3422
5178
  </g>
3423
5179
  </svg>`;
5180
+ }
5181
+ function renderSceneEventOverlay(scene, event, visibleObjectIds, theme) {
5182
+ const participants = event.objectIds.filter((objectId) => visibleObjectIds.has(objectId)).map((objectId) => scene.objects.find((object) => object.objectId === objectId && !object.hidden)).filter(Boolean);
5183
+ if (participants.length === 0) {
5184
+ return "";
5185
+ }
5186
+ const stroke = event.event.color || theme.accent;
5187
+ const label = event.event.label || event.event.id;
5188
+ const lineMarkup = participants.map((object) => `<line class="wo-event-line" x1="${event.x}" y1="${event.y}" x2="${object.x}" y2="${object.y}" stroke="${escapeAttribute(stroke)}" data-event-id="${escapeAttribute(event.eventId)}" data-object-id="${escapeAttribute(object.objectId)}" />`).join("");
5189
+ return `<g class="wo-event" data-render-id="${escapeXml(event.renderId)}" data-event-id="${escapeAttribute(event.eventId)}">
5190
+ ${lineMarkup}
5191
+ <circle class="wo-event-node" cx="${event.x}" cy="${event.y}" r="5" fill="${escapeAttribute(stroke)}" />
5192
+ <text class="wo-event-label" x="${event.x}" y="${event.y - 10}" text-anchor="middle" font-size="10">${escapeXml(label)}</text>
5193
+ </g>`;
3424
5194
  }
3425
5195
  function renderOrbitLayer(scene, visibleObjectIds, includeStructures) {
3426
5196
  const backParts = [];
@@ -3457,10 +5227,11 @@ var WorldOrbit = (() => {
3457
5227
  function renderSceneObject(sceneObject, selectedObjectId, theme) {
3458
5228
  const { object, x, y, radius, visualRadius } = sceneObject;
3459
5229
  const selectionClass = selectedObjectId === sceneObject.objectId ? " wo-object-selected" : "";
5230
+ const kindClass = object.properties.kind ? ` wo-kind-${String(object.properties.kind).toLowerCase().replace(/[^a-z0-9-]/g, "-")}` : "";
3460
5231
  const palette = resolveObjectPalette(sceneObject, theme);
3461
5232
  const imageMarkup = renderObjectImage(sceneObject);
3462
5233
  const outlineMarkup = imageMarkup ? renderObjectBody(object, x, y, radius, palette, { outlineOnly: true }) : "";
3463
- return `<g class="wo-object wo-object-${object.type}${selectionClass}" data-object-id="${escapeXml(sceneObject.objectId)}" data-parent-id="${escapeAttribute(sceneObject.parentId ?? "")}" data-group-id="${escapeAttribute(sceneObject.groupId ?? "")}" data-render-id="${escapeXml(sceneObject.renderId)}" tabindex="0" role="button" aria-label="${escapeXml(`${object.type} ${sceneObject.objectId}`)}">
5234
+ return `<g class="wo-object wo-object-${object.type}${kindClass}${selectionClass}" data-object-id="${escapeXml(sceneObject.objectId)}" data-parent-id="${escapeAttribute(sceneObject.parentId ?? "")}" data-group-id="${escapeAttribute(sceneObject.groupId ?? "")}" data-render-id="${escapeXml(sceneObject.renderId)}" tabindex="0" role="button" aria-label="${escapeXml(`${object.type} ${sceneObject.objectId}`)}">
3464
5235
  <circle class="wo-selection-ring" cx="${x}" cy="${y}" r="${visualRadius + 8}" />
3465
5236
  ${renderAtmosphere(sceneObject, palette)}
3466
5237
  ${renderObjectBody(object, x, y, radius, palette)}
@@ -3494,8 +5265,33 @@ var WorldOrbit = (() => {
3494
5265
  <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
3495
5266
  case "structure":
3496
5267
  return `<polygon points="${diamondPoints(x, y, radius)}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
3497
- case "phenomenon":
5268
+ case "phenomenon": {
5269
+ const kind = String(object.properties.kind ?? "").toLowerCase().replace(/_/g, "-");
5270
+ if (options.outlineOnly) {
5271
+ if (kind === "black-hole" || kind === "nebula" || kind === "galaxy" || kind === "dwarf-galaxy") {
5272
+ return `<circle cx="${x}" cy="${y}" r="${radius}" fill="transparent" stroke="${palette.stroke}" stroke-width="1.4" />`;
5273
+ }
5274
+ return `<polygon points="${phenomenonPoints(x, y, radius)}" fill="transparent" stroke="${palette.stroke}" stroke-width="1.4" />`;
5275
+ }
5276
+ if (kind === "black-hole") {
5277
+ return `<ellipse cx="${x}" cy="${y}" rx="${radius * 2.4}" ry="${radius * 0.55}" fill="none" stroke="${palette.accentRing ?? palette.stroke}" stroke-width="3.5" />
5278
+ <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="2" />`;
5279
+ }
5280
+ if (kind === "galaxy") {
5281
+ return `<ellipse cx="${x}" cy="${y}" rx="${radius * 2.6}" ry="${radius}" fill="${palette.halo ?? "none"}" stroke="none" />
5282
+ <ellipse cx="${x}" cy="${y}" rx="${radius * 1.5}" ry="${radius * 0.42}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.2" />
5283
+ <circle cx="${x}" cy="${y}" r="${radius * 0.28}" fill="${palette.core ?? "#fff"}" stroke="none" />`;
5284
+ }
5285
+ if (kind === "dwarf-galaxy") {
5286
+ return `<ellipse cx="${x}" cy="${y}" rx="${radius * 1.6}" ry="${radius * 0.55}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1" />
5287
+ <circle cx="${x}" cy="${y}" r="${radius * 0.25}" fill="${palette.core ?? "#fff"}" stroke="none" />`;
5288
+ }
5289
+ if (kind === "nebula") {
5290
+ return `<circle cx="${x}" cy="${y}" r="${radius * 2.2}" fill="${palette.halo ?? "none"}" stroke="none" />
5291
+ <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1" />`;
5292
+ }
3498
5293
  return `<polygon points="${phenomenonPoints(x, y, radius)}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
5294
+ }
3499
5295
  }
3500
5296
  }
3501
5297
  function renderAtmosphere(sceneObject, palette) {
@@ -3564,7 +5360,8 @@ var WorldOrbit = (() => {
3564
5360
  }
3565
5361
  }
3566
5362
  function resolveObjectPalette(sceneObject, theme) {
3567
- const base = basePaletteForType(sceneObject.object.type, theme);
5363
+ const kind = String(sceneObject.object.properties.kind ?? "").toLowerCase().replace(/_/g, "-");
5364
+ const base = basePaletteForType(sceneObject.object.type, kind, theme);
3568
5365
  const customFill = sceneObject.fillColor && isColorLike(sceneObject.fillColor) ? sceneObject.fillColor : base.fill;
3569
5366
  const albedo = numericValue(sceneObject.object.properties.albedo);
3570
5367
  const temperature = numericValue(sceneObject.object.properties.temperature);
@@ -3580,7 +5377,7 @@ var WorldOrbit = (() => {
3580
5377
  tail: sceneObject.object.type === "comet" ? rgbaString(mixColors(fill, "#ffffff", 0.5) ?? fill, 0.72) : void 0
3581
5378
  };
3582
5379
  }
3583
- function basePaletteForType(type, theme) {
5380
+ function basePaletteForType(type, kind, theme) {
3584
5381
  switch (type) {
3585
5382
  case "star":
3586
5383
  return {
@@ -3602,8 +5399,26 @@ var WorldOrbit = (() => {
3602
5399
  case "structure":
3603
5400
  return { fill: theme.accentStrong, stroke: "#fff2ea" };
3604
5401
  case "phenomenon":
3605
- return { fill: "#78ffd7", stroke: "#e9fff7" };
5402
+ return kindPhenomenonPalette(kind);
5403
+ }
5404
+ }
5405
+ function kindPhenomenonPalette(kind) {
5406
+ if (kind === "galaxy") {
5407
+ return { fill: "rgba(165,125,255,0.55)", stroke: "rgba(210,185,255,0.75)", halo: "rgba(160,120,255,0.10)", core: "#ede0ff" };
5408
+ }
5409
+ if (kind === "dwarf-galaxy") {
5410
+ return { fill: "rgba(190,165,255,0.45)", stroke: "rgba(220,205,255,0.75)", core: "#ddd0ff" };
5411
+ }
5412
+ if (kind === "black-hole") {
5413
+ return { fill: "#040408", stroke: "#ff6a00", accentRing: "rgba(255,140,20,0.72)" };
5414
+ }
5415
+ if (kind === "nebula") {
5416
+ return { fill: "rgba(105,205,255,0.45)", stroke: "rgba(180,235,255,0.72)", halo: "rgba(100,200,255,0.08)" };
5417
+ }
5418
+ if (kind === "void") {
5419
+ return { fill: "#05080f", stroke: "rgba(130,160,255,0.4)" };
3606
5420
  }
5421
+ return { fill: "#78ffd7", stroke: "#e9fff7" };
3607
5422
  }
3608
5423
  function applyTemperatureAndAlbedo(baseColor, temperature, albedo, type) {
3609
5424
  let nextColor = baseColor;
@@ -3870,11 +5685,11 @@ var WorldOrbit = (() => {
3870
5685
  });
3871
5686
  }
3872
5687
  return `<figure class="${escapeAttribute3(options.className ?? "worldorbit-block worldorbit-static")}">${renderSceneToSvg(scene, options)}</figure>`;
3873
- } catch (error) {
5688
+ } catch (error2) {
3874
5689
  if (options.strict) {
3875
- throw error;
5690
+ throw error2;
3876
5691
  }
3877
- return renderWorldOrbitError(error instanceof Error ? error.message : String(error));
5692
+ return renderWorldOrbitError(error2 instanceof Error ? error2.message : String(error2));
3878
5693
  }
3879
5694
  }
3880
5695
  function renderWorldOrbitError(message) {
@@ -3975,5 +5790,5 @@ var WorldOrbit = (() => {
3975
5790
  }
3976
5791
  return (node.children ?? []).map((child) => collectText(child)).join("");
3977
5792
  }
3978
- return __toCommonJS(dist_exports);
5793
+ return __toCommonJS(index_exports);
3979
5794
  })();