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
@@ -302,13 +302,13 @@
302
302
  function unitFamilyAllowsUnit(family, unit) {
303
303
  switch (family) {
304
304
  case "distance":
305
- return unit === null || ["au", "km", "re", "sol"].includes(unit);
305
+ return unit === null || ["au", "km", "m", "ly", "pc", "kpc", "re", "sol"].includes(unit);
306
306
  case "radius":
307
- return unit === null || ["km", "re", "sol"].includes(unit);
307
+ return unit === null || ["km", "m", "re", "rj", "sol"].includes(unit);
308
308
  case "mass":
309
- return unit === null || ["me", "sol"].includes(unit);
309
+ return unit === null || ["me", "mj", "sol"].includes(unit);
310
310
  case "duration":
311
- return unit === null || ["h", "d", "y"].includes(unit);
311
+ return unit === null || ["s", "min", "h", "d", "y", "ky", "my", "gy"].includes(unit);
312
312
  case "angle":
313
313
  return unit === null || unit === "deg";
314
314
  case "generic":
@@ -512,7 +512,7 @@
512
512
  }
513
513
 
514
514
  // packages/core/dist/normalize.js
515
- var UNIT_PATTERN = /^(-?\d+(?:\.\d+)?)(au|km|re|sol|me|d|y|h|deg)?$/;
515
+ var UNIT_PATTERN = /^(-?\d+(?:\.\d+)?)(kpc|min|mj|rj|ky|my|gy|au|km|me|re|pc|ly|deg|sol|K|m|s|h|d|y)?$/;
516
516
  var BOOLEAN_VALUES = /* @__PURE__ */ new Map([
517
517
  ["true", true],
518
518
  ["false", false],
@@ -537,7 +537,11 @@
537
537
  return {
538
538
  format: "worldorbit",
539
539
  version: "1.0",
540
+ schemaVersion: "1.0",
540
541
  system,
542
+ groups: [],
543
+ relations: [],
544
+ events: [],
541
545
  objects
542
546
  };
543
547
  }
@@ -547,13 +551,17 @@
547
551
  const fieldMap = collectFields(mergedFields);
548
552
  const placement = extractPlacement(node.objectType, fieldMap);
549
553
  const properties = normalizeProperties(fieldMap);
550
- const info = normalizeInfo(node.infoEntries);
554
+ const info2 = normalizeInfo(node.infoEntries);
551
555
  if (node.objectType === "system") {
552
556
  return {
553
557
  type: "system",
554
558
  id: node.name,
559
+ title: typeof properties.title === "string" ? properties.title : null,
560
+ description: null,
561
+ epoch: null,
562
+ referencePlane: null,
555
563
  properties,
556
- info
564
+ info: info2
557
565
  };
558
566
  }
559
567
  return {
@@ -561,7 +569,7 @@
561
569
  id: node.name,
562
570
  properties,
563
571
  placement,
564
- info
572
+ info: info2
565
573
  };
566
574
  }
567
575
  function validateFieldCompatibility(objectType, fields) {
@@ -691,14 +699,14 @@
691
699
  }
692
700
  }
693
701
  function normalizeInfo(entries) {
694
- const info = {};
702
+ const info2 = {};
695
703
  for (const entry of entries) {
696
- if (entry.key in info) {
704
+ if (entry.key in info2) {
697
705
  throw WorldOrbitError.fromLocation(`Duplicate info key "${entry.key}"`, entry.location);
698
706
  }
699
- info[entry.key] = entry.value;
707
+ info2[entry.key] = entry.value;
700
708
  }
701
- return info;
709
+ return info2;
702
710
  }
703
711
  function parseAtReference(target, location) {
704
712
  if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
@@ -869,37 +877,41 @@
869
877
  }
870
878
 
871
879
  // packages/core/dist/diagnostics.js
872
- function diagnosticFromError(error, source, code = `${source}.failed`) {
873
- if (error instanceof WorldOrbitError) {
880
+ function diagnosticFromError(error2, source, code = `${source}.failed`) {
881
+ if (error2 instanceof WorldOrbitError) {
874
882
  return {
875
883
  code,
876
884
  severity: "error",
877
885
  source,
878
- message: error.message,
879
- line: error.line,
880
- column: error.column
886
+ message: error2.message,
887
+ line: error2.line,
888
+ column: error2.column
881
889
  };
882
890
  }
883
- if (error instanceof Error) {
891
+ if (error2 instanceof Error) {
884
892
  return {
885
893
  code,
886
894
  severity: "error",
887
895
  source,
888
- message: error.message
896
+ message: error2.message
889
897
  };
890
898
  }
891
899
  return {
892
900
  code,
893
901
  severity: "error",
894
902
  source,
895
- message: String(error)
903
+ message: String(error2)
896
904
  };
897
905
  }
898
906
 
899
907
  // packages/core/dist/scene.js
900
908
  var AU_IN_KM = 1495978707e-1;
901
909
  var EARTH_RADIUS_IN_KM = 6371;
910
+ var JUPITER_RADIUS_IN_KM = 71492;
902
911
  var SOLAR_RADIUS_IN_KM = 695700;
912
+ var LY_IN_AU = 63241.077;
913
+ var PC_IN_AU = 206264.806;
914
+ var KPC_IN_AU = 206264806;
903
915
  var ISO_FLATTENING = 0.68;
904
916
  var MIN_ISO_MINOR_SCALE = 0.2;
905
917
  var ARC_SAMPLE_COUNT = 28;
@@ -913,8 +925,10 @@
913
925
  const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
914
926
  const spacingFactor = layoutPresetSpacing(layoutPreset);
915
927
  const systemId = document2.system?.id ?? null;
916
- const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
917
- const relationships = buildSceneRelationships(document2.objects, objectMap);
928
+ const activeEventId = options.activeEventId ?? null;
929
+ const effectiveObjects = createEffectiveObjects(document2.objects, document2.events ?? [], activeEventId);
930
+ const objectMap = new Map(effectiveObjects.map((object) => [object.id, object]));
931
+ const relationships = buildSceneRelationships(effectiveObjects, objectMap);
918
932
  const positions = /* @__PURE__ */ new Map();
919
933
  const orbitDrafts = [];
920
934
  const leaderDrafts = [];
@@ -923,7 +937,7 @@
923
937
  const atObjects = [];
924
938
  const surfaceChildren = /* @__PURE__ */ new Map();
925
939
  const orbitChildren = /* @__PURE__ */ new Map();
926
- for (const object of document2.objects) {
940
+ for (const object of effectiveObjects) {
927
941
  const placement = object.placement;
928
942
  if (!placement) {
929
943
  rootObjects.push(object);
@@ -1018,11 +1032,14 @@
1018
1032
  const objects = [...positions.values()].map((position) => createSceneObject(position, scaleModel, relationships));
1019
1033
  const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
1020
1034
  const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
1021
- const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
1022
- const layers = createSceneLayers(orbitVisuals, leaders, objects, labels);
1023
- const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
1035
+ const labels = createSceneLabels(objects, width, height, scaleModel.labelMultiplier);
1036
+ const relations = createSceneRelations(document2, objects);
1037
+ const events = createSceneEvents(document2.events ?? [], objects, activeEventId);
1038
+ const layers = createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels);
1039
+ const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, scaleModel.labelMultiplier);
1040
+ const semanticGroups = createSceneSemanticGroups(document2, objects);
1024
1041
  const viewpoints = createSceneViewpoints(document2, projection, frame.preset, relationships, objectMap);
1025
- const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
1042
+ const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, scaleModel.labelMultiplier);
1026
1043
  return {
1027
1044
  width,
1028
1045
  height,
@@ -1030,7 +1047,7 @@
1030
1047
  renderPreset: frame.preset,
1031
1048
  projection,
1032
1049
  scaleModel,
1033
- title: String(document2.system?.properties.title ?? document2.system?.id ?? "WorldOrbit") || "WorldOrbit",
1050
+ title: String(document2.system?.title ?? document2.system?.properties.title ?? document2.system?.id ?? "WorldOrbit") || "WorldOrbit",
1034
1051
  subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
1035
1052
  systemId,
1036
1053
  viewMode: projection,
@@ -1046,13 +1063,46 @@
1046
1063
  contentBounds,
1047
1064
  layers,
1048
1065
  groups,
1066
+ semanticGroups,
1049
1067
  viewpoints,
1068
+ events,
1069
+ activeEventId,
1050
1070
  objects,
1051
1071
  orbitVisuals,
1072
+ relations,
1052
1073
  leaders,
1053
1074
  labels
1054
1075
  };
1055
1076
  }
1077
+ function createEffectiveObjects(objects, events, activeEventId) {
1078
+ const cloned = objects.map((object) => structuredClone(object));
1079
+ if (!activeEventId) {
1080
+ return cloned;
1081
+ }
1082
+ const activeEvent = events.find((event) => event.id === activeEventId);
1083
+ if (!activeEvent) {
1084
+ return cloned;
1085
+ }
1086
+ const objectMap = new Map(cloned.map((object) => [object.id, object]));
1087
+ for (const pose of activeEvent.positions) {
1088
+ const object = objectMap.get(pose.objectId);
1089
+ if (!object) {
1090
+ continue;
1091
+ }
1092
+ object.placement = pose.placement ? structuredClone(pose.placement) : null;
1093
+ if (pose.inner) {
1094
+ object.properties.inner = { ...pose.inner };
1095
+ } else {
1096
+ delete object.properties.inner;
1097
+ }
1098
+ if (pose.outer) {
1099
+ object.properties.outer = { ...pose.outer };
1100
+ } else {
1101
+ delete object.properties.outer;
1102
+ }
1103
+ }
1104
+ return cloned;
1105
+ }
1056
1106
  function resolveLayoutPreset(document2) {
1057
1107
  const rawScale = String(document2.system?.properties.scale ?? "balanced").toLowerCase();
1058
1108
  switch (rawScale) {
@@ -1147,6 +1197,7 @@
1147
1197
  }
1148
1198
  function createSceneObject(position, scaleModel, relationships) {
1149
1199
  const { object, x, y, radius, sortKey, anchorX, anchorY } = position;
1200
+ const renderPriority = object.renderHints?.renderPriority ?? 0;
1150
1201
  return {
1151
1202
  renderId: createRenderId(object.id),
1152
1203
  objectId: object.id,
@@ -1155,11 +1206,12 @@
1155
1206
  ancestorIds: relationships.ancestorIds.get(object.id) ?? [],
1156
1207
  childIds: relationships.childIds.get(object.id) ?? [],
1157
1208
  groupId: relationships.groupIds.get(object.id) ?? null,
1209
+ semanticGroupIds: [...object.groups ?? []],
1158
1210
  x,
1159
1211
  y,
1160
1212
  radius,
1161
1213
  visualRadius: visualExtentForObject(object, radius, scaleModel),
1162
- sortKey,
1214
+ sortKey: sortKey + renderPriority * 1e-3,
1163
1215
  anchorX,
1164
1216
  anchorY,
1165
1217
  label: object.id,
@@ -1176,6 +1228,7 @@
1176
1228
  object: draft.object,
1177
1229
  parentId: draft.parentId,
1178
1230
  groupId,
1231
+ semanticGroupIds: [...draft.object.groups ?? []],
1179
1232
  kind: draft.kind,
1180
1233
  cx: draft.cx,
1181
1234
  cy: draft.cy,
@@ -1187,7 +1240,7 @@
1187
1240
  bandThickness: draft.bandThickness,
1188
1241
  frontArcPath: draft.frontArcPath,
1189
1242
  backArcPath: draft.backArcPath,
1190
- hidden: draft.object.properties.hidden === true
1243
+ hidden: draft.object.properties.hidden === true || draft.object.renderHints?.renderOrbit === false
1191
1244
  };
1192
1245
  }
1193
1246
  function createLeaderLine(draft) {
@@ -1196,6 +1249,7 @@
1196
1249
  objectId: draft.object.id,
1197
1250
  object: draft.object,
1198
1251
  groupId: draft.groupId,
1252
+ semanticGroupIds: [...draft.object.groups ?? []],
1199
1253
  x1: draft.x1,
1200
1254
  y1: draft.y1,
1201
1255
  x2: draft.x2,
@@ -1204,42 +1258,144 @@
1204
1258
  hidden: draft.object.properties.hidden === true
1205
1259
  };
1206
1260
  }
1207
- function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1261
+ function createSceneLabels(objects, sceneWidth, sceneHeight, labelMultiplier) {
1208
1262
  const labels = [];
1209
1263
  const occupied = [];
1210
- const visibleObjects = [...objects].filter((object) => !object.hidden).sort((left, right) => left.sortKey - right.sortKey);
1264
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1265
+ const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort(compareLabelPlacementOrder);
1211
1266
  for (const object of visibleObjects) {
1212
- const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1213
- const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
1214
- let labelY = object.y + direction * (object.radius + 18 * labelMultiplier);
1215
- let secondaryY = labelY + direction * (16 * labelMultiplier);
1216
- let bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1217
- let attempts = 0;
1218
- while (occupied.some((entry) => rectsOverlap(entry, bounds)) && attempts < 10) {
1219
- labelY += direction * 14 * labelMultiplier;
1220
- secondaryY += direction * 14 * labelMultiplier;
1221
- bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1222
- attempts += 1;
1223
- }
1224
- occupied.push(bounds);
1267
+ const placement = selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) ?? createLabelPlacement(object, defaultVerticalDirection(object, objectMap.get(object.parentId ?? "") ?? null, sceneHeight), 0, labelMultiplier);
1268
+ occupied.push(createLabelRect(object, placement, labelMultiplier));
1225
1269
  labels.push({
1226
1270
  renderId: `${object.renderId}-label`,
1227
1271
  objectId: object.objectId,
1228
1272
  object: object.object,
1229
1273
  groupId: object.groupId,
1274
+ semanticGroupIds: [...object.semanticGroupIds],
1230
1275
  label: object.label,
1231
1276
  secondaryLabel: object.secondaryLabel,
1232
- x: object.x,
1233
- y: labelY,
1234
- secondaryY,
1235
- textAnchor: "middle",
1236
- direction: direction < 0 ? "above" : "below",
1277
+ x: placement.x,
1278
+ y: placement.labelY,
1279
+ secondaryY: placement.secondaryY,
1280
+ textAnchor: placement.textAnchor,
1281
+ direction: placement.direction,
1237
1282
  hidden: object.hidden
1238
1283
  });
1239
1284
  }
1240
1285
  return labels;
1241
1286
  }
1242
- function createSceneLayers(orbitVisuals, leaders, objects, labels) {
1287
+ function compareLabelPlacementOrder(left, right) {
1288
+ const priorityDiff = labelPlacementPriority(left) - labelPlacementPriority(right);
1289
+ if (priorityDiff !== 0) {
1290
+ return priorityDiff;
1291
+ }
1292
+ const renderPriorityDiff = (right.object.renderHints?.renderPriority ?? 0) - (left.object.renderHints?.renderPriority ?? 0);
1293
+ if (renderPriorityDiff !== 0) {
1294
+ return renderPriorityDiff;
1295
+ }
1296
+ return left.sortKey - right.sortKey;
1297
+ }
1298
+ function labelPlacementPriority(object) {
1299
+ switch (object.object.type) {
1300
+ case "star":
1301
+ return 0;
1302
+ case "planet":
1303
+ return 1;
1304
+ case "moon":
1305
+ return 2;
1306
+ case "belt":
1307
+ case "ring":
1308
+ return 3;
1309
+ case "asteroid":
1310
+ case "comet":
1311
+ return 4;
1312
+ case "structure":
1313
+ case "phenomenon":
1314
+ return 5;
1315
+ }
1316
+ }
1317
+ function selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) {
1318
+ for (const direction of preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight)) {
1319
+ const maxAttempts = direction === "left" || direction === "right" ? 4 : 6;
1320
+ for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
1321
+ const placement = createLabelPlacement(object, direction, attempt, labelMultiplier);
1322
+ const rect = createLabelRect(object, placement, labelMultiplier);
1323
+ if (!occupied.some((entry) => rectsOverlap(entry, rect))) {
1324
+ return placement;
1325
+ }
1326
+ }
1327
+ }
1328
+ return null;
1329
+ }
1330
+ function preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight) {
1331
+ const parent = object.parentId ? objectMap.get(object.parentId) ?? null : null;
1332
+ const vertical = defaultVerticalDirection(object, parent, sceneHeight);
1333
+ const oppositeVertical = vertical === "below" ? "above" : "below";
1334
+ const horizontal = defaultHorizontalDirection(object, parent, sceneWidth);
1335
+ const oppositeHorizontal = horizontal === "right" ? "left" : "right";
1336
+ 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";
1337
+ return preferHorizontal ? [horizontal, vertical, oppositeHorizontal, oppositeVertical] : [vertical, horizontal, oppositeVertical, oppositeHorizontal];
1338
+ }
1339
+ function defaultVerticalDirection(object, parent, sceneHeight) {
1340
+ if (parent && Math.abs(object.y - parent.y) > 6) {
1341
+ return object.y >= parent.y ? "below" : "above";
1342
+ }
1343
+ return object.y > sceneHeight * 0.62 ? "above" : "below";
1344
+ }
1345
+ function defaultHorizontalDirection(object, parent, sceneWidth) {
1346
+ if (parent && Math.abs(object.x - parent.x) > 6) {
1347
+ return object.x >= parent.x ? "right" : "left";
1348
+ }
1349
+ return object.x >= sceneWidth / 2 ? "right" : "left";
1350
+ }
1351
+ function createLabelPlacement(object, direction, attempt, labelMultiplier) {
1352
+ const step = 14 * labelMultiplier;
1353
+ switch (direction) {
1354
+ case "above": {
1355
+ const labelY = object.y - (object.radius + 18 * labelMultiplier + attempt * step);
1356
+ return {
1357
+ x: object.x,
1358
+ labelY,
1359
+ secondaryY: labelY - 16 * labelMultiplier,
1360
+ textAnchor: "middle",
1361
+ direction
1362
+ };
1363
+ }
1364
+ case "below": {
1365
+ const labelY = object.y + object.radius + 18 * labelMultiplier + attempt * step;
1366
+ return {
1367
+ x: object.x,
1368
+ labelY,
1369
+ secondaryY: labelY + 16 * labelMultiplier,
1370
+ textAnchor: "middle",
1371
+ direction
1372
+ };
1373
+ }
1374
+ case "left": {
1375
+ const x = object.x - (object.visualRadius + 16 * labelMultiplier + attempt * step);
1376
+ const labelY = object.y - 4 * labelMultiplier;
1377
+ return {
1378
+ x,
1379
+ labelY,
1380
+ secondaryY: labelY + 16 * labelMultiplier,
1381
+ textAnchor: "end",
1382
+ direction
1383
+ };
1384
+ }
1385
+ case "right": {
1386
+ const x = object.x + object.visualRadius + 16 * labelMultiplier + attempt * step;
1387
+ const labelY = object.y - 4 * labelMultiplier;
1388
+ return {
1389
+ x,
1390
+ labelY,
1391
+ secondaryY: labelY + 16 * labelMultiplier,
1392
+ textAnchor: "start",
1393
+ direction
1394
+ };
1395
+ }
1396
+ }
1397
+ }
1398
+ function createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels) {
1243
1399
  const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1244
1400
  const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1245
1401
  return [
@@ -1250,6 +1406,14 @@
1250
1406
  },
1251
1407
  { id: "orbits-back", renderIds: backOrbitIds },
1252
1408
  { id: "orbits-front", renderIds: frontOrbitIds },
1409
+ {
1410
+ id: "relations",
1411
+ renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId)
1412
+ },
1413
+ {
1414
+ id: "events",
1415
+ renderIds: events.filter((event) => !event.hidden).map((event) => event.renderId)
1416
+ },
1253
1417
  {
1254
1418
  id: "objects",
1255
1419
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
@@ -1261,7 +1425,7 @@
1261
1425
  { id: "metadata", renderIds: ["wo-title", "wo-subtitle", "wo-meta"] }
1262
1426
  ];
1263
1427
  }
1264
- function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships) {
1428
+ function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, labelMultiplier) {
1265
1429
  const groups = /* @__PURE__ */ new Map();
1266
1430
  const ensureGroup = (groupId) => {
1267
1431
  if (!groupId) {
@@ -1310,10 +1474,63 @@
1310
1474
  }
1311
1475
  }
1312
1476
  for (const group of groups.values()) {
1313
- group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels);
1477
+ group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier);
1314
1478
  }
1315
1479
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1316
1480
  }
1481
+ function createSceneSemanticGroups(document2, objects) {
1482
+ return [...document2.groups].map((group) => ({
1483
+ id: group.id,
1484
+ label: group.label,
1485
+ summary: group.summary,
1486
+ color: group.color,
1487
+ tags: [...group.tags],
1488
+ hidden: group.hidden,
1489
+ objectIds: objects.filter((object) => !object.hidden && object.semanticGroupIds.includes(group.id)).map((object) => object.objectId)
1490
+ })).sort((left, right) => left.label.localeCompare(right.label));
1491
+ }
1492
+ function createSceneRelations(document2, objects) {
1493
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1494
+ return document2.relations.map((relation) => {
1495
+ const from = objectMap.get(relation.from);
1496
+ const to = objectMap.get(relation.to);
1497
+ return {
1498
+ renderId: `${createRenderId(relation.id)}-relation`,
1499
+ relationId: relation.id,
1500
+ relation,
1501
+ fromObjectId: relation.from,
1502
+ toObjectId: relation.to,
1503
+ x1: from?.x ?? 0,
1504
+ y1: from?.y ?? 0,
1505
+ x2: to?.x ?? 0,
1506
+ y2: to?.y ?? 0,
1507
+ hidden: relation.hidden || !from || !to || from.hidden || to.hidden
1508
+ };
1509
+ }).sort((left, right) => left.relation.id.localeCompare(right.relation.id));
1510
+ }
1511
+ function createSceneEvents(events, objects, activeEventId) {
1512
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1513
+ return events.map((event) => {
1514
+ const objectIds = [.../* @__PURE__ */ new Set([
1515
+ ...event.targetObjectId ? [event.targetObjectId] : [],
1516
+ ...event.participantObjectIds
1517
+ ])];
1518
+ const positions = objectIds.map((objectId) => objectMap.get(objectId)).filter(Boolean);
1519
+ const centroidX = positions.length > 0 ? positions.reduce((sum, object) => sum + object.x, 0) / positions.length : 0;
1520
+ const centroidY = positions.length > 0 ? positions.reduce((sum, object) => sum + object.y, 0) / positions.length : 0;
1521
+ return {
1522
+ renderId: `${createRenderId(event.id)}-event`,
1523
+ eventId: event.id,
1524
+ event,
1525
+ objectIds,
1526
+ participantIds: [...event.participantObjectIds],
1527
+ targetObjectId: event.targetObjectId,
1528
+ x: centroidX,
1529
+ y: centroidY,
1530
+ hidden: event.hidden || positions.length === 0 || positions.every((object) => object.hidden) || activeEventId !== null && event.id !== activeEventId
1531
+ };
1532
+ }).sort((left, right) => left.event.id.localeCompare(right.event.id));
1533
+ }
1317
1534
  function createSceneViewpoints(document2, projection, preset, relationships, objectMap) {
1318
1535
  const generatedOverview = createGeneratedOverviewViewpoint(document2, projection, preset);
1319
1536
  const drafts = /* @__PURE__ */ new Map();
@@ -1331,7 +1548,7 @@
1331
1548
  }
1332
1549
  const field = fieldParts.join(".").toLowerCase();
1333
1550
  const draft = drafts.get(id) ?? { id };
1334
- applyViewpointField(draft, field, value, projection, preset, relationships, objectMap);
1551
+ applyViewpointField(draft, field, value, document2, projection, preset, relationships, objectMap);
1335
1552
  drafts.set(id, draft);
1336
1553
  }
1337
1554
  const viewpoints = [...drafts.values()].map((draft) => finalizeViewpointDraft(draft, projection, preset, objectMap)).filter(Boolean);
@@ -1359,13 +1576,15 @@
1359
1576
  });
1360
1577
  }
1361
1578
  function createGeneratedOverviewViewpoint(document2, projection, preset) {
1362
- const label = document2.system?.properties.title ? `${String(document2.system.properties.title)} Overview` : "Overview";
1579
+ const title = document2.system?.title ?? document2.system?.properties.title;
1580
+ const label = title ? `${String(title)} Overview` : "Overview";
1363
1581
  return {
1364
1582
  id: "overview",
1365
1583
  label,
1366
1584
  summary: "Fit the whole system with the current atlas defaults.",
1367
1585
  objectId: null,
1368
1586
  selectedObjectId: null,
1587
+ eventIds: [],
1369
1588
  projection,
1370
1589
  preset,
1371
1590
  rotationDeg: 0,
@@ -1375,7 +1594,7 @@
1375
1594
  generated: true
1376
1595
  };
1377
1596
  }
1378
- function applyViewpointField(draft, field, value, projection, preset, relationships, objectMap) {
1597
+ function applyViewpointField(draft, field, value, document2, projection, preset, relationships, objectMap) {
1379
1598
  const normalizedValue = value.trim();
1380
1599
  switch (field) {
1381
1600
  case "label":
@@ -1402,6 +1621,9 @@
1402
1621
  draft.select = normalizedValue;
1403
1622
  }
1404
1623
  return;
1624
+ case "events":
1625
+ draft.eventIds = splitListValue(normalizedValue);
1626
+ return;
1405
1627
  case "projection":
1406
1628
  case "view":
1407
1629
  draft.projection = parseViewProjection(normalizedValue) ?? projection;
@@ -1442,7 +1664,7 @@
1442
1664
  case "groups":
1443
1665
  draft.filter = {
1444
1666
  ...draft.filter ?? createEmptyViewpointFilter(),
1445
- groupIds: parseViewpointGroups(normalizedValue, relationships, objectMap)
1667
+ groupIds: parseViewpointGroups(normalizedValue, document2, relationships, objectMap)
1446
1668
  };
1447
1669
  return;
1448
1670
  }
@@ -1458,6 +1680,7 @@
1458
1680
  summary: draft.summary?.trim() || createViewpointSummary(label, objectId, filter),
1459
1681
  objectId,
1460
1682
  selectedObjectId,
1683
+ eventIds: [...new Set(draft.eventIds ?? [])],
1461
1684
  projection: draft.projection ?? projection,
1462
1685
  preset: draft.preset ?? preset,
1463
1686
  rotationDeg: draft.rotationDeg ?? 0,
@@ -1515,7 +1738,7 @@
1515
1738
  next["orbits-front"] = enabled;
1516
1739
  continue;
1517
1740
  }
1518
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1741
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "events" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1519
1742
  next[rawLayer] = enabled;
1520
1743
  }
1521
1744
  }
@@ -1524,8 +1747,11 @@
1524
1747
  function parseViewpointObjectTypes(value) {
1525
1748
  return splitListValue(value).filter((entry) => entry === "star" || entry === "planet" || entry === "moon" || entry === "belt" || entry === "asteroid" || entry === "comet" || entry === "ring" || entry === "structure" || entry === "phenomenon");
1526
1749
  }
1527
- function parseViewpointGroups(value, relationships, objectMap) {
1750
+ function parseViewpointGroups(value, document2, relationships, objectMap) {
1528
1751
  return splitListValue(value).map((entry) => {
1752
+ if (document2.schemaVersion === "2.1" || document2.groups.some((group) => group.id === entry)) {
1753
+ return entry;
1754
+ }
1529
1755
  if (entry.startsWith("wo-") && entry.endsWith("-group")) {
1530
1756
  return entry;
1531
1757
  }
@@ -1560,7 +1786,7 @@
1560
1786
  }
1561
1787
  return parts.join(" - ");
1562
1788
  }
1563
- function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels) {
1789
+ function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, labelMultiplier) {
1564
1790
  let minX = Number.POSITIVE_INFINITY;
1565
1791
  let minY = Number.POSITIVE_INFINITY;
1566
1792
  let maxX = Number.NEGATIVE_INFINITY;
@@ -1590,7 +1816,7 @@
1590
1816
  for (const label of labels) {
1591
1817
  if (label.hidden)
1592
1818
  continue;
1593
- includeLabelBounds(label, include);
1819
+ includeLabelBounds(label, include, labelMultiplier);
1594
1820
  }
1595
1821
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
1596
1822
  return createBounds(0, 0, width, height);
@@ -1628,13 +1854,10 @@
1628
1854
  include(object.x - object.visualRadius - 24, object.y - object.visualRadius - 16);
1629
1855
  include(object.x + object.visualRadius + 24, object.y + object.visualRadius + 36);
1630
1856
  }
1631
- function includeLabelBounds(label, include) {
1632
- const labelScale = 1;
1633
- const labelHalfWidth = estimateLabelHalfWidthFromText(label.label, label.secondaryLabel, labelScale);
1634
- include(label.x - labelHalfWidth, label.y - 18);
1635
- include(label.x + labelHalfWidth, label.y + 8);
1636
- include(label.x - labelHalfWidth, label.secondaryY - 14);
1637
- include(label.x + labelHalfWidth, label.secondaryY + 8);
1857
+ function includeLabelBounds(label, include, labelMultiplier) {
1858
+ const bounds = createLabelRectFromText(label.x, label.y, label.secondaryY, label.textAnchor, label.direction, label.label, label.secondaryLabel, labelMultiplier);
1859
+ include(bounds.left, bounds.top);
1860
+ include(bounds.right, bounds.bottom);
1638
1861
  }
1639
1862
  function placeObject(object, x, y, depth, positions, orbitDrafts, leaderDrafts, context) {
1640
1863
  if (positions.has(object.id)) {
@@ -1656,8 +1879,9 @@
1656
1879
  }
1657
1880
  const orbiting = [...context.orbitChildren.get(object.id) ?? []].sort(compareOrbiting);
1658
1881
  const orbitMetricContext = computeOrbitMetricContext(orbiting, parent.radius, context.spacingFactor, context.scaleModel);
1882
+ const orbitRadiiPx = resolveOrbitRadiiPx(orbiting, orbitMetricContext);
1659
1883
  orbiting.forEach((child, index) => {
1660
- const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, context);
1884
+ const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, orbitRadiiPx[index] ?? orbitMetricContext.innerPx, context);
1661
1885
  orbitDrafts.push({
1662
1886
  object: child,
1663
1887
  parentId: object.id,
@@ -1731,7 +1955,8 @@
1731
1955
  metricSpread: 0,
1732
1956
  innerPx,
1733
1957
  stepPx,
1734
- pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx)
1958
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
1959
+ minimumGapPx: stepPx * 0.42
1735
1960
  };
1736
1961
  }
1737
1962
  const minMetric = Math.min(...presentMetrics);
@@ -1744,10 +1969,11 @@
1744
1969
  metricSpread,
1745
1970
  innerPx,
1746
1971
  stepPx,
1747
- pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx)
1972
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
1973
+ minimumGapPx: stepPx * 0.42
1748
1974
  };
1749
1975
  }
1750
- function resolveOrbitGeometry(object, index, count, parent, metricContext, context) {
1976
+ function resolveOrbitGeometry(object, index, count, parent, metricContext, orbitRadiusPx, context) {
1751
1977
  const placement = object.placement;
1752
1978
  const band = object.type === "belt" || object.type === "ring";
1753
1979
  if (!placement || placement.mode !== "orbit") {
@@ -1765,7 +1991,7 @@
1765
1991
  };
1766
1992
  }
1767
1993
  const eccentricity = clampNumber(typeof placement.eccentricity === "number" ? placement.eccentricity : 0, 0, 0.92);
1768
- const semiMajor = resolveOrbitRadiusPx(object, index, metricContext);
1994
+ const semiMajor = orbitRadiusPx;
1769
1995
  const baseMinor = Math.max(semiMajor * Math.sqrt(1 - eccentricity * eccentricity), semiMajor * 0.18);
1770
1996
  const inclinationDeg = unitValueToDegrees(placement.inclination) ?? 0;
1771
1997
  const inclinationScale = context.projection === "isometric" ? Math.max(MIN_ISO_MINOR_SCALE, Math.cos(degreesToRadians(inclinationDeg))) * ISO_FLATTENING : 1;
@@ -1795,15 +2021,19 @@
1795
2021
  objectY: objectPoint.y
1796
2022
  };
1797
2023
  }
1798
- function resolveOrbitRadiusPx(object, index, metricContext) {
1799
- const metric = orbitMetric(object);
1800
- if (metric === null) {
1801
- return metricContext.innerPx + index * metricContext.stepPx;
1802
- }
1803
- if (metricContext.metricSpread > 0) {
1804
- return metricContext.innerPx + (metric - metricContext.minMetric) / metricContext.metricSpread * metricContext.pixelSpread;
1805
- }
1806
- return metricContext.innerPx + Math.log10(metric + 1) * metricContext.stepPx;
2024
+ function resolveOrbitRadiusPx(metric, metricContext) {
2025
+ return metricContext.innerPx + metricContext.stepPx * log2(Math.max(metric, 0) + 1);
2026
+ }
2027
+ function resolveOrbitRadiiPx(objects, metricContext) {
2028
+ const radii = [];
2029
+ objects.forEach((object, index) => {
2030
+ const metric = orbitMetric(object);
2031
+ const fallbackRadius = metricContext.innerPx + index * metricContext.stepPx;
2032
+ const baseRadius = metric === null ? fallbackRadius : resolveOrbitRadiusPx(metric, metricContext);
2033
+ const minimumRadius = index === 0 ? metricContext.innerPx : (radii[index - 1] ?? metricContext.innerPx) + metricContext.minimumGapPx;
2034
+ radii.push(Math.max(baseRadius, minimumRadius));
2035
+ });
2036
+ return radii;
1807
2037
  }
1808
2038
  function orbitMetric(object) {
1809
2039
  if (!object.placement || object.placement.mode !== "orbit") {
@@ -1811,6 +2041,9 @@
1811
2041
  }
1812
2042
  return toDistanceMetric(object.placement.semiMajor ?? object.placement.distance ?? null);
1813
2043
  }
2044
+ function log2(value) {
2045
+ return Math.log(value) / Math.log(2);
2046
+ }
1814
2047
  function resolveOrbitPhase(phase, index, count) {
1815
2048
  const degreeValue = phase ? unitValueToDegrees(phase) : null;
1816
2049
  if (degreeValue !== null) {
@@ -2014,7 +2247,7 @@
2014
2247
  return null;
2015
2248
  }
2016
2249
  }
2017
- function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels) {
2250
+ function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier) {
2018
2251
  let minX = Number.POSITIVE_INFINITY;
2019
2252
  let minY = Number.POSITIVE_INFINITY;
2020
2253
  let maxX = Number.NEGATIVE_INFINITY;
@@ -2043,7 +2276,7 @@
2043
2276
  }
2044
2277
  for (const label of labels) {
2045
2278
  if (!label.hidden && group.labelIds.includes(label.objectId)) {
2046
- includeLabelBounds(label, include);
2279
+ includeLabelBounds(label, include, labelMultiplier);
2047
2280
  }
2048
2281
  }
2049
2282
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
@@ -2068,12 +2301,28 @@
2068
2301
  }
2069
2302
  return current.id;
2070
2303
  }
2071
- function createLabelRect(x, labelY, secondaryY, labelHalfWidth, direction) {
2304
+ function createLabelRect(object, placement, labelMultiplier) {
2305
+ return createLabelRectFromText(placement.x, placement.labelY, placement.secondaryY, placement.textAnchor, placement.direction, object.label, object.secondaryLabel, labelMultiplier);
2306
+ }
2307
+ function createLabelRectFromText(x, labelY, secondaryY, textAnchor, direction, label, secondaryLabel, labelMultiplier) {
2308
+ const labelHalfWidth = estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier);
2309
+ const labelWidth = labelHalfWidth * 2;
2310
+ const topPadding = direction === "above" ? 18 : 12;
2311
+ const bottomPadding = direction === "above" ? 8 : 12;
2312
+ let left = x - labelHalfWidth;
2313
+ let right = x + labelHalfWidth;
2314
+ if (textAnchor === "start") {
2315
+ left = x;
2316
+ right = x + labelWidth;
2317
+ } else if (textAnchor === "end") {
2318
+ left = x - labelWidth;
2319
+ right = x;
2320
+ }
2072
2321
  return {
2073
- left: x - labelHalfWidth,
2074
- right: x + labelHalfWidth,
2075
- top: Math.min(labelY, secondaryY) - (direction < 0 ? 18 : 12),
2076
- bottom: Math.max(labelY, secondaryY) + (direction < 0 ? 8 : 12)
2322
+ left,
2323
+ right,
2324
+ top: Math.min(labelY, secondaryY) - topPadding,
2325
+ bottom: Math.max(labelY, secondaryY) + bottomPadding
2077
2326
  };
2078
2327
  }
2079
2328
  function rectsOverlap(left, right) {
@@ -2134,8 +2383,18 @@
2134
2383
  return value.value;
2135
2384
  case "km":
2136
2385
  return value.value / AU_IN_KM;
2386
+ case "m":
2387
+ return value.value / 1e3 / AU_IN_KM;
2388
+ case "ly":
2389
+ return value.value * LY_IN_AU;
2390
+ case "pc":
2391
+ return value.value * PC_IN_AU;
2392
+ case "kpc":
2393
+ return value.value * KPC_IN_AU;
2137
2394
  case "re":
2138
2395
  return value.value * EARTH_RADIUS_IN_KM / AU_IN_KM;
2396
+ case "rj":
2397
+ return value.value * JUPITER_RADIUS_IN_KM / AU_IN_KM;
2139
2398
  case "sol":
2140
2399
  return value.value * SOLAR_RADIUS_IN_KM / AU_IN_KM;
2141
2400
  default:
@@ -2250,11 +2509,6 @@
2250
2509
  function customColorFor(value) {
2251
2510
  return typeof value === "string" && value.trim() ? value : void 0;
2252
2511
  }
2253
- function estimateLabelHalfWidth(object, labelMultiplier) {
2254
- const primaryWidth = object.label.length * 4.6 * labelMultiplier + 18;
2255
- const secondaryWidth = object.secondaryLabel.length * 3.9 * labelMultiplier + 18;
2256
- return Math.max(primaryWidth, secondaryWidth, object.visualRadius + 18);
2257
- }
2258
2512
  function estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier) {
2259
2513
  const primaryWidth = label.length * 4.6 * labelMultiplier + 18;
2260
2514
  const secondaryWidth = secondaryLabel.length * 3.9 * labelMultiplier + 18;
@@ -2271,28 +2525,95 @@
2271
2525
  }
2272
2526
 
2273
2527
  // packages/core/dist/draft.js
2274
- function materializeAtlasDocument(document2) {
2528
+ function materializeAtlasDocument(document2, options = {}) {
2275
2529
  const system = document2.system ? {
2276
2530
  type: "system",
2277
2531
  id: document2.system.id,
2532
+ title: document2.system.title,
2533
+ description: document2.system.description,
2534
+ epoch: document2.system.epoch,
2535
+ referencePlane: document2.system.referencePlane,
2278
2536
  properties: materializeDraftSystemProperties(document2.system),
2279
2537
  info: materializeDraftSystemInfo(document2.system)
2280
2538
  } : null;
2539
+ const objects = document2.objects.map(cloneWorldOrbitObject);
2540
+ applyEventPoseOverrides(objects, document2.events ?? [], options.activeEventId ?? null);
2281
2541
  return {
2282
2542
  format: "worldorbit",
2283
2543
  version: "1.0",
2544
+ schemaVersion: document2.version,
2284
2545
  system,
2285
- objects: document2.objects.map(cloneWorldOrbitObject)
2546
+ groups: structuredClone(document2.groups ?? []),
2547
+ relations: structuredClone(document2.relations ?? []),
2548
+ events: document2.events.map(cloneWorldOrbitEvent),
2549
+ objects
2286
2550
  };
2287
2551
  }
2288
2552
  function cloneWorldOrbitObject(object) {
2289
2553
  return {
2290
2554
  ...object,
2555
+ groups: object.groups ? [...object.groups] : void 0,
2556
+ resonance: object.resonance ? { ...object.resonance } : object.resonance,
2557
+ renderHints: object.renderHints ? { ...object.renderHints } : object.renderHints,
2558
+ deriveRules: object.deriveRules ? object.deriveRules.map((rule) => ({ ...rule })) : void 0,
2559
+ validationRules: object.validationRules ? object.validationRules.map((rule) => ({ ...rule })) : void 0,
2560
+ lockedFields: object.lockedFields ? [...object.lockedFields] : void 0,
2561
+ tolerances: object.tolerances ? object.tolerances.map((entry) => ({
2562
+ field: entry.field,
2563
+ 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
2564
+ })) : void 0,
2565
+ typedBlocks: object.typedBlocks ? Object.fromEntries(Object.entries(object.typedBlocks).map(([key, block]) => [key, { ...block ?? {} }])) : void 0,
2291
2566
  properties: cloneProperties(object.properties),
2292
2567
  placement: object.placement ? structuredClone(object.placement) : null,
2293
2568
  info: { ...object.info }
2294
2569
  };
2295
2570
  }
2571
+ function cloneWorldOrbitEvent(event) {
2572
+ return {
2573
+ ...event,
2574
+ participantObjectIds: [...event.participantObjectIds],
2575
+ tags: [...event.tags],
2576
+ positions: event.positions.map(cloneWorldOrbitEventPose)
2577
+ };
2578
+ }
2579
+ function cloneWorldOrbitEventPose(pose) {
2580
+ return {
2581
+ objectId: pose.objectId,
2582
+ placement: clonePlacement(pose.placement),
2583
+ inner: pose.inner ? { ...pose.inner } : void 0,
2584
+ outer: pose.outer ? { ...pose.outer } : void 0
2585
+ };
2586
+ }
2587
+ function clonePlacement(placement) {
2588
+ return placement ? structuredClone(placement) : null;
2589
+ }
2590
+ function applyEventPoseOverrides(objects, events, activeEventId) {
2591
+ if (!activeEventId) {
2592
+ return;
2593
+ }
2594
+ const event = events.find((entry) => entry.id === activeEventId);
2595
+ if (!event) {
2596
+ return;
2597
+ }
2598
+ const objectMap = new Map(objects.map((object) => [object.id, object]));
2599
+ for (const pose of event.positions) {
2600
+ const object = objectMap.get(pose.objectId);
2601
+ if (!object) {
2602
+ continue;
2603
+ }
2604
+ object.placement = clonePlacement(pose.placement);
2605
+ if (pose.inner) {
2606
+ object.properties.inner = { ...pose.inner };
2607
+ } else {
2608
+ delete object.properties.inner;
2609
+ }
2610
+ if (pose.outer) {
2611
+ object.properties.outer = { ...pose.outer };
2612
+ } else {
2613
+ delete object.properties.outer;
2614
+ }
2615
+ }
2616
+ }
2296
2617
  function cloneProperties(properties) {
2297
2618
  const next = {};
2298
2619
  for (const [key, value] of Object.entries(properties)) {
@@ -2323,71 +2644,83 @@
2323
2644
  if (system.defaults.units) {
2324
2645
  properties.units = system.defaults.units;
2325
2646
  }
2647
+ if (system.description) {
2648
+ properties.description = system.description;
2649
+ }
2650
+ if (system.epoch) {
2651
+ properties.epoch = system.epoch;
2652
+ }
2653
+ if (system.referencePlane) {
2654
+ properties.referencePlane = system.referencePlane;
2655
+ }
2326
2656
  return properties;
2327
2657
  }
2328
2658
  function materializeDraftSystemInfo(system) {
2329
- const info = {
2659
+ const info2 = {
2330
2660
  ...system.atlasMetadata
2331
2661
  };
2332
2662
  if (system.defaults.theme) {
2333
- info["atlas.theme"] = system.defaults.theme;
2663
+ info2["atlas.theme"] = system.defaults.theme;
2334
2664
  }
2335
2665
  for (const viewpoint of system.viewpoints) {
2336
2666
  const prefix = `viewpoint.${viewpoint.id}`;
2337
- info[`${prefix}.label`] = viewpoint.label;
2667
+ info2[`${prefix}.label`] = viewpoint.label;
2338
2668
  if (viewpoint.summary) {
2339
- info[`${prefix}.summary`] = viewpoint.summary;
2669
+ info2[`${prefix}.summary`] = viewpoint.summary;
2340
2670
  }
2341
2671
  if (viewpoint.focusObjectId) {
2342
- info[`${prefix}.focus`] = viewpoint.focusObjectId;
2672
+ info2[`${prefix}.focus`] = viewpoint.focusObjectId;
2343
2673
  }
2344
2674
  if (viewpoint.selectedObjectId) {
2345
- info[`${prefix}.select`] = viewpoint.selectedObjectId;
2675
+ info2[`${prefix}.select`] = viewpoint.selectedObjectId;
2346
2676
  }
2347
2677
  if (viewpoint.projection) {
2348
- info[`${prefix}.projection`] = viewpoint.projection;
2678
+ info2[`${prefix}.projection`] = viewpoint.projection;
2349
2679
  }
2350
2680
  if (viewpoint.preset) {
2351
- info[`${prefix}.preset`] = viewpoint.preset;
2681
+ info2[`${prefix}.preset`] = viewpoint.preset;
2352
2682
  }
2353
2683
  if (viewpoint.zoom !== null) {
2354
- info[`${prefix}.zoom`] = String(viewpoint.zoom);
2684
+ info2[`${prefix}.zoom`] = String(viewpoint.zoom);
2355
2685
  }
2356
2686
  if (viewpoint.rotationDeg !== 0) {
2357
- info[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2687
+ info2[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2358
2688
  }
2359
2689
  const serializedLayers = serializeViewpointLayers(viewpoint.layers);
2360
2690
  if (serializedLayers) {
2361
- info[`${prefix}.layers`] = serializedLayers;
2691
+ info2[`${prefix}.layers`] = serializedLayers;
2362
2692
  }
2363
2693
  if (viewpoint.filter?.query) {
2364
- info[`${prefix}.query`] = viewpoint.filter.query;
2694
+ info2[`${prefix}.query`] = viewpoint.filter.query;
2365
2695
  }
2366
2696
  if ((viewpoint.filter?.objectTypes.length ?? 0) > 0) {
2367
- info[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
2697
+ info2[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
2368
2698
  }
2369
2699
  if ((viewpoint.filter?.tags.length ?? 0) > 0) {
2370
- info[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
2700
+ info2[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
2371
2701
  }
2372
2702
  if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2373
- info[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2703
+ info2[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2704
+ }
2705
+ if (viewpoint.events.length > 0) {
2706
+ info2[`${prefix}.events`] = viewpoint.events.join(" ");
2374
2707
  }
2375
2708
  }
2376
2709
  for (const annotation of system.annotations) {
2377
2710
  const prefix = `annotation.${annotation.id}`;
2378
- info[`${prefix}.label`] = annotation.label;
2711
+ info2[`${prefix}.label`] = annotation.label;
2379
2712
  if (annotation.targetObjectId) {
2380
- info[`${prefix}.target`] = annotation.targetObjectId;
2713
+ info2[`${prefix}.target`] = annotation.targetObjectId;
2381
2714
  }
2382
- info[`${prefix}.body`] = annotation.body;
2715
+ info2[`${prefix}.body`] = annotation.body;
2383
2716
  if (annotation.tags.length > 0) {
2384
- info[`${prefix}.tags`] = annotation.tags.join(" ");
2717
+ info2[`${prefix}.tags`] = annotation.tags.join(" ");
2385
2718
  }
2386
2719
  if (annotation.sourceObjectId) {
2387
- info[`${prefix}.source`] = annotation.sourceObjectId;
2720
+ info2[`${prefix}.source`] = annotation.sourceObjectId;
2388
2721
  }
2389
2722
  }
2390
- return info;
2723
+ return info2;
2391
2724
  }
2392
2725
  function serializeViewpointLayers(layers) {
2393
2726
  const tokens = [];
@@ -2396,7 +2729,7 @@
2396
2729
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2397
2730
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2398
2731
  }
2399
- for (const key of ["background", "guides", "objects", "labels", "metadata"]) {
2732
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
2400
2733
  if (layers[key] !== void 0) {
2401
2734
  tokens.push(layers[key] ? key : `-${key}`);
2402
2735
  }
@@ -2404,171 +2737,838 @@
2404
2737
  return tokens.join(" ");
2405
2738
  }
2406
2739
 
2407
- // packages/core/dist/draft-parse.js
2408
- function parseWorldOrbitAtlas(source) {
2409
- return parseAtlasSource(source, "2.0");
2740
+ // packages/core/dist/atlas-utils.js
2741
+ 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)?$/;
2742
+ var BOOLEAN_VALUES2 = /* @__PURE__ */ new Map([
2743
+ ["true", true],
2744
+ ["false", false],
2745
+ ["yes", true],
2746
+ ["no", false]
2747
+ ]);
2748
+ var URL_SCHEME_PATTERN2 = /^[A-Za-z][A-Za-z0-9+.-]*:/;
2749
+ function normalizeIdentifier(value) {
2750
+ return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
2410
2751
  }
2411
- function parseAtlasSource(source, outputVersion) {
2412
- const lines = source.split(/\r?\n/);
2413
- let sawSchemaHeader = false;
2414
- let schemaVersion = "2.0";
2415
- let system = null;
2416
- let section = null;
2417
- const objectNodes = [];
2418
- let sawDefaults = false;
2419
- let sawAtlas = false;
2420
- const viewpointIds = /* @__PURE__ */ new Set();
2421
- const annotationIds = /* @__PURE__ */ new Set();
2422
- for (let index = 0; index < lines.length; index++) {
2423
- const rawLine = lines[index];
2424
- const lineNumber = index + 1;
2425
- if (!rawLine.trim()) {
2426
- continue;
2427
- }
2428
- const indent = getIndent(rawLine);
2429
- const tokens = tokenizeLineDetailed(rawLine.slice(indent), {
2430
- line: lineNumber,
2431
- columnOffset: indent
2432
- });
2433
- if (tokens.length === 0) {
2434
- continue;
2435
- }
2436
- if (!sawSchemaHeader) {
2437
- schemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
2438
- sawSchemaHeader = true;
2439
- continue;
2440
- }
2441
- if (indent === 0) {
2442
- section = startTopLevelSection(tokens, lineNumber, system, objectNodes, viewpointIds, annotationIds, {
2443
- sawDefaults,
2444
- sawAtlas
2445
- });
2446
- if (section.kind === "system") {
2447
- system = section.system;
2448
- } else if (section.kind === "defaults") {
2449
- sawDefaults = true;
2450
- } else if (section.kind === "atlas") {
2451
- sawAtlas = true;
2452
- }
2453
- continue;
2454
- }
2455
- if (!section) {
2456
- throw new WorldOrbitError("Indented line without parent atlas section", lineNumber, indent + 1);
2457
- }
2458
- handleSectionLine(section, indent, tokens, lineNumber);
2459
- }
2460
- if (!sawSchemaHeader) {
2461
- throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
2752
+ function humanizeIdentifier2(value) {
2753
+ return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
2754
+ }
2755
+ function parseAtlasUnitValue(input, location, fieldKey) {
2756
+ const match = input.match(UNIT_PATTERN2);
2757
+ if (!match) {
2758
+ throw WorldOrbitError.fromLocation(`Invalid unit value "${input}"`, location);
2462
2759
  }
2463
- const ast = {
2464
- type: "document",
2465
- objects: objectNodes
2760
+ const unitValue = {
2761
+ value: Number(match[1]),
2762
+ unit: match[2] ?? null
2466
2763
  };
2467
- const normalizedObjects = normalizeDocument(ast).objects;
2468
- validateDocument({
2469
- format: "worldorbit",
2470
- version: "1.0",
2471
- system: null,
2472
- objects: normalizedObjects
2473
- });
2474
- const diagnostics = schemaVersion === "2.0-draft" && outputVersion === "2.0" ? [
2475
- {
2476
- code: "load.schema.deprecatedDraft",
2477
- severity: "warning",
2478
- source: "upgrade",
2479
- message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".'
2764
+ if (fieldKey) {
2765
+ const schema = getFieldSchema(fieldKey);
2766
+ if (schema?.unitFamily && !unitFamilyAllowsUnit(schema.unitFamily, unitValue.unit)) {
2767
+ throw WorldOrbitError.fromLocation(`Unit "${unitValue.unit ?? "none"}" is not valid for "${fieldKey}"`, location);
2480
2768
  }
2481
- ] : [];
2769
+ }
2770
+ return unitValue;
2771
+ }
2772
+ function tryParseAtlasUnitValue(input) {
2773
+ const match = input.match(UNIT_PATTERN2);
2774
+ if (!match) {
2775
+ return null;
2776
+ }
2482
2777
  return {
2483
- format: "worldorbit",
2484
- version: outputVersion,
2485
- sourceVersion: "1.0",
2486
- system,
2487
- objects: normalizedObjects,
2488
- diagnostics
2778
+ value: Number(match[1]),
2779
+ unit: match[2] ?? null
2489
2780
  };
2490
2781
  }
2491
- function assertDraftSchemaHeader(tokens, line) {
2492
- if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || tokens[1].value.toLowerCase() !== "2.0-draft" && tokens[1].value.toLowerCase() !== "2.0") {
2493
- throw new WorldOrbitError('Expected atlas header "schema 2.0" or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
2782
+ function parseAtlasNumber(input, key, location) {
2783
+ const value = Number(input);
2784
+ if (!Number.isFinite(value)) {
2785
+ throw WorldOrbitError.fromLocation(`Invalid numeric value "${input}" for "${key}"`, location);
2494
2786
  }
2495
- return tokens[1].value.toLowerCase() === "2.0-draft" ? "2.0-draft" : "2.0";
2787
+ return value;
2496
2788
  }
2497
- function startTopLevelSection(tokens, line, system, objectNodes, viewpointIds, annotationIds, flags) {
2498
- const keyword = tokens[0]?.value.toLowerCase();
2499
- switch (keyword) {
2500
- case "system":
2501
- if (system) {
2502
- throw new WorldOrbitError('Atlas section "system" may only appear once', line, tokens[0].column);
2503
- }
2504
- return startSystemSection(tokens, line);
2505
- case "defaults":
2506
- if (!system) {
2507
- throw new WorldOrbitError('Atlas section "defaults" requires a preceding system declaration', line, tokens[0].column);
2508
- }
2509
- if (flags.sawDefaults) {
2510
- throw new WorldOrbitError('Atlas section "defaults" may only appear once', line, tokens[0].column);
2511
- }
2512
- return {
2513
- kind: "defaults",
2514
- system,
2515
- seenFields: /* @__PURE__ */ new Set()
2516
- };
2517
- case "atlas":
2518
- if (!system) {
2519
- throw new WorldOrbitError('Atlas section "atlas" requires a preceding system declaration', line, tokens[0].column);
2520
- }
2521
- if (flags.sawAtlas) {
2522
- throw new WorldOrbitError('Atlas section "atlas" may only appear once', line, tokens[0].column);
2523
- }
2524
- return {
2525
- kind: "atlas",
2526
- system,
2527
- inMetadata: false,
2528
- metadataIndent: null
2529
- };
2530
- case "viewpoint":
2531
- if (!system) {
2532
- throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
2533
- }
2534
- return startViewpointSection(tokens, line, system, viewpointIds);
2535
- case "annotation":
2536
- if (!system) {
2537
- throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
2538
- }
2539
- return startAnnotationSection(tokens, line, system, annotationIds);
2540
- case "object":
2541
- return startObjectSection(tokens, line, objectNodes);
2542
- default:
2543
- throw new WorldOrbitError(`Unknown atlas section "${tokens[0]?.value ?? ""}"`, line, tokens[0]?.column ?? 1);
2789
+ function parseAtlasBoolean(input, key, location) {
2790
+ const parsed = BOOLEAN_VALUES2.get(input.toLowerCase());
2791
+ if (parsed === void 0) {
2792
+ throw WorldOrbitError.fromLocation(`Invalid boolean value "${input}" for "${key}"`, location);
2544
2793
  }
2794
+ return parsed;
2545
2795
  }
2546
- function startSystemSection(tokens, line) {
2547
- if (tokens.length !== 2) {
2548
- throw new WorldOrbitError("Invalid atlas system declaration", line, tokens[0]?.column ?? 1);
2796
+ function parseAtlasAtReference(target, location) {
2797
+ if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
2798
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
2549
2799
  }
2550
- const system = {
2551
- type: "system",
2552
- id: tokens[1].value,
2553
- title: null,
2554
- defaults: {
2555
- view: "topdown",
2556
- scale: null,
2557
- units: null,
2558
- preset: null,
2559
- theme: null
2560
- },
2561
- atlasMetadata: {},
2562
- viewpoints: [],
2563
- annotations: []
2564
- };
2565
- return {
2566
- kind: "system",
2567
- system,
2800
+ const pairedMatch = target.match(/^([A-Za-z0-9._-]+)-([A-Za-z0-9._-]+):(L[1-5])$/);
2801
+ if (pairedMatch) {
2802
+ return {
2803
+ kind: "lagrange",
2804
+ primary: pairedMatch[1],
2805
+ secondary: pairedMatch[2],
2806
+ point: pairedMatch[3]
2807
+ };
2808
+ }
2809
+ const simpleMatch = target.match(/^([A-Za-z0-9._-]+):(L[1-5])$/);
2810
+ if (simpleMatch) {
2811
+ return {
2812
+ kind: "lagrange",
2813
+ primary: simpleMatch[1],
2814
+ secondary: null,
2815
+ point: simpleMatch[2]
2816
+ };
2817
+ }
2818
+ if (/^[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
2819
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
2820
+ }
2821
+ const anchorMatch = target.match(/^([A-Za-z0-9._-]+):([A-Za-z0-9._-]+)$/);
2822
+ if (anchorMatch) {
2823
+ return {
2824
+ kind: "anchor",
2825
+ objectId: anchorMatch[1],
2826
+ anchor: anchorMatch[2]
2827
+ };
2828
+ }
2829
+ return {
2830
+ kind: "named",
2831
+ name: target
2832
+ };
2833
+ }
2834
+ function validateAtlasImageSource(value, location) {
2835
+ if (!value) {
2836
+ throw WorldOrbitError.fromLocation('Field "image" must not be empty', location);
2837
+ }
2838
+ if (value.startsWith("//")) {
2839
+ throw WorldOrbitError.fromLocation('Field "image" must use a relative path, root-relative path, or an http/https URL', location);
2840
+ }
2841
+ const schemeMatch = value.match(URL_SCHEME_PATTERN2);
2842
+ if (!schemeMatch) {
2843
+ return;
2844
+ }
2845
+ const scheme = schemeMatch[0].slice(0, -1).toLowerCase();
2846
+ if (scheme !== "http" && scheme !== "https") {
2847
+ throw WorldOrbitError.fromLocation(`Field "image" does not support the "${scheme}" scheme`, location);
2848
+ }
2849
+ }
2850
+ function normalizeLegacyScalarValue(key, values, location) {
2851
+ const schema = getFieldSchema(key);
2852
+ if (!schema) {
2853
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
2854
+ }
2855
+ if (schema.arity === "single" && values.length !== 1) {
2856
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
2857
+ }
2858
+ switch (schema.kind) {
2859
+ case "list":
2860
+ return values;
2861
+ case "boolean":
2862
+ return parseAtlasBoolean(singleAtlasValue(values, key, location), key, location);
2863
+ case "number":
2864
+ return parseAtlasNumber(singleAtlasValue(values, key, location), key, location);
2865
+ case "unit":
2866
+ return parseAtlasUnitValue(singleAtlasValue(values, key, location), location, key);
2867
+ case "string": {
2868
+ const value = values.join(" ").trim();
2869
+ if (key === "image") {
2870
+ validateAtlasImageSource(value, location);
2871
+ }
2872
+ return value;
2873
+ }
2874
+ }
2875
+ }
2876
+ function ensureAtlasFieldSupported(key, objectType, location) {
2877
+ const schema = getFieldSchema(key);
2878
+ if (!schema) {
2879
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
2880
+ }
2881
+ if (!schema.objectTypes.includes(objectType)) {
2882
+ throw WorldOrbitError.fromLocation(`Field "${key}" is not valid on "${objectType}"`, location);
2883
+ }
2884
+ }
2885
+ function singleAtlasValue(values, key, location) {
2886
+ if (values.length !== 1) {
2887
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
2888
+ }
2889
+ return values[0];
2890
+ }
2891
+
2892
+ // packages/core/dist/atlas-validate.js
2893
+ var SURFACE_TARGET_TYPES2 = /* @__PURE__ */ new Set(["star", "planet", "moon", "asteroid", "comet"]);
2894
+ var EARTH_MASSES_PER_SOLAR = 332946.0487;
2895
+ var JUPITER_MASSES_PER_SOLAR = 1047.3486;
2896
+ var AU_IN_KM2 = 1495978707e-1;
2897
+ var EARTH_RADIUS_IN_KM2 = 6371;
2898
+ var SOLAR_RADIUS_IN_KM2 = 695700;
2899
+ var LY_IN_AU2 = 63241.077;
2900
+ var PC_IN_AU2 = 206264.806;
2901
+ var KPC_IN_AU2 = 206264806;
2902
+ function collectAtlasDiagnostics(document2, sourceSchemaVersion) {
2903
+ const diagnostics = [];
2904
+ const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
2905
+ const groupIds = new Set(document2.groups.map((group) => group.id));
2906
+ const eventIds = new Set(document2.events.map((event) => event.id));
2907
+ if (!document2.system) {
2908
+ diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
2909
+ }
2910
+ const knownIds = /* @__PURE__ */ new Map();
2911
+ for (const [kind, ids] of [
2912
+ ["group", document2.groups.map((group) => group.id)],
2913
+ ["viewpoint", document2.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
2914
+ ["annotation", document2.system?.annotations.map((annotation) => annotation.id) ?? []],
2915
+ ["relation", document2.relations.map((relation) => relation.id)],
2916
+ ["event", document2.events.map((event) => event.id)],
2917
+ ["object", document2.objects.map((object) => object.id)]
2918
+ ]) {
2919
+ for (const id of ids) {
2920
+ const previous = knownIds.get(id);
2921
+ if (previous) {
2922
+ diagnostics.push(error("validate.id.duplicate", `Duplicate ${kind} id "${id}" already used by ${previous}.`));
2923
+ } else {
2924
+ knownIds.set(id, kind);
2925
+ }
2926
+ }
2927
+ }
2928
+ for (const relation of document2.relations) {
2929
+ validateRelation(relation, objectMap, diagnostics);
2930
+ }
2931
+ for (const viewpoint of document2.system?.viewpoints ?? []) {
2932
+ validateViewpoint(viewpoint.filter, viewpoint.events ?? [], groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpoint.id);
2933
+ }
2934
+ for (const object of document2.objects) {
2935
+ validateObject(object, document2.system, objectMap, groupIds, diagnostics);
2936
+ }
2937
+ for (const event of document2.events) {
2938
+ validateEvent(event, objectMap, diagnostics);
2939
+ }
2940
+ return diagnostics;
2941
+ }
2942
+ function validateRelation(relation, objectMap, diagnostics) {
2943
+ if (!relation.from) {
2944
+ diagnostics.push(error("validate.relation.from.required", `Relation "${relation.id}" is missing a "from" target.`));
2945
+ } else if (!objectMap.has(relation.from)) {
2946
+ diagnostics.push(error("validate.relation.from.unknown", `Unknown relation source "${relation.from}" on "${relation.id}".`));
2947
+ }
2948
+ if (!relation.to) {
2949
+ diagnostics.push(error("validate.relation.to.required", `Relation "${relation.id}" is missing a "to" target.`));
2950
+ } else if (!objectMap.has(relation.to)) {
2951
+ diagnostics.push(error("validate.relation.to.unknown", `Unknown relation target "${relation.to}" on "${relation.id}".`));
2952
+ }
2953
+ if (!relation.kind) {
2954
+ diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
2955
+ }
2956
+ }
2957
+ function validateViewpoint(filter, eventRefs, groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpointId) {
2958
+ if (sourceSchemaVersion === "2.1") {
2959
+ if (filter) {
2960
+ for (const groupId of filter.groupIds) {
2961
+ if (!groupIds.has(groupId)) {
2962
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.groups`));
2963
+ }
2964
+ }
2965
+ }
2966
+ for (const eventId of eventRefs) {
2967
+ if (!eventIds.has(eventId)) {
2968
+ diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.events`));
2969
+ }
2970
+ }
2971
+ }
2972
+ }
2973
+ function validateObject(object, system, objectMap, groupIds, diagnostics) {
2974
+ const placement = object.placement;
2975
+ const orbitPlacement = placement?.mode === "orbit" ? placement : null;
2976
+ const parentObject = placement?.mode === "orbit" ? objectMap.get(placement.target) ?? null : null;
2977
+ if (object.groups) {
2978
+ for (const groupId of object.groups) {
2979
+ if (!groupIds.has(groupId)) {
2980
+ diagnostics.push(warn("validate.group.unknown", `Unknown group "${groupId}" on "${object.id}".`, object.id, "groups"));
2981
+ }
2982
+ }
2983
+ }
2984
+ if (orbitPlacement) {
2985
+ if (!objectMap.has(orbitPlacement.target)) {
2986
+ diagnostics.push(error("validate.orbit.target.unknown", `Unknown placement target "${orbitPlacement.target}" on "${object.id}".`, object.id, "orbit"));
2987
+ }
2988
+ if (orbitPlacement.distance && orbitPlacement.semiMajor) {
2989
+ diagnostics.push(error("validate.orbit.distanceConflict", `Object "${object.id}" cannot declare both "distance" and "semiMajor".`, object.id, "distance"));
2990
+ }
2991
+ if (orbitPlacement.phase && !object.epoch && !system?.epoch) {
2992
+ diagnostics.push(warn("validate.phase.epochMissing", `Object "${object.id}" sets "phase" without an object or system epoch.`, object.id, "phase"));
2993
+ }
2994
+ if (orbitPlacement.inclination && !object.referencePlane && !system?.referencePlane) {
2995
+ diagnostics.push(warn("validate.inclination.referencePlaneMissing", `Object "${object.id}" sets "inclination" without an object or system reference plane.`, object.id, "inclination"));
2996
+ }
2997
+ if (orbitPlacement.period && !massInSolar(parentObject?.properties.mass)) {
2998
+ diagnostics.push(warn("validate.period.massMissing", `Object "${object.id}" sets "period" but its central mass cannot be derived.`, object.id, "period"));
2999
+ }
3000
+ }
3001
+ if (placement?.mode === "surface") {
3002
+ const target = objectMap.get(placement.target);
3003
+ if (!target) {
3004
+ diagnostics.push(error("validate.surface.target.unknown", `Unknown placement target "${placement.target}" on "${object.id}".`, object.id, "surface"));
3005
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
3006
+ diagnostics.push(error("validate.surface.target.invalid", `Surface target "${placement.target}" on "${object.id}" is not surface-capable.`, object.id, "surface"));
3007
+ }
3008
+ }
3009
+ if (placement?.mode === "at") {
3010
+ if (object.type !== "structure" && object.type !== "phenomenon") {
3011
+ diagnostics.push(error("validate.at.objectType", `Only structures and phenomena may use "at" placement; found "${object.type}" on "${object.id}".`, object.id, "at"));
3012
+ }
3013
+ if (!validateAtTarget(object, objectMap, diagnostics)) {
3014
+ diagnostics.push(error("validate.at.target.unknown", `Unknown at-reference target "${placement.target}" on "${object.id}".`, object.id, "at"));
3015
+ }
3016
+ }
3017
+ if (object.resonance) {
3018
+ const target = objectMap.get(object.resonance.targetObjectId);
3019
+ if (!target) {
3020
+ diagnostics.push(error("validate.resonance.target.unknown", `Unknown resonance target "${object.resonance.targetObjectId}" on "${object.id}".`, object.id, "resonance"));
3021
+ } else if (object.placement?.mode !== "orbit" || target.placement?.mode !== "orbit" || object.placement.target !== target.placement.target) {
3022
+ diagnostics.push(warn("validate.resonance.orbitMismatch", `Resonance target "${object.resonance.targetObjectId}" on "${object.id}" does not share a compatible orbital parent.`, object.id, "resonance"));
3023
+ }
3024
+ }
3025
+ for (const rule of object.deriveRules ?? []) {
3026
+ if (rule.field !== "period" || rule.strategy !== "kepler") {
3027
+ diagnostics.push(warn("validate.derive.unsupported", `Unsupported derive rule "${rule.field} ${rule.strategy}" on "${object.id}".`, object.id, "derive"));
3028
+ continue;
3029
+ }
3030
+ const derivedPeriodDays = keplerPeriodDays(object, parentObject);
3031
+ if (derivedPeriodDays === null) {
3032
+ diagnostics.push(warn("validate.derive.inputsMissing", `Object "${object.id}" requests "derive period kepler" but lacks enough input data.`, object.id, "derive"));
3033
+ continue;
3034
+ }
3035
+ if (!orbitPlacement?.period) {
3036
+ diagnostics.push(info("validate.derive.period.available", `Object "${object.id}" can derive a Kepler period of ${formatDays(derivedPeriodDays)}.`, object.id, "derive"));
3037
+ }
3038
+ }
3039
+ for (const rule of object.validationRules ?? []) {
3040
+ if (rule.rule !== "kepler") {
3041
+ diagnostics.push(warn("validate.rule.unsupported", `Unsupported validation rule "${rule.rule}" on "${object.id}".`, object.id, "validate"));
3042
+ continue;
3043
+ }
3044
+ const actualPeriodDays = durationInDays(orbitPlacement?.period);
3045
+ const derivedPeriodDays = keplerPeriodDays(object, parentObject);
3046
+ if (actualPeriodDays === null || derivedPeriodDays === null) {
3047
+ continue;
3048
+ }
3049
+ const toleranceDays = toleranceForField(object, "period");
3050
+ if (Math.abs(actualPeriodDays - derivedPeriodDays) > toleranceDays) {
3051
+ diagnostics.push(error("validate.kepler.mismatch", `Object "${object.id}" fails Kepler validation for "period".`, object.id, "validate"));
3052
+ }
3053
+ }
3054
+ }
3055
+ function validateEvent(event, objectMap, diagnostics) {
3056
+ const fieldPrefix = `event.${event.id}`;
3057
+ const referencedIds = /* @__PURE__ */ new Set();
3058
+ if (!event.kind.trim()) {
3059
+ diagnostics.push(error("validate.event.kind.required", `Event "${event.id}" is missing a "kind" value.`, void 0, `${fieldPrefix}.kind`));
3060
+ }
3061
+ if (!event.targetObjectId && event.participantObjectIds.length === 0) {
3062
+ diagnostics.push(error("validate.event.references.required", `Event "${event.id}" must define a "target" or at least one participant.`, void 0, `${fieldPrefix}.participants`));
3063
+ }
3064
+ if (event.targetObjectId) {
3065
+ referencedIds.add(event.targetObjectId);
3066
+ if (!objectMap.has(event.targetObjectId)) {
3067
+ diagnostics.push(error("validate.event.target.unknown", `Unknown event target "${event.targetObjectId}" on "${event.id}".`, void 0, `${fieldPrefix}.target`));
3068
+ }
3069
+ }
3070
+ const seenParticipants = /* @__PURE__ */ new Set();
3071
+ for (const participantId of event.participantObjectIds) {
3072
+ referencedIds.add(participantId);
3073
+ if (seenParticipants.has(participantId)) {
3074
+ diagnostics.push(warn("validate.event.participants.duplicate", `Event "${event.id}" repeats participant "${participantId}".`, void 0, `${fieldPrefix}.participants`));
3075
+ continue;
3076
+ }
3077
+ seenParticipants.add(participantId);
3078
+ if (!objectMap.has(participantId)) {
3079
+ diagnostics.push(error("validate.event.participants.unknown", `Unknown event participant "${participantId}" on "${event.id}".`, void 0, `${fieldPrefix}.participants`));
3080
+ }
3081
+ }
3082
+ if (event.targetObjectId && event.participantObjectIds.length > 0 && !event.participantObjectIds.includes(event.targetObjectId)) {
3083
+ diagnostics.push(warn("validate.event.target.notParticipant", `Event "${event.id}" defines a target outside its participants list.`, void 0, `${fieldPrefix}.target`));
3084
+ }
3085
+ if (event.positions.length === 0) {
3086
+ diagnostics.push(warn("validate.event.positions.missing", `Event "${event.id}" has no positions block and cannot drive a scene snapshot.`, void 0, `${fieldPrefix}.positions`));
3087
+ }
3088
+ if (/(?:^|[-_])(solar-eclipse|lunar-eclipse|transit|occultation)(?:$|[-_])/.test(event.kind) && referencedIds.size < 3) {
3089
+ 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`));
3090
+ }
3091
+ const poseIds = /* @__PURE__ */ new Set();
3092
+ for (const pose of event.positions) {
3093
+ const poseFieldPrefix = `${fieldPrefix}.pose.${pose.objectId}`;
3094
+ if (poseIds.has(pose.objectId)) {
3095
+ diagnostics.push(error("validate.event.pose.duplicate", `Event "${event.id}" defines "${pose.objectId}" more than once in positions.`, void 0, poseFieldPrefix));
3096
+ continue;
3097
+ }
3098
+ poseIds.add(pose.objectId);
3099
+ const object = objectMap.get(pose.objectId);
3100
+ if (!object) {
3101
+ diagnostics.push(error("validate.event.pose.object.unknown", `Unknown event pose object "${pose.objectId}" on "${event.id}".`, void 0, poseFieldPrefix));
3102
+ continue;
3103
+ }
3104
+ if (!referencedIds.has(pose.objectId)) {
3105
+ diagnostics.push(warn("validate.event.pose.unreferenced", `Event pose "${pose.objectId}" on "${event.id}" is not listed in target/participants.`, void 0, poseFieldPrefix));
3106
+ }
3107
+ validateEventPose(pose, object, objectMap, diagnostics, poseFieldPrefix, event.id);
3108
+ }
3109
+ }
3110
+ function validateEventPose(pose, object, objectMap, diagnostics, fieldPrefix, eventId) {
3111
+ const placement = pose.placement;
3112
+ if (!placement) {
3113
+ diagnostics.push(error("validate.event.pose.placement.required", `Event "${eventId}" pose "${pose.objectId}" is missing a placement mode.`, void 0, fieldPrefix));
3114
+ return;
3115
+ }
3116
+ if (placement.mode === "orbit") {
3117
+ if (!objectMap.has(placement.target)) {
3118
+ diagnostics.push(error("validate.event.pose.orbit.target.unknown", `Unknown event orbit target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.orbit`));
3119
+ }
3120
+ if (placement.distance && placement.semiMajor) {
3121
+ diagnostics.push(error("validate.event.pose.orbit.distanceConflict", `Event "${eventId}" pose "${pose.objectId}" cannot declare both "distance" and "semiMajor".`, void 0, `${fieldPrefix}.distance`));
3122
+ }
3123
+ return;
3124
+ }
3125
+ if (placement.mode === "surface") {
3126
+ const target = objectMap.get(placement.target);
3127
+ if (!target) {
3128
+ diagnostics.push(error("validate.event.pose.surface.target.unknown", `Unknown event surface target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.surface`));
3129
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
3130
+ 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`));
3131
+ }
3132
+ return;
3133
+ }
3134
+ if (placement.mode === "at") {
3135
+ if (object.type !== "structure" && object.type !== "phenomenon") {
3136
+ 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`));
3137
+ }
3138
+ const reference = placement.reference;
3139
+ if (reference.kind === "named" && !objectMap.has(reference.name)) {
3140
+ diagnostics.push(error("validate.event.pose.at.target.unknown", `Unknown event at-reference target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3141
+ } else if (reference.kind === "anchor" && !objectMap.has(reference.objectId)) {
3142
+ diagnostics.push(error("validate.event.pose.anchor.target.unknown", `Unknown event anchor target "${reference.objectId}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3143
+ } else if (reference.kind === "lagrange") {
3144
+ if (!objectMap.has(reference.primary)) {
3145
+ diagnostics.push(error("validate.event.pose.lagrange.primary.unknown", `Unknown event Lagrange target "${reference.primary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3146
+ } else if (reference.secondary && !objectMap.has(reference.secondary)) {
3147
+ diagnostics.push(error("validate.event.pose.lagrange.secondary.unknown", `Unknown event Lagrange target "${reference.secondary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3148
+ }
3149
+ }
3150
+ }
3151
+ }
3152
+ function validateAtTarget(object, objectMap, diagnostics) {
3153
+ const reference = object.placement?.mode === "at" ? object.placement.reference : null;
3154
+ if (!reference) {
3155
+ return true;
3156
+ }
3157
+ if (reference.kind === "named") {
3158
+ return objectMap.has(reference.name);
3159
+ }
3160
+ if (reference.kind === "anchor") {
3161
+ if (!objectMap.has(reference.objectId)) {
3162
+ diagnostics.push(error("validate.anchor.target.unknown", `Unknown anchor target "${reference.objectId}" on "${object.id}".`, object.id, "at"));
3163
+ return false;
3164
+ }
3165
+ return true;
3166
+ }
3167
+ if (!objectMap.has(reference.primary)) {
3168
+ diagnostics.push(error("validate.lagrange.primary.unknown", `Unknown Lagrange reference "${reference.primary}" on "${object.id}".`, object.id, "at"));
3169
+ return false;
3170
+ }
3171
+ if (reference.secondary && !objectMap.has(reference.secondary)) {
3172
+ diagnostics.push(error("validate.lagrange.secondary.unknown", `Unknown Lagrange reference "${reference.secondary}" on "${object.id}".`, object.id, "at"));
3173
+ return false;
3174
+ }
3175
+ return true;
3176
+ }
3177
+ function keplerPeriodDays(object, parentObject) {
3178
+ const placement = object.placement;
3179
+ if (!placement || placement.mode !== "orbit") {
3180
+ return null;
3181
+ }
3182
+ const semiMajorAu = distanceInAu(placement.semiMajor ?? placement.distance);
3183
+ const centralMassSolar = massInSolar(parentObject?.properties.mass);
3184
+ if (semiMajorAu === null || centralMassSolar === null || centralMassSolar <= 0) {
3185
+ return null;
3186
+ }
3187
+ const periodYears = Math.sqrt(semiMajorAu ** 3 / centralMassSolar);
3188
+ return periodYears * 365.25;
3189
+ }
3190
+ function distanceInAu(value) {
3191
+ if (!value)
3192
+ return null;
3193
+ switch (value.unit) {
3194
+ case null:
3195
+ case "au":
3196
+ return value.value;
3197
+ case "km":
3198
+ return value.value / AU_IN_KM2;
3199
+ case "m":
3200
+ return value.value / (AU_IN_KM2 * 1e3);
3201
+ case "ly":
3202
+ return value.value * LY_IN_AU2;
3203
+ case "pc":
3204
+ return value.value * PC_IN_AU2;
3205
+ case "kpc":
3206
+ return value.value * KPC_IN_AU2;
3207
+ case "re":
3208
+ return value.value * EARTH_RADIUS_IN_KM2 / AU_IN_KM2;
3209
+ case "sol":
3210
+ return value.value * SOLAR_RADIUS_IN_KM2 / AU_IN_KM2;
3211
+ default:
3212
+ return null;
3213
+ }
3214
+ }
3215
+ function massInSolar(value) {
3216
+ if (!value || typeof value !== "object" || !("value" in value)) {
3217
+ return null;
3218
+ }
3219
+ const unitValue = value;
3220
+ switch (unitValue.unit) {
3221
+ case null:
3222
+ case "sol":
3223
+ return unitValue.value;
3224
+ case "me":
3225
+ return unitValue.value / EARTH_MASSES_PER_SOLAR;
3226
+ case "mj":
3227
+ return unitValue.value / JUPITER_MASSES_PER_SOLAR;
3228
+ default:
3229
+ return null;
3230
+ }
3231
+ }
3232
+ function durationInDays(value) {
3233
+ if (!value)
3234
+ return null;
3235
+ switch (value.unit) {
3236
+ case null:
3237
+ case "d":
3238
+ return value.value;
3239
+ case "s":
3240
+ return value.value / 86400;
3241
+ case "min":
3242
+ return value.value / 1440;
3243
+ case "h":
3244
+ return value.value / 24;
3245
+ case "y":
3246
+ return value.value * 365.25;
3247
+ case "ky":
3248
+ return value.value * 365250;
3249
+ case "my":
3250
+ return value.value * 36525e4;
3251
+ case "gy":
3252
+ return value.value * 36525e7;
3253
+ default:
3254
+ return null;
3255
+ }
3256
+ }
3257
+ function toleranceForField(object, field) {
3258
+ const tolerance = object.tolerances?.find((entry) => entry.field === field)?.value;
3259
+ if (typeof tolerance === "number") {
3260
+ return tolerance;
3261
+ }
3262
+ if (tolerance && typeof tolerance === "object" && "value" in tolerance) {
3263
+ return durationInDays(tolerance) ?? 0;
3264
+ }
3265
+ return 0;
3266
+ }
3267
+ function formatDays(days) {
3268
+ return `${Math.round(days * 100) / 100}d`;
3269
+ }
3270
+ function error(code, message, objectId, field) {
3271
+ return { code, severity: "error", source: "validate", message, objectId, field };
3272
+ }
3273
+ function warn(code, message, objectId, field) {
3274
+ return { code, severity: "warning", source: "validate", message, objectId, field };
3275
+ }
3276
+ function info(code, message, objectId, field) {
3277
+ return { code, severity: "info", source: "validate", message, objectId, field };
3278
+ }
3279
+
3280
+ // packages/core/dist/draft-parse.js
3281
+ var STRUCTURED_TYPED_BLOCKS = /* @__PURE__ */ new Set([
3282
+ "climate",
3283
+ "habitability",
3284
+ "settlement"
3285
+ ]);
3286
+ var DRAFT_OBJECT_FIELD_SPECS = /* @__PURE__ */ new Map();
3287
+ for (const key of [
3288
+ "orbit",
3289
+ "distance",
3290
+ "semiMajor",
3291
+ "eccentricity",
3292
+ "period",
3293
+ "angle",
3294
+ "inclination",
3295
+ "phase",
3296
+ "at",
3297
+ "surface",
3298
+ "free",
3299
+ "kind",
3300
+ "class",
3301
+ "culture",
3302
+ "tags",
3303
+ "color",
3304
+ "image",
3305
+ "hidden",
3306
+ "radius",
3307
+ "mass",
3308
+ "density",
3309
+ "gravity",
3310
+ "temperature",
3311
+ "albedo",
3312
+ "atmosphere",
3313
+ "inner",
3314
+ "outer",
3315
+ "on",
3316
+ "source",
3317
+ "cycle"
3318
+ ]) {
3319
+ const schema = getFieldSchema(key);
3320
+ if (schema) {
3321
+ DRAFT_OBJECT_FIELD_SPECS.set(key, {
3322
+ key,
3323
+ version: "2.0",
3324
+ inlineMode: schema.arity === "multiple" ? "multiple" : "single",
3325
+ allowRepeat: false,
3326
+ legacySchema: schema
3327
+ });
3328
+ }
3329
+ }
3330
+ for (const spec of [
3331
+ { key: "groups", inlineMode: "multiple", allowRepeat: false },
3332
+ { key: "epoch", inlineMode: "single", allowRepeat: false },
3333
+ { key: "referencePlane", inlineMode: "single", allowRepeat: false },
3334
+ { key: "tidalLock", inlineMode: "single", allowRepeat: false },
3335
+ { key: "renderLabel", inlineMode: "single", allowRepeat: false },
3336
+ { key: "renderOrbit", inlineMode: "single", allowRepeat: false },
3337
+ { key: "renderPriority", inlineMode: "single", allowRepeat: false },
3338
+ { key: "resonance", inlineMode: "pair", allowRepeat: false },
3339
+ { key: "derive", inlineMode: "pair", allowRepeat: true },
3340
+ { key: "validate", inlineMode: "single", allowRepeat: true },
3341
+ { key: "locked", inlineMode: "multiple", allowRepeat: false },
3342
+ { key: "tolerance", inlineMode: "pair", allowRepeat: true }
3343
+ ]) {
3344
+ DRAFT_OBJECT_FIELD_SPECS.set(spec.key, {
3345
+ key: spec.key,
3346
+ version: "2.1",
3347
+ inlineMode: spec.inlineMode,
3348
+ allowRepeat: spec.allowRepeat
3349
+ });
3350
+ }
3351
+ var DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
3352
+ var EVENT_POSE_FIELD_KEYS = /* @__PURE__ */ new Set([
3353
+ "orbit",
3354
+ "distance",
3355
+ "semiMajor",
3356
+ "eccentricity",
3357
+ "period",
3358
+ "angle",
3359
+ "inclination",
3360
+ "phase",
3361
+ "at",
3362
+ "surface",
3363
+ "free",
3364
+ "inner",
3365
+ "outer"
3366
+ ]);
3367
+ function parseWorldOrbitAtlas(source) {
3368
+ return parseAtlasSource(source);
3369
+ }
3370
+ function parseAtlasSource(source, forcedOutputVersion) {
3371
+ const prepared = preprocessAtlasSource(source);
3372
+ const lines = prepared.source.split(/\r?\n/);
3373
+ const diagnostics = [];
3374
+ let sawSchemaHeader = false;
3375
+ let sourceSchemaVersion = "2.0";
3376
+ let system = null;
3377
+ let section = null;
3378
+ const objectNodes = [];
3379
+ const groups = [];
3380
+ const relations = [];
3381
+ const events = [];
3382
+ const eventPoseNodes = /* @__PURE__ */ new Map();
3383
+ let sawDefaults = false;
3384
+ let sawAtlas = false;
3385
+ const viewpointIds = /* @__PURE__ */ new Set();
3386
+ const annotationIds = /* @__PURE__ */ new Set();
3387
+ const groupIds = /* @__PURE__ */ new Set();
3388
+ const relationIds = /* @__PURE__ */ new Set();
3389
+ const eventIds = /* @__PURE__ */ new Set();
3390
+ for (let index = 0; index < lines.length; index++) {
3391
+ const rawLine = lines[index];
3392
+ const lineNumber = index + 1;
3393
+ if (!rawLine.trim()) {
3394
+ continue;
3395
+ }
3396
+ const indent = getIndent(rawLine);
3397
+ const tokens = tokenizeLineDetailed(rawLine.slice(indent), {
3398
+ line: lineNumber,
3399
+ columnOffset: indent
3400
+ });
3401
+ if (tokens.length === 0) {
3402
+ continue;
3403
+ }
3404
+ if (!sawSchemaHeader) {
3405
+ sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
3406
+ sawSchemaHeader = true;
3407
+ if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
3408
+ diagnostics.push({
3409
+ code: "parse.schema21.commentCompatibility",
3410
+ severity: "warning",
3411
+ source: "parse",
3412
+ message: `Comments require schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
3413
+ line: prepared.comments[0].line,
3414
+ column: prepared.comments[0].column
3415
+ });
3416
+ }
3417
+ continue;
3418
+ }
3419
+ if (indent === 0) {
3420
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, { sawDefaults, sawAtlas });
3421
+ if (section.kind === "system") {
3422
+ system = section.system;
3423
+ } else if (section.kind === "defaults") {
3424
+ sawDefaults = true;
3425
+ } else if (section.kind === "atlas") {
3426
+ sawAtlas = true;
3427
+ }
3428
+ continue;
3429
+ }
3430
+ if (!section) {
3431
+ throw new WorldOrbitError("Indented line without parent atlas section", lineNumber, indent + 1);
3432
+ }
3433
+ handleSectionLine(section, indent, tokens, lineNumber);
3434
+ }
3435
+ if (!sawSchemaHeader) {
3436
+ throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
3437
+ }
3438
+ const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
3439
+ const normalizedEvents = events.map((event) => normalizeDraftEvent(event, eventPoseNodes.get(event.id) ?? []));
3440
+ const outputVersion = forcedOutputVersion ?? (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
3441
+ const baseDocument = {
3442
+ format: "worldorbit",
3443
+ sourceVersion: "1.0",
3444
+ system,
3445
+ groups,
3446
+ relations,
3447
+ events: normalizedEvents,
3448
+ objects,
3449
+ diagnostics
3450
+ };
3451
+ if (outputVersion === "2.0-draft") {
3452
+ const document3 = {
3453
+ ...baseDocument,
3454
+ version: "2.0-draft",
3455
+ schemaVersion: "2.0-draft"
3456
+ };
3457
+ document3.diagnostics.push(...collectAtlasDiagnostics(document3, sourceSchemaVersion));
3458
+ return document3;
3459
+ }
3460
+ const document2 = {
3461
+ ...baseDocument,
3462
+ version: outputVersion,
3463
+ schemaVersion: outputVersion
3464
+ };
3465
+ if (sourceSchemaVersion === "2.0-draft") {
3466
+ document2.diagnostics.push({
3467
+ code: "load.schema.deprecatedDraft",
3468
+ severity: "warning",
3469
+ source: "upgrade",
3470
+ message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".'
3471
+ });
3472
+ }
3473
+ document2.diagnostics.push(...collectAtlasDiagnostics(document2, sourceSchemaVersion));
3474
+ return document2;
3475
+ }
3476
+ function assertDraftSchemaHeader(tokens, line) {
3477
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
3478
+ throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
3479
+ }
3480
+ const version = tokens[1].value.toLowerCase();
3481
+ return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
3482
+ }
3483
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, flags) {
3484
+ const keyword = tokens[0]?.value.toLowerCase();
3485
+ switch (keyword) {
3486
+ case "system":
3487
+ if (system) {
3488
+ throw new WorldOrbitError('Atlas section "system" may only appear once', line, tokens[0].column);
3489
+ }
3490
+ return startSystemSection(tokens, line, sourceSchemaVersion, diagnostics);
3491
+ case "defaults":
3492
+ if (!system) {
3493
+ throw new WorldOrbitError('Atlas section "defaults" requires a preceding system declaration', line, tokens[0].column);
3494
+ }
3495
+ if (flags.sawDefaults) {
3496
+ throw new WorldOrbitError('Atlas section "defaults" may only appear once', line, tokens[0].column);
3497
+ }
3498
+ return {
3499
+ kind: "defaults",
3500
+ system,
3501
+ seenFields: /* @__PURE__ */ new Set()
3502
+ };
3503
+ case "atlas":
3504
+ if (!system) {
3505
+ throw new WorldOrbitError('Atlas section "atlas" requires a preceding system declaration', line, tokens[0].column);
3506
+ }
3507
+ if (flags.sawAtlas) {
3508
+ throw new WorldOrbitError('Atlas section "atlas" may only appear once', line, tokens[0].column);
3509
+ }
3510
+ return {
3511
+ kind: "atlas",
3512
+ system,
3513
+ inMetadata: false,
3514
+ metadataIndent: null
3515
+ };
3516
+ case "viewpoint":
3517
+ if (!system) {
3518
+ throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
3519
+ }
3520
+ return startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics);
3521
+ case "annotation":
3522
+ if (!system) {
3523
+ throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
3524
+ }
3525
+ return startAnnotationSection(tokens, line, system, annotationIds);
3526
+ case "group":
3527
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "group", { line, column: tokens[0].column });
3528
+ return startGroupSection(tokens, line, groups, groupIds);
3529
+ case "relation":
3530
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
3531
+ return startRelationSection(tokens, line, relations, relationIds);
3532
+ case "event":
3533
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "event", { line, column: tokens[0].column });
3534
+ return startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics);
3535
+ case "object":
3536
+ return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
3537
+ default:
3538
+ throw new WorldOrbitError(`Unknown atlas section "${tokens[0]?.value ?? ""}"`, line, tokens[0]?.column ?? 1);
3539
+ }
3540
+ }
3541
+ function startSystemSection(tokens, line, sourceSchemaVersion, diagnostics) {
3542
+ if (tokens.length !== 2) {
3543
+ throw new WorldOrbitError("Invalid atlas system declaration", line, tokens[0]?.column ?? 1);
3544
+ }
3545
+ const system = {
3546
+ type: "system",
3547
+ id: tokens[1].value,
3548
+ title: null,
3549
+ description: null,
3550
+ epoch: null,
3551
+ referencePlane: null,
3552
+ defaults: {
3553
+ view: "topdown",
3554
+ scale: null,
3555
+ units: null,
3556
+ preset: null,
3557
+ theme: null
3558
+ },
3559
+ atlasMetadata: {},
3560
+ viewpoints: [],
3561
+ annotations: []
3562
+ };
3563
+ return {
3564
+ kind: "system",
3565
+ system,
3566
+ sourceSchemaVersion,
3567
+ diagnostics,
2568
3568
  seenFields: /* @__PURE__ */ new Set()
2569
3569
  };
2570
3570
  }
2571
- function startViewpointSection(tokens, line, system, viewpointIds) {
3571
+ function startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics) {
2572
3572
  if (tokens.length !== 2) {
2573
3573
  throw new WorldOrbitError("Invalid viewpoint declaration", line, tokens[0]?.column ?? 1);
2574
3574
  }
@@ -2585,6 +3585,7 @@
2585
3585
  summary: "",
2586
3586
  focusObjectId: null,
2587
3587
  selectedObjectId: null,
3588
+ events: [],
2588
3589
  projection: system.defaults.view,
2589
3590
  preset: system.defaults.preset,
2590
3591
  zoom: null,
@@ -2597,6 +3598,8 @@
2597
3598
  return {
2598
3599
  kind: "viewpoint",
2599
3600
  viewpoint,
3601
+ sourceSchemaVersion,
3602
+ diagnostics,
2600
3603
  seenFields: /* @__PURE__ */ new Set(),
2601
3604
  inFilter: false,
2602
3605
  filterIndent: null,
@@ -2630,7 +3633,107 @@
2630
3633
  seenFields: /* @__PURE__ */ new Set()
2631
3634
  };
2632
3635
  }
2633
- function startObjectSection(tokens, line, objectNodes) {
3636
+ function startGroupSection(tokens, line, groups, groupIds) {
3637
+ if (tokens.length !== 2) {
3638
+ throw new WorldOrbitError("Invalid group declaration", line, tokens[0]?.column ?? 1);
3639
+ }
3640
+ const id = normalizeIdentifier(tokens[1].value);
3641
+ if (!id) {
3642
+ throw new WorldOrbitError("Group id must not be empty", line, tokens[1].column);
3643
+ }
3644
+ if (groupIds.has(id)) {
3645
+ throw new WorldOrbitError(`Duplicate group id "${id}"`, line, tokens[1].column);
3646
+ }
3647
+ const group = {
3648
+ id,
3649
+ label: humanizeIdentifier2(id),
3650
+ summary: "",
3651
+ color: null,
3652
+ tags: [],
3653
+ hidden: false
3654
+ };
3655
+ groups.push(group);
3656
+ groupIds.add(id);
3657
+ return {
3658
+ kind: "group",
3659
+ group,
3660
+ seenFields: /* @__PURE__ */ new Set()
3661
+ };
3662
+ }
3663
+ function startRelationSection(tokens, line, relations, relationIds) {
3664
+ if (tokens.length !== 2) {
3665
+ throw new WorldOrbitError("Invalid relation declaration", line, tokens[0]?.column ?? 1);
3666
+ }
3667
+ const id = normalizeIdentifier(tokens[1].value);
3668
+ if (!id) {
3669
+ throw new WorldOrbitError("Relation id must not be empty", line, tokens[1].column);
3670
+ }
3671
+ if (relationIds.has(id)) {
3672
+ throw new WorldOrbitError(`Duplicate relation id "${id}"`, line, tokens[1].column);
3673
+ }
3674
+ const relation = {
3675
+ id,
3676
+ from: "",
3677
+ to: "",
3678
+ kind: "",
3679
+ label: null,
3680
+ summary: null,
3681
+ tags: [],
3682
+ color: null,
3683
+ hidden: false
3684
+ };
3685
+ relations.push(relation);
3686
+ relationIds.add(id);
3687
+ return {
3688
+ kind: "relation",
3689
+ relation,
3690
+ seenFields: /* @__PURE__ */ new Set()
3691
+ };
3692
+ }
3693
+ function startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics) {
3694
+ if (tokens.length !== 2) {
3695
+ throw new WorldOrbitError("Invalid event declaration", line, tokens[0]?.column ?? 1);
3696
+ }
3697
+ const id = normalizeIdentifier(tokens[1].value);
3698
+ if (!id) {
3699
+ throw new WorldOrbitError("Event id must not be empty", line, tokens[1].column);
3700
+ }
3701
+ if (eventIds.has(id)) {
3702
+ throw new WorldOrbitError(`Duplicate event id "${id}"`, line, tokens[1].column);
3703
+ }
3704
+ const event = {
3705
+ id,
3706
+ kind: "",
3707
+ label: humanizeIdentifier2(id),
3708
+ summary: null,
3709
+ targetObjectId: null,
3710
+ participantObjectIds: [],
3711
+ timing: null,
3712
+ visibility: null,
3713
+ tags: [],
3714
+ color: null,
3715
+ hidden: false,
3716
+ positions: []
3717
+ };
3718
+ const rawPoses = [];
3719
+ events.push(event);
3720
+ eventPoseNodes.set(id, rawPoses);
3721
+ eventIds.add(id);
3722
+ return {
3723
+ kind: "event",
3724
+ event,
3725
+ sourceSchemaVersion,
3726
+ diagnostics,
3727
+ seenFields: /* @__PURE__ */ new Set(),
3728
+ rawPoses,
3729
+ inPositions: false,
3730
+ positionsIndent: null,
3731
+ activePose: null,
3732
+ poseIndent: null,
3733
+ activePoseSeenFields: /* @__PURE__ */ new Set()
3734
+ };
3735
+ }
3736
+ function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
2634
3737
  if (tokens.length < 3) {
2635
3738
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
2636
3739
  }
@@ -2641,12 +3744,11 @@
2641
3744
  throw new WorldOrbitError(`Unknown object type "${objectTypeToken.value}"`, line, objectTypeToken.column);
2642
3745
  }
2643
3746
  const objectNode = {
2644
- type: "object",
2645
3747
  objectType,
2646
- name: idToken.value,
2647
- inlineFields: parseInlineFields2(tokens.slice(3), line),
2648
- blockFields: [],
3748
+ id: idToken.value,
3749
+ fields: parseInlineObjectFields(tokens.slice(3), line, objectType, sourceSchemaVersion, diagnostics),
2649
3750
  infoEntries: [],
3751
+ typedBlockEntries: {},
2650
3752
  location: {
2651
3753
  line,
2652
3754
  column: objectTypeToken.column
@@ -2656,8 +3758,12 @@
2656
3758
  return {
2657
3759
  kind: "object",
2658
3760
  objectNode,
2659
- inInfoBlock: false,
2660
- infoIndent: null
3761
+ sourceSchemaVersion,
3762
+ diagnostics,
3763
+ activeBlock: null,
3764
+ blockIndent: null,
3765
+ seenInfoKeys: /* @__PURE__ */ new Set(),
3766
+ seenTypedBlockKeys: {}
2661
3767
  };
2662
3768
  }
2663
3769
  function handleSectionLine(section, indent, tokens, line) {
@@ -2677,6 +3783,15 @@
2677
3783
  case "annotation":
2678
3784
  applyAnnotationField(section, tokens, line);
2679
3785
  return;
3786
+ case "group":
3787
+ applyGroupField(section, tokens, line);
3788
+ return;
3789
+ case "relation":
3790
+ applyRelationField(section, tokens, line);
3791
+ return;
3792
+ case "event":
3793
+ applyEventField(section, indent, tokens, line);
3794
+ return;
2680
3795
  case "object":
2681
3796
  applyObjectField(section, indent, tokens, line);
2682
3797
  return;
@@ -2684,10 +3799,35 @@
2684
3799
  }
2685
3800
  function applySystemField(section, tokens, line) {
2686
3801
  const key = requireUniqueField(tokens, section.seenFields, line);
2687
- if (key !== "title") {
2688
- throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
3802
+ const value = joinFieldValue(tokens, line);
3803
+ switch (key) {
3804
+ case "title":
3805
+ section.system.title = value;
3806
+ return;
3807
+ case "description":
3808
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
3809
+ line,
3810
+ column: tokens[0].column
3811
+ });
3812
+ section.system.description = value;
3813
+ return;
3814
+ case "epoch":
3815
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
3816
+ line,
3817
+ column: tokens[0].column
3818
+ });
3819
+ section.system.epoch = value;
3820
+ return;
3821
+ case "referenceplane":
3822
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "referencePlane", {
3823
+ line,
3824
+ column: tokens[0].column
3825
+ });
3826
+ section.system.referencePlane = value;
3827
+ return;
3828
+ default:
3829
+ throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
2689
3830
  }
2690
- section.system.title = joinFieldValue(tokens, line);
2691
3831
  }
2692
3832
  function applyDefaultsField(section, tokens, line) {
2693
3833
  const key = requireUniqueField(tokens, section.seenFields, line);
@@ -2718,14 +3858,11 @@
2718
3858
  section.metadataIndent = null;
2719
3859
  }
2720
3860
  if (section.inMetadata) {
2721
- if (tokens.length < 2) {
2722
- throw new WorldOrbitError("Invalid atlas metadata entry", line, tokens[0]?.column ?? 1);
2723
- }
2724
- const key = tokens[0].value;
2725
- if (key in section.system.atlasMetadata) {
2726
- throw new WorldOrbitError(`Duplicate atlas metadata key "${key}"`, line, tokens[0].column);
3861
+ const entry = parseInfoLikeEntry(tokens, line, "Invalid atlas metadata entry");
3862
+ if (entry.key in section.system.atlasMetadata) {
3863
+ throw new WorldOrbitError(`Duplicate atlas metadata key "${entry.key}"`, line, tokens[0].column);
2727
3864
  }
2728
- section.system.atlasMetadata[key] = joinFieldValue(tokens, line);
3865
+ section.system.atlasMetadata[entry.key] = entry.value;
2729
3866
  return;
2730
3867
  }
2731
3868
  if (tokens.length === 1 && tokens[0].value.toLowerCase() === "metadata") {
@@ -2781,7 +3918,14 @@
2781
3918
  section.viewpoint.rotationDeg = parseFiniteNumber2(value, line, tokens[0].column, "rotation");
2782
3919
  return;
2783
3920
  case "layers":
2784
- section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line);
3921
+ section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line, section.sourceSchemaVersion, section.diagnostics);
3922
+ return;
3923
+ case "events":
3924
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.events", {
3925
+ line,
3926
+ column: tokens[0].column
3927
+ });
3928
+ section.viewpoint.events = parseTokenList(tokens.slice(1), line, "events");
2785
3929
  return;
2786
3930
  default:
2787
3931
  throw new WorldOrbitError(`Unknown viewpoint field "${tokens[0].value}"`, line, tokens[0].column);
@@ -2806,42 +3950,223 @@
2806
3950
  default:
2807
3951
  throw new WorldOrbitError(`Unknown viewpoint filter field "${tokens[0].value}"`, line, tokens[0].column);
2808
3952
  }
2809
- section.viewpoint.filter = filter;
2810
- }
2811
- function applyAnnotationField(section, tokens, line) {
3953
+ section.viewpoint.filter = filter;
3954
+ }
3955
+ function applyAnnotationField(section, tokens, line) {
3956
+ const key = requireUniqueField(tokens, section.seenFields, line);
3957
+ switch (key) {
3958
+ case "label":
3959
+ section.annotation.label = joinFieldValue(tokens, line);
3960
+ return;
3961
+ case "target":
3962
+ section.annotation.targetObjectId = joinFieldValue(tokens, line);
3963
+ return;
3964
+ case "body":
3965
+ section.annotation.body = joinFieldValue(tokens, line);
3966
+ return;
3967
+ case "tags":
3968
+ section.annotation.tags = parseTokenList(tokens.slice(1), line, "tags");
3969
+ return;
3970
+ default:
3971
+ throw new WorldOrbitError(`Unknown annotation field "${tokens[0].value}"`, line, tokens[0].column);
3972
+ }
3973
+ }
3974
+ function applyGroupField(section, tokens, line) {
3975
+ const key = requireUniqueField(tokens, section.seenFields, line);
3976
+ switch (key) {
3977
+ case "label":
3978
+ section.group.label = joinFieldValue(tokens, line);
3979
+ return;
3980
+ case "summary":
3981
+ section.group.summary = joinFieldValue(tokens, line);
3982
+ return;
3983
+ case "color":
3984
+ section.group.color = joinFieldValue(tokens, line);
3985
+ return;
3986
+ case "tags":
3987
+ section.group.tags = parseTokenList(tokens.slice(1), line, "tags");
3988
+ return;
3989
+ case "hidden":
3990
+ section.group.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
3991
+ line,
3992
+ column: tokens[0].column
3993
+ });
3994
+ return;
3995
+ default:
3996
+ throw new WorldOrbitError(`Unknown group field "${tokens[0].value}"`, line, tokens[0].column);
3997
+ }
3998
+ }
3999
+ function applyRelationField(section, tokens, line) {
4000
+ const key = requireUniqueField(tokens, section.seenFields, line);
4001
+ switch (key) {
4002
+ case "from":
4003
+ section.relation.from = joinFieldValue(tokens, line);
4004
+ return;
4005
+ case "to":
4006
+ section.relation.to = joinFieldValue(tokens, line);
4007
+ return;
4008
+ case "kind":
4009
+ section.relation.kind = joinFieldValue(tokens, line);
4010
+ return;
4011
+ case "label":
4012
+ section.relation.label = joinFieldValue(tokens, line);
4013
+ return;
4014
+ case "summary":
4015
+ section.relation.summary = joinFieldValue(tokens, line);
4016
+ return;
4017
+ case "tags":
4018
+ section.relation.tags = parseTokenList(tokens.slice(1), line, "tags");
4019
+ return;
4020
+ case "color":
4021
+ section.relation.color = joinFieldValue(tokens, line);
4022
+ return;
4023
+ case "hidden":
4024
+ section.relation.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4025
+ line,
4026
+ column: tokens[0].column
4027
+ });
4028
+ return;
4029
+ default:
4030
+ throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
4031
+ }
4032
+ }
4033
+ function applyEventField(section, indent, tokens, line) {
4034
+ if (section.activePose && indent <= (section.poseIndent ?? 0)) {
4035
+ section.activePose = null;
4036
+ section.poseIndent = null;
4037
+ section.activePoseSeenFields.clear();
4038
+ }
4039
+ if (!section.activePose && section.inPositions && indent <= (section.positionsIndent ?? 0)) {
4040
+ section.inPositions = false;
4041
+ section.positionsIndent = null;
4042
+ }
4043
+ if (section.activePose) {
4044
+ section.activePose.fields.push(parseEventPoseField(tokens, line, section.activePoseSeenFields));
4045
+ return;
4046
+ }
4047
+ if (section.inPositions) {
4048
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "pose") {
4049
+ throw new WorldOrbitError(`Unknown event positions field "${tokens[0].value}"`, line, tokens[0]?.column ?? 1);
4050
+ }
4051
+ const objectId = tokens[1].value;
4052
+ if (!objectId.trim()) {
4053
+ throw new WorldOrbitError("Event pose object id must not be empty", line, tokens[1].column);
4054
+ }
4055
+ const rawPose = {
4056
+ objectId,
4057
+ fields: [],
4058
+ location: { line, column: tokens[0].column }
4059
+ };
4060
+ section.rawPoses.push(rawPose);
4061
+ section.activePose = rawPose;
4062
+ section.poseIndent = indent;
4063
+ section.activePoseSeenFields = /* @__PURE__ */ new Set();
4064
+ return;
4065
+ }
4066
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "positions") {
4067
+ if (section.seenFields.has("positions")) {
4068
+ throw new WorldOrbitError('Duplicate event field "positions"', line, tokens[0].column);
4069
+ }
4070
+ section.seenFields.add("positions");
4071
+ section.inPositions = true;
4072
+ section.positionsIndent = indent;
4073
+ return;
4074
+ }
2812
4075
  const key = requireUniqueField(tokens, section.seenFields, line);
2813
4076
  switch (key) {
4077
+ case "kind":
4078
+ section.event.kind = joinFieldValue(tokens, line);
4079
+ return;
2814
4080
  case "label":
2815
- section.annotation.label = joinFieldValue(tokens, line);
4081
+ section.event.label = joinFieldValue(tokens, line);
4082
+ return;
4083
+ case "summary":
4084
+ section.event.summary = joinFieldValue(tokens, line);
2816
4085
  return;
2817
4086
  case "target":
2818
- section.annotation.targetObjectId = joinFieldValue(tokens, line);
4087
+ section.event.targetObjectId = joinFieldValue(tokens, line);
2819
4088
  return;
2820
- case "body":
2821
- section.annotation.body = joinFieldValue(tokens, line);
4089
+ case "participants":
4090
+ section.event.participantObjectIds = parseTokenList(tokens.slice(1), line, "participants");
4091
+ return;
4092
+ case "timing":
4093
+ section.event.timing = joinFieldValue(tokens, line);
4094
+ return;
4095
+ case "visibility":
4096
+ section.event.visibility = joinFieldValue(tokens, line);
2822
4097
  return;
2823
4098
  case "tags":
2824
- section.annotation.tags = parseTokenList(tokens.slice(1), line, "tags");
4099
+ section.event.tags = parseTokenList(tokens.slice(1), line, "tags");
4100
+ return;
4101
+ case "color":
4102
+ section.event.color = joinFieldValue(tokens, line);
4103
+ return;
4104
+ case "hidden":
4105
+ section.event.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4106
+ line,
4107
+ column: tokens[0].column
4108
+ });
2825
4109
  return;
2826
4110
  default:
2827
- throw new WorldOrbitError(`Unknown annotation field "${tokens[0].value}"`, line, tokens[0].column);
4111
+ throw new WorldOrbitError(`Unknown event field "${tokens[0].value}"`, line, tokens[0].column);
2828
4112
  }
2829
4113
  }
2830
- function applyObjectField(section, indent, tokens, line) {
2831
- if (tokens.length === 1 && tokens[0].value === "info") {
2832
- section.inInfoBlock = true;
2833
- section.infoIndent = indent;
2834
- return;
4114
+ function parseEventPoseField(tokens, line, seenFields) {
4115
+ if (tokens.length < 2) {
4116
+ throw new WorldOrbitError("Invalid event pose field line", line, tokens[0]?.column ?? 1);
4117
+ }
4118
+ const key = tokens[0].value;
4119
+ if (!EVENT_POSE_FIELD_KEYS.has(key)) {
4120
+ throw new WorldOrbitError(`Unknown event pose field "${key}"`, line, tokens[0].column);
2835
4121
  }
2836
- if (section.inInfoBlock && indent <= (section.infoIndent ?? 0)) {
2837
- section.inInfoBlock = false;
2838
- section.infoIndent = null;
4122
+ if (seenFields.has(key)) {
4123
+ throw new WorldOrbitError(`Duplicate event pose field "${key}"`, line, tokens[0].column);
4124
+ }
4125
+ seenFields.add(key);
4126
+ return {
4127
+ type: "field",
4128
+ key,
4129
+ values: tokens.slice(1).map((token) => token.value),
4130
+ location: { line, column: tokens[0].column }
4131
+ };
4132
+ }
4133
+ function applyObjectField(section, indent, tokens, line) {
4134
+ if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
4135
+ section.activeBlock = null;
4136
+ section.blockIndent = null;
4137
+ }
4138
+ if (tokens.length === 1) {
4139
+ const blockName = tokens[0].value.toLowerCase();
4140
+ if (blockName === "info" || STRUCTURED_TYPED_BLOCKS.has(blockName)) {
4141
+ if (blockName !== "info") {
4142
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, blockName, { line, column: tokens[0].column });
4143
+ }
4144
+ section.activeBlock = blockName;
4145
+ section.blockIndent = indent;
4146
+ return;
4147
+ }
2839
4148
  }
2840
- if (section.inInfoBlock) {
2841
- section.objectNode.infoEntries.push(parseInfoEntry2(tokens, line));
4149
+ if (section.activeBlock) {
4150
+ const entry = parseInfoLikeEntry(tokens, line, `Invalid ${section.activeBlock} entry`);
4151
+ if (section.activeBlock === "info") {
4152
+ if (section.seenInfoKeys.has(entry.key)) {
4153
+ throw new WorldOrbitError(`Duplicate info key "${entry.key}"`, line, tokens[0].column);
4154
+ }
4155
+ section.seenInfoKeys.add(entry.key);
4156
+ section.objectNode.infoEntries.push(entry);
4157
+ return;
4158
+ }
4159
+ const typedBlock = section.activeBlock;
4160
+ const seenKeys = section.seenTypedBlockKeys[typedBlock] ?? (section.seenTypedBlockKeys[typedBlock] = /* @__PURE__ */ new Set());
4161
+ if (seenKeys.has(entry.key)) {
4162
+ throw new WorldOrbitError(`Duplicate ${typedBlock} key "${entry.key}"`, line, tokens[0].column);
4163
+ }
4164
+ seenKeys.add(entry.key);
4165
+ const entries = section.objectNode.typedBlockEntries[typedBlock] ?? (section.objectNode.typedBlockEntries[typedBlock] = []);
4166
+ entries.push(entry);
2842
4167
  return;
2843
4168
  }
2844
- section.objectNode.blockFields.push(parseField2(tokens, line));
4169
+ section.objectNode.fields.push(parseObjectField(tokens, line, section.objectNode.objectType, section.sourceSchemaVersion, section.diagnostics));
2845
4170
  }
2846
4171
  function requireUniqueField(tokens, seenFields, line) {
2847
4172
  if (tokens.length < 2) {
@@ -2861,50 +4186,46 @@
2861
4186
  return tokens.slice(1).map((token) => token.value).join(" ").trim();
2862
4187
  }
2863
4188
  function parseObjectTypeTokens(tokens, line) {
2864
- if (tokens.length === 0) {
2865
- throw new WorldOrbitError("Missing value for atlas field", line);
2866
- }
2867
- return tokens.map((token) => {
2868
- const value = token.value;
2869
- if (value !== "star" && value !== "planet" && value !== "moon" && value !== "belt" && value !== "asteroid" && value !== "comet" && value !== "ring" && value !== "structure" && value !== "phenomenon") {
2870
- throw new WorldOrbitError(`Unknown viewpoint object type "${token.value}"`, line, token.column);
2871
- }
2872
- return value;
2873
- });
4189
+ 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");
2874
4190
  }
2875
- function parseTokenList(tokens, line, field) {
2876
- if (tokens.length === 0) {
2877
- throw new WorldOrbitError(`Missing value for field "${field}"`, line);
4191
+ function parseLayerTokens(tokens, line, sourceSchemaVersion, diagnostics) {
4192
+ const layers = {};
4193
+ for (const token of parseTokenList(tokens, line, "layers")) {
4194
+ const enabled = !token.startsWith("-") && !token.startsWith("!");
4195
+ const raw = token.replace(/^[-!]+/, "").toLowerCase();
4196
+ if (raw === "orbits") {
4197
+ layers["orbits-back"] = enabled;
4198
+ layers["orbits-front"] = enabled;
4199
+ continue;
4200
+ }
4201
+ if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "events" || raw === "objects" || raw === "labels" || raw === "metadata") {
4202
+ if (raw === "events" && sourceSchemaVersion && diagnostics) {
4203
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "layers.events", {
4204
+ line,
4205
+ column: tokens[0]?.column ?? 1
4206
+ });
4207
+ }
4208
+ layers[raw] = enabled;
4209
+ }
2878
4210
  }
2879
- return tokens.map((token) => token.value);
4211
+ return layers;
2880
4212
  }
2881
- function parseLayerTokens(tokens, line) {
4213
+ function parseTokenList(tokens, line, fieldName) {
2882
4214
  if (tokens.length === 0) {
2883
- throw new WorldOrbitError('Missing value for field "layers"', line);
4215
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, 1);
2884
4216
  }
2885
- const next = {};
2886
- for (const token of tokens) {
2887
- const enabled = !token.value.startsWith("-") && !token.value.startsWith("!");
2888
- const rawLayer = token.value.replace(/^[-!]+/, "").toLowerCase();
2889
- if (rawLayer === "orbits") {
2890
- next["orbits-back"] = enabled;
2891
- next["orbits-front"] = enabled;
2892
- continue;
2893
- }
2894
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
2895
- next[rawLayer] = enabled;
2896
- continue;
2897
- }
2898
- throw new WorldOrbitError(`Unknown layer token "${token.value}"`, line, token.column);
4217
+ const values = tokens.map((token) => token.value).filter(Boolean);
4218
+ if (values.length === 0) {
4219
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, tokens[0]?.column ?? 1);
2899
4220
  }
2900
- return next;
4221
+ return values;
2901
4222
  }
2902
4223
  function parseProjectionValue(value, line, column) {
2903
4224
  const normalized = value.toLowerCase();
2904
- if (normalized === "topdown" || normalized === "isometric") {
2905
- return normalized;
4225
+ if (normalized !== "topdown" && normalized !== "isometric") {
4226
+ throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
2906
4227
  }
2907
- throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
4228
+ return normalized;
2908
4229
  }
2909
4230
  function parsePresetValue(value, line, column) {
2910
4231
  const normalized = value.toLowerCase();
@@ -2914,16 +4235,16 @@
2914
4235
  throw new WorldOrbitError(`Unknown render preset "${value}"`, line, column);
2915
4236
  }
2916
4237
  function parsePositiveNumber2(value, line, column, field) {
2917
- const parsed = Number(value);
2918
- if (!Number.isFinite(parsed) || parsed <= 0) {
2919
- throw new WorldOrbitError(`Field "${field}" expects a positive number`, line, column);
4238
+ const parsed = parseFiniteNumber2(value, line, column, field);
4239
+ if (parsed <= 0) {
4240
+ throw new WorldOrbitError(`Field "${field}" must be greater than zero`, line, column);
2920
4241
  }
2921
4242
  return parsed;
2922
4243
  }
2923
4244
  function parseFiniteNumber2(value, line, column, field) {
2924
4245
  const parsed = Number(value);
2925
4246
  if (!Number.isFinite(parsed)) {
2926
- throw new WorldOrbitError(`Field "${field}" expects a finite number`, line, column);
4247
+ throw new WorldOrbitError(`Invalid numeric value "${value}" for "${field}"`, line, column);
2927
4248
  }
2928
4249
  return parsed;
2929
4250
  }
@@ -2935,28 +4256,43 @@
2935
4256
  groupIds: []
2936
4257
  };
2937
4258
  }
2938
- function parseInlineFields2(tokens, line) {
4259
+ function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
2939
4260
  const fields = [];
2940
4261
  let index = 0;
2941
4262
  while (index < tokens.length) {
2942
4263
  const keyToken = tokens[index];
2943
- const schema = getFieldSchema(keyToken.value);
2944
- if (!schema) {
4264
+ const spec = getDraftObjectFieldSpec(keyToken.value);
4265
+ if (!spec) {
2945
4266
  throw new WorldOrbitError(`Unknown field "${keyToken.value}"`, line, keyToken.column);
2946
4267
  }
4268
+ if (spec.version === "2.1") {
4269
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, keyToken.value, {
4270
+ line,
4271
+ column: keyToken.column
4272
+ });
4273
+ }
2947
4274
  index++;
2948
4275
  const valueTokens = [];
2949
- if (schema.arity === "multiple") {
2950
- while (index < tokens.length && !isKnownFieldKey(tokens[index].value)) {
2951
- valueTokens.push(tokens[index]);
2952
- index++;
2953
- }
2954
- } else {
4276
+ if (spec.inlineMode === "single") {
2955
4277
  const nextToken = tokens[index];
2956
4278
  if (nextToken) {
2957
4279
  valueTokens.push(nextToken);
2958
4280
  index++;
2959
4281
  }
4282
+ } else if (spec.inlineMode === "pair") {
4283
+ for (let count = 0; count < 2; count++) {
4284
+ const nextToken = tokens[index];
4285
+ if (!nextToken) {
4286
+ break;
4287
+ }
4288
+ valueTokens.push(nextToken);
4289
+ index++;
4290
+ }
4291
+ } else {
4292
+ while (index < tokens.length && !DRAFT_OBJECT_FIELD_KEYS.has(tokens[index].value)) {
4293
+ valueTokens.push(tokens[index]);
4294
+ index++;
4295
+ }
2960
4296
  }
2961
4297
  if (valueTokens.length === 0) {
2962
4298
  throw new WorldOrbitError(`Missing value for field "${keyToken.value}"`, line, keyToken.column);
@@ -2968,25 +4304,35 @@
2968
4304
  location: { line, column: keyToken.column }
2969
4305
  });
2970
4306
  }
4307
+ validateDraftObjectFieldCompatibility(fields, objectType);
2971
4308
  return fields;
2972
4309
  }
2973
- function parseField2(tokens, line) {
4310
+ function parseObjectField(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
2974
4311
  if (tokens.length < 2) {
2975
4312
  throw new WorldOrbitError("Invalid field line", line, tokens[0]?.column ?? 1);
2976
4313
  }
2977
- if (!getFieldSchema(tokens[0].value)) {
4314
+ const spec = getDraftObjectFieldSpec(tokens[0].value);
4315
+ if (!spec) {
2978
4316
  throw new WorldOrbitError(`Unknown field "${tokens[0].value}"`, line, tokens[0].column);
2979
4317
  }
2980
- return {
4318
+ if (spec.version === "2.1") {
4319
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, tokens[0].value, {
4320
+ line,
4321
+ column: tokens[0].column
4322
+ });
4323
+ }
4324
+ const field = {
2981
4325
  type: "field",
2982
4326
  key: tokens[0].value,
2983
4327
  values: tokens.slice(1).map((token) => token.value),
2984
4328
  location: { line, column: tokens[0].column }
2985
4329
  };
4330
+ validateDraftObjectFieldCompatibility([field], objectType);
4331
+ return field;
2986
4332
  }
2987
- function parseInfoEntry2(tokens, line) {
4333
+ function parseInfoLikeEntry(tokens, line, errorMessage) {
2988
4334
  if (tokens.length < 2) {
2989
- throw new WorldOrbitError("Invalid info entry", line, tokens[0]?.column ?? 1);
4335
+ throw new WorldOrbitError(errorMessage, line, tokens[0]?.column ?? 1);
2990
4336
  }
2991
4337
  return {
2992
4338
  type: "info-entry",
@@ -2995,18 +4341,366 @@
2995
4341
  location: { line, column: tokens[0].column }
2996
4342
  };
2997
4343
  }
2998
- function normalizeIdentifier(value) {
2999
- return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
4344
+ function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
4345
+ const fieldMap = collectDraftFields(node.fields);
4346
+ const placement = extractPlacementFromFieldMap(fieldMap);
4347
+ const properties = normalizeDraftProperties(node.objectType, fieldMap);
4348
+ const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
4349
+ const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
4350
+ const referencePlane = parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0]);
4351
+ const tidalLock = fieldMap.has("tidalLock") ? parseAtlasBoolean(singleFieldValue2(fieldMap.get("tidalLock")[0]), "tidalLock", fieldMap.get("tidalLock")[0].location) : void 0;
4352
+ const resonance = fieldMap.has("resonance") ? parseResonanceField(fieldMap.get("resonance")[0]) : void 0;
4353
+ const renderHints = extractRenderHints(fieldMap);
4354
+ const deriveRules = fieldMap.get("derive")?.map((field) => parseDeriveField(field));
4355
+ const validationRules = fieldMap.get("validate")?.map((field) => ({
4356
+ rule: singleFieldValue2(field)
4357
+ }));
4358
+ const lockedFields = fieldMap.has("locked") ? [...new Set(fieldMap.get("locked").flatMap((field) => field.values))] : void 0;
4359
+ const tolerances = fieldMap.get("tolerance")?.map((field) => parseToleranceField(field));
4360
+ const typedBlocks = normalizeTypedBlocks(node.typedBlockEntries);
4361
+ const info2 = normalizeInfoEntries(node.infoEntries, "info");
4362
+ const object = {
4363
+ type: node.objectType,
4364
+ id: node.id,
4365
+ properties,
4366
+ placement,
4367
+ info: info2
4368
+ };
4369
+ if (groups.length > 0)
4370
+ object.groups = groups;
4371
+ if (epoch)
4372
+ object.epoch = epoch;
4373
+ if (referencePlane)
4374
+ object.referencePlane = referencePlane;
4375
+ if (tidalLock !== void 0)
4376
+ object.tidalLock = tidalLock;
4377
+ if (resonance)
4378
+ object.resonance = resonance;
4379
+ if (renderHints)
4380
+ object.renderHints = renderHints;
4381
+ if (deriveRules?.length)
4382
+ object.deriveRules = deriveRules;
4383
+ if (validationRules?.length)
4384
+ object.validationRules = validationRules;
4385
+ if (lockedFields?.length)
4386
+ object.lockedFields = lockedFields;
4387
+ if (tolerances?.length)
4388
+ object.tolerances = tolerances;
4389
+ if (typedBlocks && Object.keys(typedBlocks).length > 0)
4390
+ object.typedBlocks = typedBlocks;
4391
+ if (sourceSchemaVersion !== "2.1") {
4392
+ 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) {
4393
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
4394
+ }
4395
+ }
4396
+ return object;
3000
4397
  }
3001
- function humanizeIdentifier2(value) {
3002
- return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
4398
+ function normalizeDraftEvent(event, rawPoses) {
4399
+ return {
4400
+ ...event,
4401
+ participantObjectIds: [...new Set(event.participantObjectIds)],
4402
+ tags: [...new Set(event.tags)],
4403
+ positions: rawPoses.map((pose) => normalizeDraftEventPose(pose))
4404
+ };
4405
+ }
4406
+ function normalizeDraftEventPose(rawPose) {
4407
+ const fieldMap = collectDraftFields(rawPose.fields);
4408
+ const placement = extractPlacementFromFieldMap(fieldMap);
4409
+ return {
4410
+ objectId: rawPose.objectId,
4411
+ placement,
4412
+ inner: parseOptionalUnitField(fieldMap.get("inner")?.[0], "inner"),
4413
+ outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer")
4414
+ };
4415
+ }
4416
+ function collectDraftFields(fields) {
4417
+ const grouped = /* @__PURE__ */ new Map();
4418
+ for (const field of fields) {
4419
+ const spec = getDraftObjectFieldSpec(field.key);
4420
+ if (!spec) {
4421
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
4422
+ }
4423
+ if (!spec.allowRepeat && grouped.has(field.key)) {
4424
+ throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
4425
+ }
4426
+ const existing = grouped.get(field.key) ?? [];
4427
+ existing.push(field);
4428
+ grouped.set(field.key, existing);
4429
+ }
4430
+ return grouped;
4431
+ }
4432
+ function extractPlacementFromFieldMap(fieldMap) {
4433
+ const orbitField = fieldMap.get("orbit")?.[0];
4434
+ const atField = fieldMap.get("at")?.[0];
4435
+ const surfaceField = fieldMap.get("surface")?.[0];
4436
+ const freeField = fieldMap.get("free")?.[0];
4437
+ const count = [orbitField, atField, surfaceField, freeField].filter(Boolean).length;
4438
+ if (count > 1) {
4439
+ const conflictingField = orbitField ?? atField ?? surfaceField ?? freeField;
4440
+ throw WorldOrbitError.fromLocation("Object has multiple placement modes", conflictingField?.location);
4441
+ }
4442
+ if (orbitField) {
4443
+ return {
4444
+ mode: "orbit",
4445
+ target: singleFieldValue2(orbitField),
4446
+ distance: parseOptionalUnitField(fieldMap.get("distance")?.[0], "distance"),
4447
+ semiMajor: parseOptionalUnitField(fieldMap.get("semiMajor")?.[0], "semiMajor"),
4448
+ eccentricity: parseOptionalNumberField(fieldMap.get("eccentricity")?.[0], "eccentricity"),
4449
+ period: parseOptionalUnitField(fieldMap.get("period")?.[0], "period"),
4450
+ angle: parseOptionalUnitField(fieldMap.get("angle")?.[0], "angle"),
4451
+ inclination: parseOptionalUnitField(fieldMap.get("inclination")?.[0], "inclination"),
4452
+ phase: parseOptionalUnitField(fieldMap.get("phase")?.[0], "phase")
4453
+ };
4454
+ }
4455
+ if (atField) {
4456
+ const target = singleFieldValue2(atField);
4457
+ return {
4458
+ mode: "at",
4459
+ target,
4460
+ reference: parseAtlasAtReference(target, atField.location)
4461
+ };
4462
+ }
4463
+ if (surfaceField) {
4464
+ return {
4465
+ mode: "surface",
4466
+ target: singleFieldValue2(surfaceField)
4467
+ };
4468
+ }
4469
+ if (freeField) {
4470
+ const raw = singleFieldValue2(freeField);
4471
+ const distance = tryParseAtlasUnitValue(raw);
4472
+ return {
4473
+ mode: "free",
4474
+ distance: distance ?? void 0,
4475
+ descriptor: distance ? void 0 : raw
4476
+ };
4477
+ }
4478
+ return null;
4479
+ }
4480
+ function normalizeDraftProperties(objectType, fieldMap) {
4481
+ const properties = {};
4482
+ for (const [key, fields] of fieldMap.entries()) {
4483
+ const field = fields[0];
4484
+ const spec = getDraftObjectFieldSpec(key);
4485
+ if (!field || !spec?.legacySchema || spec.legacySchema.placement) {
4486
+ continue;
4487
+ }
4488
+ ensureAtlasFieldSupported(key, objectType, field.location);
4489
+ properties[key] = normalizeLegacyScalarValue(key, field.values, field.location);
4490
+ }
4491
+ return properties;
4492
+ }
4493
+ function normalizeInfoEntries(entries, label) {
4494
+ const normalized = {};
4495
+ for (const entry of entries) {
4496
+ if (entry.key in normalized) {
4497
+ throw WorldOrbitError.fromLocation(`Duplicate ${label} key "${entry.key}"`, entry.location);
4498
+ }
4499
+ normalized[entry.key] = entry.value;
4500
+ }
4501
+ return normalized;
4502
+ }
4503
+ function normalizeTypedBlocks(typedBlockEntries) {
4504
+ const typedBlocks = {};
4505
+ for (const blockName of Object.keys(typedBlockEntries)) {
4506
+ const entries = typedBlockEntries[blockName];
4507
+ if (entries?.length) {
4508
+ typedBlocks[blockName] = normalizeInfoEntries(entries, blockName);
4509
+ }
4510
+ }
4511
+ return typedBlocks;
4512
+ }
4513
+ function extractRenderHints(fieldMap) {
4514
+ const renderHints = {};
4515
+ const renderLabelField = fieldMap.get("renderLabel")?.[0];
4516
+ const renderOrbitField = fieldMap.get("renderOrbit")?.[0];
4517
+ const renderPriorityField = fieldMap.get("renderPriority")?.[0];
4518
+ if (renderLabelField) {
4519
+ renderHints.renderLabel = parseAtlasBoolean(singleFieldValue2(renderLabelField), "renderLabel", renderLabelField.location);
4520
+ }
4521
+ if (renderOrbitField) {
4522
+ renderHints.renderOrbit = parseAtlasBoolean(singleFieldValue2(renderOrbitField), "renderOrbit", renderOrbitField.location);
4523
+ }
4524
+ if (renderPriorityField) {
4525
+ renderHints.renderPriority = parseAtlasNumber(singleFieldValue2(renderPriorityField), "renderPriority", renderPriorityField.location);
4526
+ }
4527
+ return Object.keys(renderHints).length > 0 ? renderHints : void 0;
4528
+ }
4529
+ function parseResonanceField(field) {
4530
+ if (field.values.length !== 2) {
4531
+ throw WorldOrbitError.fromLocation('Field "resonance" expects "<targetObjectId> <ratio>"', field.location);
4532
+ }
4533
+ const ratio = field.values[1];
4534
+ if (!/^\d+:\d+$/.test(ratio)) {
4535
+ throw WorldOrbitError.fromLocation(`Invalid resonance ratio "${ratio}"`, field.location);
4536
+ }
4537
+ return {
4538
+ targetObjectId: field.values[0],
4539
+ ratio
4540
+ };
4541
+ }
4542
+ function parseDeriveField(field) {
4543
+ if (field.values.length !== 2) {
4544
+ throw WorldOrbitError.fromLocation('Field "derive" expects "<field> <strategy>"', field.location);
4545
+ }
4546
+ return {
4547
+ field: field.values[0],
4548
+ strategy: field.values[1]
4549
+ };
4550
+ }
4551
+ function parseToleranceField(field) {
4552
+ if (field.values.length !== 2) {
4553
+ throw WorldOrbitError.fromLocation('Field "tolerance" expects "<field> <value>"', field.location);
4554
+ }
4555
+ const rawValue = field.values[1];
4556
+ const unitValue = tryParseAtlasUnitValue(rawValue);
4557
+ const numericValue2 = Number(rawValue);
4558
+ return {
4559
+ field: field.values[0],
4560
+ value: unitValue ?? (Number.isFinite(numericValue2) ? numericValue2 : rawValue)
4561
+ };
4562
+ }
4563
+ function parseOptionalTokenList(field) {
4564
+ return field ? [...new Set(field.values)] : [];
4565
+ }
4566
+ function parseOptionalJoinedValue(field) {
4567
+ if (!field) {
4568
+ return null;
4569
+ }
4570
+ return field.values.join(" ").trim() || null;
4571
+ }
4572
+ function parseOptionalUnitField(field, key) {
4573
+ return field ? parseAtlasUnitValue(singleFieldValue2(field), field.location, key) : void 0;
4574
+ }
4575
+ function parseOptionalNumberField(field, key) {
4576
+ return field ? parseAtlasNumber(singleFieldValue2(field), key, field.location) : void 0;
4577
+ }
4578
+ function singleFieldValue2(field) {
4579
+ return singleAtlasValue(field.values, field.key, field.location);
4580
+ }
4581
+ function getDraftObjectFieldSpec(key) {
4582
+ return DRAFT_OBJECT_FIELD_SPECS.get(key);
4583
+ }
4584
+ function validateDraftObjectFieldCompatibility(fields, objectType) {
4585
+ for (const field of fields) {
4586
+ const spec = getDraftObjectFieldSpec(field.key);
4587
+ if (!spec) {
4588
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
4589
+ }
4590
+ if (spec.legacySchema) {
4591
+ ensureAtlasFieldSupported(field.key, objectType, field.location);
4592
+ continue;
4593
+ }
4594
+ if ((field.key === "renderLabel" || field.key === "renderOrbit" || field.key === "tidalLock") && field.values.length !== 1) {
4595
+ throw WorldOrbitError.fromLocation(`Field "${field.key}" expects exactly one value`, field.location);
4596
+ }
4597
+ }
4598
+ }
4599
+ function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
4600
+ if (sourceSchemaVersion === "2.1") {
4601
+ return;
4602
+ }
4603
+ diagnostics.push({
4604
+ code: "parse.schema21.featureCompatibility",
4605
+ severity: "warning",
4606
+ source: "parse",
4607
+ message: `Feature "${featureName}" requires schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
4608
+ line: location.line,
4609
+ column: location.column
4610
+ });
4611
+ }
4612
+ function preprocessAtlasSource(source) {
4613
+ const chars = [...source];
4614
+ const comments = [];
4615
+ let inString = false;
4616
+ let inBlockComment = false;
4617
+ let blockCommentStart = null;
4618
+ let line = 1;
4619
+ let column = 1;
4620
+ for (let index = 0; index < chars.length; index++) {
4621
+ const ch = chars[index];
4622
+ const next = chars[index + 1];
4623
+ if (inBlockComment) {
4624
+ if (ch === "*" && next === "/") {
4625
+ chars[index] = " ";
4626
+ chars[index + 1] = " ";
4627
+ inBlockComment = false;
4628
+ blockCommentStart = null;
4629
+ index++;
4630
+ column += 2;
4631
+ continue;
4632
+ }
4633
+ if (ch !== "\n" && ch !== "\r") {
4634
+ chars[index] = " ";
4635
+ }
4636
+ if (ch === "\n") {
4637
+ line++;
4638
+ column = 1;
4639
+ } else {
4640
+ column++;
4641
+ }
4642
+ continue;
4643
+ }
4644
+ if (!inString && ch === "/" && next === "*") {
4645
+ comments.push({ kind: "block", line, column });
4646
+ chars[index] = " ";
4647
+ chars[index + 1] = " ";
4648
+ inBlockComment = true;
4649
+ blockCommentStart = { line, column };
4650
+ index++;
4651
+ column += 2;
4652
+ continue;
4653
+ }
4654
+ if (!inString && ch === "#" && !isHexColorLiteral(chars, index)) {
4655
+ comments.push({ kind: "line", line, column });
4656
+ chars[index] = " ";
4657
+ let inner = index + 1;
4658
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
4659
+ chars[inner] = " ";
4660
+ inner++;
4661
+ }
4662
+ column += inner - index;
4663
+ index = inner - 1;
4664
+ continue;
4665
+ }
4666
+ if (ch === '"' && chars[index - 1] !== "\\") {
4667
+ inString = !inString;
4668
+ }
4669
+ if (ch === "\n") {
4670
+ line++;
4671
+ column = 1;
4672
+ } else {
4673
+ column++;
4674
+ }
4675
+ }
4676
+ if (inBlockComment) {
4677
+ throw WorldOrbitError.fromLocation("Unclosed block comment", blockCommentStart ?? void 0);
4678
+ }
4679
+ return {
4680
+ source: chars.join(""),
4681
+ comments
4682
+ };
4683
+ }
4684
+ function isHexColorLiteral(chars, start) {
4685
+ let index = start + 1;
4686
+ let length = 0;
4687
+ while (index < chars.length && /[0-9a-f]/i.test(chars[index] ?? "")) {
4688
+ index++;
4689
+ length++;
4690
+ }
4691
+ if (![3, 4, 6, 8].includes(length)) {
4692
+ return false;
4693
+ }
4694
+ const next = chars[index];
4695
+ return next === void 0 || next === " " || next === " " || next === "\r" || next === "\n";
3003
4696
  }
3004
4697
 
3005
4698
  // packages/core/dist/load.js
3006
- var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0)?$/i;
4699
+ var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1)?$/i;
4700
+ var ATLAS_SCHEMA_21_PATTERN = /^schema\s+2\.1$/i;
3007
4701
  var LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
3008
4702
  function detectWorldOrbitSchemaVersion(source) {
3009
- for (const line of source.split(/\r?\n/)) {
4703
+ for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
3010
4704
  const trimmed = line.trim();
3011
4705
  if (!trimmed) {
3012
4706
  continue;
@@ -3014,6 +4708,9 @@
3014
4708
  if (LEGACY_DRAFT_SCHEMA_PATTERN.test(trimmed)) {
3015
4709
  return "2.0-draft";
3016
4710
  }
4711
+ if (ATLAS_SCHEMA_21_PATTERN.test(trimmed)) {
4712
+ return "2.1";
4713
+ }
3017
4714
  if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
3018
4715
  return "2.0";
3019
4716
  }
@@ -3021,6 +4718,49 @@
3021
4718
  }
3022
4719
  return "1.0";
3023
4720
  }
4721
+ function stripCommentsForSchemaDetection(source) {
4722
+ const chars = [...source];
4723
+ let inString = false;
4724
+ let inBlockComment = false;
4725
+ for (let index = 0; index < chars.length; index++) {
4726
+ const ch = chars[index];
4727
+ const next = chars[index + 1];
4728
+ if (inBlockComment) {
4729
+ if (ch === "*" && next === "/") {
4730
+ chars[index] = " ";
4731
+ chars[index + 1] = " ";
4732
+ inBlockComment = false;
4733
+ index++;
4734
+ continue;
4735
+ }
4736
+ if (ch !== "\n" && ch !== "\r") {
4737
+ chars[index] = " ";
4738
+ }
4739
+ continue;
4740
+ }
4741
+ if (!inString && ch === "/" && next === "*") {
4742
+ chars[index] = " ";
4743
+ chars[index + 1] = " ";
4744
+ inBlockComment = true;
4745
+ index++;
4746
+ continue;
4747
+ }
4748
+ if (!inString && ch === "#") {
4749
+ chars[index] = " ";
4750
+ let inner = index + 1;
4751
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
4752
+ chars[inner] = " ";
4753
+ inner++;
4754
+ }
4755
+ index = inner - 1;
4756
+ continue;
4757
+ }
4758
+ if (ch === '"' && chars[index - 1] !== "\\") {
4759
+ inString = !inString;
4760
+ }
4761
+ }
4762
+ return chars.join("");
4763
+ }
3024
4764
  function loadWorldOrbitSource(source) {
3025
4765
  const result = loadWorldOrbitSourceWithDiagnostics(source);
3026
4766
  if (!result.ok || !result.value) {
@@ -3031,36 +4771,36 @@
3031
4771
  }
3032
4772
  function loadWorldOrbitSourceWithDiagnostics(source) {
3033
4773
  const schemaVersion = detectWorldOrbitSchemaVersion(source);
3034
- if (schemaVersion === "2.0" || schemaVersion === "2.0-draft") {
4774
+ if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1") {
3035
4775
  return loadAtlasSourceWithDiagnostics(source, schemaVersion);
3036
4776
  }
3037
4777
  let ast;
3038
4778
  try {
3039
4779
  ast = parseWorldOrbit(source);
3040
- } catch (error) {
4780
+ } catch (error2) {
3041
4781
  return {
3042
4782
  ok: false,
3043
4783
  value: null,
3044
- diagnostics: [diagnosticFromError(error, "parse")]
4784
+ diagnostics: [diagnosticFromError(error2, "parse")]
3045
4785
  };
3046
4786
  }
3047
4787
  let document2;
3048
4788
  try {
3049
4789
  document2 = normalizeDocument(ast);
3050
- } catch (error) {
4790
+ } catch (error2) {
3051
4791
  return {
3052
4792
  ok: false,
3053
4793
  value: null,
3054
- diagnostics: [diagnosticFromError(error, "normalize")]
4794
+ diagnostics: [diagnosticFromError(error2, "normalize")]
3055
4795
  };
3056
4796
  }
3057
4797
  try {
3058
4798
  validateDocument(document2);
3059
- } catch (error) {
4799
+ } catch (error2) {
3060
4800
  return {
3061
4801
  ok: false,
3062
4802
  value: null,
3063
- diagnostics: [diagnosticFromError(error, "validate")]
4803
+ diagnostics: [diagnosticFromError(error2, "validate")]
3064
4804
  };
3065
4805
  }
3066
4806
  return {
@@ -3080,30 +4820,29 @@
3080
4820
  let atlasDocument;
3081
4821
  try {
3082
4822
  atlasDocument = parseWorldOrbitAtlas(source);
3083
- } catch (error) {
4823
+ } catch (error2) {
3084
4824
  return {
3085
4825
  ok: false,
3086
4826
  value: null,
3087
- diagnostics: [diagnosticFromError(error, "parse", "load.atlas.failed")]
4827
+ diagnostics: [diagnosticFromError(error2, "parse", "load.atlas.failed")]
3088
4828
  };
3089
4829
  }
3090
- let document2;
3091
- try {
3092
- document2 = materializeAtlasDocument(atlasDocument);
3093
- } catch (error) {
4830
+ const atlasDiagnostics = [...atlasDocument.diagnostics];
4831
+ if (atlasDiagnostics.some((diagnostic) => diagnostic.severity === "error")) {
3094
4832
  return {
3095
4833
  ok: false,
3096
4834
  value: null,
3097
- diagnostics: [diagnosticFromError(error, "normalize", "load.atlas.materialize.failed")]
4835
+ diagnostics: atlasDiagnostics
3098
4836
  };
3099
4837
  }
4838
+ let document2;
3100
4839
  try {
3101
- validateDocument(document2);
3102
- } catch (error) {
4840
+ document2 = materializeAtlasDocument(atlasDocument);
4841
+ } catch (error2) {
3103
4842
  return {
3104
4843
  ok: false,
3105
4844
  value: null,
3106
- diagnostics: [diagnosticFromError(error, "validate", "load.atlas.validate.failed")]
4845
+ diagnostics: [diagnosticFromError(error2, "normalize", "load.atlas.materialize.failed")]
3107
4846
  };
3108
4847
  }
3109
4848
  const loaded = {
@@ -3112,12 +4851,12 @@
3112
4851
  document: document2,
3113
4852
  atlasDocument,
3114
4853
  draftDocument: atlasDocument,
3115
- diagnostics: [...atlasDocument.diagnostics]
4854
+ diagnostics: atlasDiagnostics
3116
4855
  };
3117
4856
  return {
3118
4857
  ok: true,
3119
4858
  value: loaded,
3120
- diagnostics: [...atlasDocument.diagnostics]
4859
+ diagnostics: atlasDiagnostics
3121
4860
  };
3122
4861
  }
3123
4862
 
@@ -3125,6 +4864,8 @@
3125
4864
  var DEFAULT_LAYERS = {
3126
4865
  background: true,
3127
4866
  guides: true,
4867
+ relations: true,
4868
+ events: true,
3128
4869
  orbits: true,
3129
4870
  objects: true,
3130
4871
  labels: true,
@@ -3139,6 +4880,7 @@
3139
4880
  backgroundGlow: "rgba(240, 180, 100, 0.18)",
3140
4881
  panel: "rgba(7, 17, 27, 0.9)",
3141
4882
  panelLine: "rgba(168, 207, 242, 0.18)",
4883
+ relation: "rgba(240, 180, 100, 0.42)",
3142
4884
  orbit: "rgba(163, 209, 255, 0.24)",
3143
4885
  orbitBand: "rgba(255, 190, 120, 0.28)",
3144
4886
  guide: "rgba(255, 255, 255, 0.04)",
@@ -3161,6 +4903,7 @@
3161
4903
  backgroundGlow: "rgba(120, 255, 215, 0.16)",
3162
4904
  panel: "rgba(7, 20, 30, 0.9)",
3163
4905
  panelLine: "rgba(120, 255, 215, 0.16)",
4906
+ relation: "rgba(156, 231, 255, 0.42)",
3164
4907
  orbit: "rgba(120, 255, 215, 0.2)",
3165
4908
  orbitBand: "rgba(137, 185, 255, 0.24)",
3166
4909
  guide: "rgba(255, 255, 255, 0.035)",
@@ -3183,6 +4926,7 @@
3183
4926
  backgroundGlow: "rgba(255, 127, 95, 0.18)",
3184
4927
  panel: "rgba(24, 9, 13, 0.9)",
3185
4928
  panelLine: "rgba(255, 166, 149, 0.16)",
4929
+ relation: "rgba(255, 178, 125, 0.42)",
3186
4930
  orbit: "rgba(255, 188, 164, 0.22)",
3187
4931
  orbitBand: "rgba(255, 214, 139, 0.24)",
3188
4932
  guide: "rgba(255, 255, 255, 0.03)",
@@ -3264,7 +5008,11 @@
3264
5008
  return false;
3265
5009
  }
3266
5010
  if (filter.groupIds?.length && (!object.groupId || !filter.groupIds.includes(object.groupId))) {
3267
- return false;
5011
+ const hasSemanticMatch = object.semanticGroupIds.length > 0 && filter.groupIds.some((groupId) => object.semanticGroupIds.includes(groupId));
5012
+ const hasLegacyMatch = Boolean(object.groupId && filter.groupIds.includes(object.groupId));
5013
+ if (!hasSemanticMatch && !hasLegacyMatch) {
5014
+ return false;
5015
+ }
3268
5016
  }
3269
5017
  if (filter.tags?.length) {
3270
5018
  const objectTags = Array.isArray(object.object.properties.tags) ? object.object.properties.tags.filter((entry) => typeof entry === "string") : [];
@@ -3320,6 +5068,8 @@
3320
5068
  const imageDefinitions = buildImageDefinitions(visibleObjects);
3321
5069
  const orbitMarkup = layers.orbits ? renderOrbitLayer(scene, visibleObjectIds, layers.structures) : { back: "", front: "" };
3322
5070
  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("") : "";
5071
+ 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("") : "";
5072
+ const eventMarkup = layers.events ? scene.events.filter((event) => !event.hidden).map((event) => renderSceneEventOverlay(scene, event, visibleObjectIds, theme)).join("") : "";
3323
5073
  const objectMarkup = layers.objects ? visibleObjects.map((object) => renderSceneObject(object, options.selectedObjectId ?? null, theme)).join("") : "";
3324
5074
  const labelMarkup = layers.labels ? visibleLabels.map((label) => renderSceneLabel(scene, label, options.selectedObjectId ?? null)).join("") : "";
3325
5075
  const metadataMarkup = layers.metadata ? `<text class="wo-title" x="56" y="64">${escapeXml(scene.title)}</text>
@@ -3354,6 +5104,10 @@
3354
5104
  .wo-orbit-back { opacity: 0.38; stroke-dasharray: 8 6; }
3355
5105
  .wo-orbit-front { opacity: 0.9; }
3356
5106
  .wo-orbit-band { stroke: ${theme.orbitBand}; stroke-linecap: round; }
5107
+ .wo-relation { stroke: ${theme.relation}; stroke-width: 2; stroke-dasharray: 10 6; }
5108
+ .wo-event-line { stroke: ${theme.accent}; stroke-width: 1.6; stroke-dasharray: 5 5; opacity: 0.72; }
5109
+ .wo-event-node { fill: ${theme.accent}; stroke: ${theme.selected}; stroke-width: 1.4; opacity: 0.92; }
5110
+ .wo-event-label { fill: ${theme.accent}; font-family: ${theme.fontFamily}; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
3357
5111
  .wo-leader { stroke: ${theme.leader}; stroke-width: 1.5; stroke-dasharray: 6 5; }
3358
5112
  .wo-label { fill: ${theme.ink}; font-family: ${theme.fontFamily}; font-weight: 600; letter-spacing: 0.02em; }
3359
5113
  .wo-label-secondary { fill: ${theme.muted}; font-family: ${theme.fontFamily}; font-weight: 500; }
@@ -3387,6 +5141,8 @@
3387
5141
  <g data-worldorbit-world-content="true">
3388
5142
  ${layers.orbits ? `<g data-layer-id="orbits-back">${orbitMarkup.back}</g>` : ""}
3389
5143
  ${layers.guides ? `<g data-layer-id="guides">${leaderMarkup}</g>` : ""}
5144
+ ${layers.relations ? `<g data-layer-id="relations">${relationMarkup}</g>` : ""}
5145
+ ${layers.events ? `<g data-layer-id="events">${eventMarkup}</g>` : ""}
3390
5146
  ${layers.objects ? `<g data-layer-id="objects">${objectMarkup}</g>` : ""}
3391
5147
  ${layers.orbits ? `<g data-layer-id="orbits-front">${orbitMarkup.front}</g>` : ""}
3392
5148
  ${layers.labels ? `<g data-layer-id="labels">${labelMarkup}</g>` : ""}
@@ -3394,6 +5150,20 @@
3394
5150
  </g>
3395
5151
  </g>
3396
5152
  </svg>`;
5153
+ }
5154
+ function renderSceneEventOverlay(scene, event, visibleObjectIds, theme) {
5155
+ const participants = event.objectIds.filter((objectId) => visibleObjectIds.has(objectId)).map((objectId) => scene.objects.find((object) => object.objectId === objectId && !object.hidden)).filter(Boolean);
5156
+ if (participants.length === 0) {
5157
+ return "";
5158
+ }
5159
+ const stroke = event.event.color || theme.accent;
5160
+ const label = event.event.label || event.event.id;
5161
+ 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("");
5162
+ return `<g class="wo-event" data-render-id="${escapeXml(event.renderId)}" data-event-id="${escapeAttribute(event.eventId)}">
5163
+ ${lineMarkup}
5164
+ <circle class="wo-event-node" cx="${event.x}" cy="${event.y}" r="5" fill="${escapeAttribute(stroke)}" />
5165
+ <text class="wo-event-label" x="${event.x}" y="${event.y - 10}" text-anchor="middle" font-size="10">${escapeXml(label)}</text>
5166
+ </g>`;
3397
5167
  }
3398
5168
  function renderOrbitLayer(scene, visibleObjectIds, includeStructures) {
3399
5169
  const backParts = [];
@@ -3430,10 +5200,11 @@
3430
5200
  function renderSceneObject(sceneObject, selectedObjectId, theme) {
3431
5201
  const { object, x, y, radius, visualRadius } = sceneObject;
3432
5202
  const selectionClass = selectedObjectId === sceneObject.objectId ? " wo-object-selected" : "";
5203
+ const kindClass = object.properties.kind ? ` wo-kind-${String(object.properties.kind).toLowerCase().replace(/[^a-z0-9-]/g, "-")}` : "";
3433
5204
  const palette = resolveObjectPalette(sceneObject, theme);
3434
5205
  const imageMarkup = renderObjectImage(sceneObject);
3435
5206
  const outlineMarkup = imageMarkup ? renderObjectBody(object, x, y, radius, palette, { outlineOnly: true }) : "";
3436
- return `<g class="wo-object wo-object-${object.type}${selectionClass}" data-object-id="${escapeXml(sceneObject.objectId)}" data-parent-id="${escapeAttribute(sceneObject.parentId ?? "")}" data-group-id="${escapeAttribute(sceneObject.groupId ?? "")}" data-render-id="${escapeXml(sceneObject.renderId)}" tabindex="0" role="button" aria-label="${escapeXml(`${object.type} ${sceneObject.objectId}`)}">
5207
+ return `<g class="wo-object wo-object-${object.type}${kindClass}${selectionClass}" data-object-id="${escapeXml(sceneObject.objectId)}" data-parent-id="${escapeAttribute(sceneObject.parentId ?? "")}" data-group-id="${escapeAttribute(sceneObject.groupId ?? "")}" data-render-id="${escapeXml(sceneObject.renderId)}" tabindex="0" role="button" aria-label="${escapeXml(`${object.type} ${sceneObject.objectId}`)}">
3437
5208
  <circle class="wo-selection-ring" cx="${x}" cy="${y}" r="${visualRadius + 8}" />
3438
5209
  ${renderAtmosphere(sceneObject, palette)}
3439
5210
  ${renderObjectBody(object, x, y, radius, palette)}
@@ -3467,8 +5238,33 @@
3467
5238
  <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
3468
5239
  case "structure":
3469
5240
  return `<polygon points="${diamondPoints(x, y, radius)}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
3470
- case "phenomenon":
5241
+ case "phenomenon": {
5242
+ const kind = String(object.properties.kind ?? "").toLowerCase().replace(/_/g, "-");
5243
+ if (options.outlineOnly) {
5244
+ if (kind === "black-hole" || kind === "nebula" || kind === "galaxy" || kind === "dwarf-galaxy") {
5245
+ return `<circle cx="${x}" cy="${y}" r="${radius}" fill="transparent" stroke="${palette.stroke}" stroke-width="1.4" />`;
5246
+ }
5247
+ return `<polygon points="${phenomenonPoints(x, y, radius)}" fill="transparent" stroke="${palette.stroke}" stroke-width="1.4" />`;
5248
+ }
5249
+ if (kind === "black-hole") {
5250
+ 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" />
5251
+ <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="2" />`;
5252
+ }
5253
+ if (kind === "galaxy") {
5254
+ return `<ellipse cx="${x}" cy="${y}" rx="${radius * 2.6}" ry="${radius}" fill="${palette.halo ?? "none"}" stroke="none" />
5255
+ <ellipse cx="${x}" cy="${y}" rx="${radius * 1.5}" ry="${radius * 0.42}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.2" />
5256
+ <circle cx="${x}" cy="${y}" r="${radius * 0.28}" fill="${palette.core ?? "#fff"}" stroke="none" />`;
5257
+ }
5258
+ if (kind === "dwarf-galaxy") {
5259
+ return `<ellipse cx="${x}" cy="${y}" rx="${radius * 1.6}" ry="${radius * 0.55}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1" />
5260
+ <circle cx="${x}" cy="${y}" r="${radius * 0.25}" fill="${palette.core ?? "#fff"}" stroke="none" />`;
5261
+ }
5262
+ if (kind === "nebula") {
5263
+ return `<circle cx="${x}" cy="${y}" r="${radius * 2.2}" fill="${palette.halo ?? "none"}" stroke="none" />
5264
+ <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1" />`;
5265
+ }
3471
5266
  return `<polygon points="${phenomenonPoints(x, y, radius)}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
5267
+ }
3472
5268
  }
3473
5269
  }
3474
5270
  function renderAtmosphere(sceneObject, palette) {
@@ -3537,7 +5333,8 @@
3537
5333
  }
3538
5334
  }
3539
5335
  function resolveObjectPalette(sceneObject, theme) {
3540
- const base = basePaletteForType(sceneObject.object.type, theme);
5336
+ const kind = String(sceneObject.object.properties.kind ?? "").toLowerCase().replace(/_/g, "-");
5337
+ const base = basePaletteForType(sceneObject.object.type, kind, theme);
3541
5338
  const customFill = sceneObject.fillColor && isColorLike(sceneObject.fillColor) ? sceneObject.fillColor : base.fill;
3542
5339
  const albedo = numericValue(sceneObject.object.properties.albedo);
3543
5340
  const temperature = numericValue(sceneObject.object.properties.temperature);
@@ -3553,7 +5350,7 @@
3553
5350
  tail: sceneObject.object.type === "comet" ? rgbaString(mixColors(fill, "#ffffff", 0.5) ?? fill, 0.72) : void 0
3554
5351
  };
3555
5352
  }
3556
- function basePaletteForType(type, theme) {
5353
+ function basePaletteForType(type, kind, theme) {
3557
5354
  switch (type) {
3558
5355
  case "star":
3559
5356
  return {
@@ -3575,8 +5372,26 @@
3575
5372
  case "structure":
3576
5373
  return { fill: theme.accentStrong, stroke: "#fff2ea" };
3577
5374
  case "phenomenon":
3578
- return { fill: "#78ffd7", stroke: "#e9fff7" };
5375
+ return kindPhenomenonPalette(kind);
5376
+ }
5377
+ }
5378
+ function kindPhenomenonPalette(kind) {
5379
+ if (kind === "galaxy") {
5380
+ return { fill: "rgba(165,125,255,0.55)", stroke: "rgba(210,185,255,0.75)", halo: "rgba(160,120,255,0.10)", core: "#ede0ff" };
5381
+ }
5382
+ if (kind === "dwarf-galaxy") {
5383
+ return { fill: "rgba(190,165,255,0.45)", stroke: "rgba(220,205,255,0.75)", core: "#ddd0ff" };
5384
+ }
5385
+ if (kind === "black-hole") {
5386
+ return { fill: "#040408", stroke: "#ff6a00", accentRing: "rgba(255,140,20,0.72)" };
5387
+ }
5388
+ if (kind === "nebula") {
5389
+ return { fill: "rgba(105,205,255,0.45)", stroke: "rgba(180,235,255,0.72)", halo: "rgba(100,200,255,0.08)" };
5390
+ }
5391
+ if (kind === "void") {
5392
+ return { fill: "#05080f", stroke: "rgba(130,160,255,0.4)" };
3579
5393
  }
5394
+ return { fill: "#78ffd7", stroke: "#e9fff7" };
3580
5395
  }
3581
5396
  function applyTemperatureAndAlbedo(baseColor, temperature, albedo, type) {
3582
5397
  let nextColor = baseColor;
@@ -3843,11 +5658,11 @@
3843
5658
  });
3844
5659
  }
3845
5660
  return `<figure class="${escapeAttribute3(options.className ?? "worldorbit-block worldorbit-static")}">${renderSceneToSvg(scene, options)}</figure>`;
3846
- } catch (error) {
5661
+ } catch (error2) {
3847
5662
  if (options.strict) {
3848
- throw error;
5663
+ throw error2;
3849
5664
  }
3850
- return renderWorldOrbitError(error instanceof Error ? error.message : String(error));
5665
+ return renderWorldOrbitError(error2 instanceof Error ? error2.message : String(error2));
3851
5666
  }
3852
5667
  }
3853
5668
  function renderWorldOrbitError(message) {