worldorbit 2.5.15 → 2.5.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +1 -1
  2. package/dist/browser/core/dist/index.js +2444 -342
  3. package/dist/browser/editor/dist/index.js +11702 -0
  4. package/dist/browser/markdown/dist/index.js +2207 -392
  5. package/dist/browser/viewer/dist/index.js +2302 -382
  6. package/dist/unpkg/core/dist/index.js +2447 -345
  7. package/dist/unpkg/editor/dist/index.js +11727 -0
  8. package/dist/unpkg/markdown/dist/index.js +2210 -395
  9. package/dist/unpkg/viewer/dist/index.js +2305 -385
  10. package/dist/unpkg/worldorbit-core.min.js +12 -12
  11. package/dist/unpkg/worldorbit-editor.min.js +894 -0
  12. package/dist/unpkg/worldorbit-markdown.min.js +66 -58
  13. package/dist/unpkg/worldorbit-viewer.min.js +76 -68
  14. package/dist/unpkg/worldorbit.js +797 -78
  15. package/dist/unpkg/worldorbit.min.js +80 -72
  16. package/package.json +3 -2
  17. package/packages/core/dist/atlas-edit.js +74 -0
  18. package/packages/core/dist/atlas-validate.js +122 -8
  19. package/packages/core/dist/draft-parse.js +212 -8
  20. package/packages/core/dist/draft.d.ts +5 -2
  21. package/packages/core/dist/draft.js +59 -3
  22. package/packages/core/dist/format.js +63 -1
  23. package/packages/core/dist/normalize.js +1 -0
  24. package/packages/core/dist/scene.js +248 -46
  25. package/packages/core/dist/types.d.ts +41 -2
  26. package/packages/editor/dist/editor.d.ts +2 -0
  27. package/packages/editor/dist/editor.js +3578 -0
  28. package/packages/editor/dist/index.d.ts +2 -0
  29. package/packages/editor/dist/index.js +1 -0
  30. package/packages/editor/dist/types.d.ts +55 -0
  31. package/packages/editor/dist/types.js +1 -0
  32. package/packages/markdown/dist/html.d.ts +3 -0
  33. package/packages/markdown/dist/html.js +57 -0
  34. package/packages/markdown/dist/index.d.ts +4 -0
  35. package/packages/markdown/dist/index.js +3 -0
  36. package/packages/markdown/dist/rehype.d.ts +10 -0
  37. package/packages/markdown/dist/rehype.js +49 -0
  38. package/packages/markdown/dist/remark.d.ts +9 -0
  39. package/packages/markdown/dist/remark.js +28 -0
  40. package/packages/markdown/dist/types.d.ts +11 -0
  41. package/packages/markdown/dist/types.js +1 -0
  42. package/packages/viewer/dist/atlas-state.js +6 -0
  43. package/packages/viewer/dist/atlas-viewer.js +1 -0
  44. package/packages/viewer/dist/render.js +31 -2
  45. package/packages/viewer/dist/theme.js +1 -0
  46. package/packages/viewer/dist/tooltip.js +9 -0
  47. package/packages/viewer/dist/types.d.ts +8 -1
  48. package/packages/viewer/dist/viewer.js +12 -1
@@ -19,8 +19,8 @@ var WorldOrbit = (() => {
19
19
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
20
 
21
21
  // packages/viewer/dist/index.js
22
- var dist_exports = {};
23
- __export(dist_exports, {
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
24
  DEFAULT_VIEWER_STATE: () => DEFAULT_VIEWER_STATE,
25
25
  WORLD_LAYER_ID: () => WORLD_LAYER_ID,
26
26
  clampScale: () => clampScale,
@@ -60,6 +60,8 @@ var WorldOrbit = (() => {
60
60
  var DEFAULT_LAYERS = {
61
61
  background: true,
62
62
  guides: true,
63
+ relations: true,
64
+ events: true,
63
65
  orbits: true,
64
66
  objects: true,
65
67
  labels: true,
@@ -74,6 +76,7 @@ var WorldOrbit = (() => {
74
76
  backgroundGlow: "rgba(240, 180, 100, 0.18)",
75
77
  panel: "rgba(7, 17, 27, 0.9)",
76
78
  panelLine: "rgba(168, 207, 242, 0.18)",
79
+ relation: "rgba(240, 180, 100, 0.42)",
77
80
  orbit: "rgba(163, 209, 255, 0.24)",
78
81
  orbitBand: "rgba(255, 190, 120, 0.28)",
79
82
  guide: "rgba(255, 255, 255, 0.04)",
@@ -96,6 +99,7 @@ var WorldOrbit = (() => {
96
99
  backgroundGlow: "rgba(120, 255, 215, 0.16)",
97
100
  panel: "rgba(7, 20, 30, 0.9)",
98
101
  panelLine: "rgba(120, 255, 215, 0.16)",
102
+ relation: "rgba(156, 231, 255, 0.42)",
99
103
  orbit: "rgba(120, 255, 215, 0.2)",
100
104
  orbitBand: "rgba(137, 185, 255, 0.24)",
101
105
  guide: "rgba(255, 255, 255, 0.035)",
@@ -118,6 +122,7 @@ var WorldOrbit = (() => {
118
122
  backgroundGlow: "rgba(255, 127, 95, 0.18)",
119
123
  panel: "rgba(24, 9, 13, 0.9)",
120
124
  panelLine: "rgba(255, 166, 149, 0.16)",
125
+ relation: "rgba(255, 178, 125, 0.42)",
121
126
  orbit: "rgba(255, 188, 164, 0.22)",
122
127
  orbitBand: "rgba(255, 214, 139, 0.24)",
123
128
  guide: "rgba(255, 255, 255, 0.03)",
@@ -208,12 +213,14 @@ var WorldOrbit = (() => {
208
213
  return {
209
214
  version: "2.0",
210
215
  viewpointId,
216
+ activeEventId: renderOptions.activeEventId ?? null,
211
217
  viewerState: { ...viewerState },
212
218
  renderOptions: {
213
219
  preset: renderOptions.preset,
214
220
  projection: renderOptions.projection,
215
221
  layers: renderOptions.layers ? { ...renderOptions.layers } : void 0,
216
- scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0
222
+ scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0,
223
+ activeEventId: renderOptions.activeEventId ?? null
217
224
  },
218
225
  filter: normalizeViewerFilter(filter)
219
226
  };
@@ -226,6 +233,7 @@ var WorldOrbit = (() => {
226
233
  return {
227
234
  version: "2.0",
228
235
  viewpointId: raw.viewpointId ?? null,
236
+ activeEventId: raw.activeEventId ?? raw.renderOptions?.activeEventId ?? null,
229
237
  viewerState: {
230
238
  scale: raw.viewerState?.scale ?? 1,
231
239
  rotationDeg: raw.viewerState?.rotationDeg ?? 0,
@@ -237,7 +245,8 @@ var WorldOrbit = (() => {
237
245
  preset: raw.renderOptions?.preset,
238
246
  projection: raw.renderOptions?.projection,
239
247
  layers: raw.renderOptions?.layers ? { ...raw.renderOptions.layers } : void 0,
240
- scaleModel: raw.renderOptions?.scaleModel ? { ...raw.renderOptions.scaleModel } : void 0
248
+ scaleModel: raw.renderOptions?.scaleModel ? { ...raw.renderOptions.scaleModel } : void 0,
249
+ activeEventId: raw.activeEventId ?? raw.renderOptions?.activeEventId ?? null
241
250
  },
242
251
  filter: normalizeViewerFilter(raw.filter ?? null)
243
252
  };
@@ -253,7 +262,8 @@ var WorldOrbit = (() => {
253
262
  renderOptions: {
254
263
  ...atlasState.renderOptions,
255
264
  layers: atlasState.renderOptions.layers ? { ...atlasState.renderOptions.layers } : void 0,
256
- scaleModel: atlasState.renderOptions.scaleModel ? { ...atlasState.renderOptions.scaleModel } : void 0
265
+ scaleModel: atlasState.renderOptions.scaleModel ? { ...atlasState.renderOptions.scaleModel } : void 0,
266
+ activeEventId: atlasState.renderOptions.activeEventId ?? null
257
267
  },
258
268
  filter: atlasState.filter ? { ...atlasState.filter } : null
259
269
  }
@@ -270,6 +280,8 @@ var WorldOrbit = (() => {
270
280
  return {
271
281
  background: viewpoint.layers.background,
272
282
  guides: viewpoint.layers.guides,
283
+ relations: viewpoint.layers.relations,
284
+ events: viewpoint.layers.events,
273
285
  orbits: viewpoint.layers["orbits-front"] === void 0 && viewpoint.layers["orbits-back"] === void 0 ? void 0 : viewpoint.layers["orbits-front"] !== false || viewpoint.layers["orbits-back"] !== false,
274
286
  objects: viewpoint.layers.objects,
275
287
  labels: viewpoint.layers.labels,
@@ -307,7 +319,11 @@ var WorldOrbit = (() => {
307
319
  return false;
308
320
  }
309
321
  if (filter.groupIds?.length && (!object.groupId || !filter.groupIds.includes(object.groupId))) {
310
- return false;
322
+ const hasSemanticMatch = object.semanticGroupIds.length > 0 && filter.groupIds.some((groupId) => object.semanticGroupIds.includes(groupId));
323
+ const hasLegacyMatch = Boolean(object.groupId && filter.groupIds.includes(object.groupId));
324
+ if (!hasSemanticMatch && !hasLegacyMatch) {
325
+ return false;
326
+ }
311
327
  }
312
328
  if (filter.tags?.length) {
313
329
  const objectTags = Array.isArray(object.object.properties.tags) ? object.object.properties.tags.filter((entry) => typeof entry === "string") : [];
@@ -674,13 +690,13 @@ var WorldOrbit = (() => {
674
690
  function unitFamilyAllowsUnit(family, unit) {
675
691
  switch (family) {
676
692
  case "distance":
677
- return unit === null || ["au", "km", "re", "sol"].includes(unit);
693
+ return unit === null || ["au", "km", "m", "ly", "pc", "kpc", "re", "sol"].includes(unit);
678
694
  case "radius":
679
- return unit === null || ["km", "re", "sol"].includes(unit);
695
+ return unit === null || ["km", "m", "re", "rj", "sol"].includes(unit);
680
696
  case "mass":
681
- return unit === null || ["me", "sol"].includes(unit);
697
+ return unit === null || ["me", "mj", "sol"].includes(unit);
682
698
  case "duration":
683
- return unit === null || ["h", "d", "y"].includes(unit);
699
+ return unit === null || ["s", "min", "h", "d", "y", "ky", "my", "gy"].includes(unit);
684
700
  case "angle":
685
701
  return unit === null || unit === "deg";
686
702
  case "generic":
@@ -884,7 +900,7 @@ var WorldOrbit = (() => {
884
900
  }
885
901
 
886
902
  // packages/core/dist/normalize.js
887
- var UNIT_PATTERN = /^(-?\d+(?:\.\d+)?)(au|km|re|sol|me|d|y|h|deg)?$/;
903
+ 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)?$/;
888
904
  var BOOLEAN_VALUES = /* @__PURE__ */ new Map([
889
905
  ["true", true],
890
906
  ["false", false],
@@ -909,7 +925,11 @@ var WorldOrbit = (() => {
909
925
  return {
910
926
  format: "worldorbit",
911
927
  version: "1.0",
928
+ schemaVersion: "1.0",
912
929
  system,
930
+ groups: [],
931
+ relations: [],
932
+ events: [],
913
933
  objects
914
934
  };
915
935
  }
@@ -919,13 +939,17 @@ var WorldOrbit = (() => {
919
939
  const fieldMap = collectFields(mergedFields);
920
940
  const placement = extractPlacement(node.objectType, fieldMap);
921
941
  const properties = normalizeProperties(fieldMap);
922
- const info = normalizeInfo(node.infoEntries);
942
+ const info2 = normalizeInfo(node.infoEntries);
923
943
  if (node.objectType === "system") {
924
944
  return {
925
945
  type: "system",
926
946
  id: node.name,
947
+ title: typeof properties.title === "string" ? properties.title : null,
948
+ description: null,
949
+ epoch: null,
950
+ referencePlane: null,
927
951
  properties,
928
- info
952
+ info: info2
929
953
  };
930
954
  }
931
955
  return {
@@ -933,7 +957,7 @@ var WorldOrbit = (() => {
933
957
  id: node.name,
934
958
  properties,
935
959
  placement,
936
- info
960
+ info: info2
937
961
  };
938
962
  }
939
963
  function validateFieldCompatibility(objectType, fields) {
@@ -1063,14 +1087,14 @@ var WorldOrbit = (() => {
1063
1087
  }
1064
1088
  }
1065
1089
  function normalizeInfo(entries) {
1066
- const info = {};
1090
+ const info2 = {};
1067
1091
  for (const entry of entries) {
1068
- if (entry.key in info) {
1092
+ if (entry.key in info2) {
1069
1093
  throw WorldOrbitError.fromLocation(`Duplicate info key "${entry.key}"`, entry.location);
1070
1094
  }
1071
- info[entry.key] = entry.value;
1095
+ info2[entry.key] = entry.value;
1072
1096
  }
1073
- return info;
1097
+ return info2;
1074
1098
  }
1075
1099
  function parseAtReference(target, location) {
1076
1100
  if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
@@ -1241,37 +1265,41 @@ var WorldOrbit = (() => {
1241
1265
  }
1242
1266
 
1243
1267
  // packages/core/dist/diagnostics.js
1244
- function diagnosticFromError(error, source, code = `${source}.failed`) {
1245
- if (error instanceof WorldOrbitError) {
1268
+ function diagnosticFromError(error2, source, code = `${source}.failed`) {
1269
+ if (error2 instanceof WorldOrbitError) {
1246
1270
  return {
1247
1271
  code,
1248
1272
  severity: "error",
1249
1273
  source,
1250
- message: error.message,
1251
- line: error.line,
1252
- column: error.column
1274
+ message: error2.message,
1275
+ line: error2.line,
1276
+ column: error2.column
1253
1277
  };
1254
1278
  }
1255
- if (error instanceof Error) {
1279
+ if (error2 instanceof Error) {
1256
1280
  return {
1257
1281
  code,
1258
1282
  severity: "error",
1259
1283
  source,
1260
- message: error.message
1284
+ message: error2.message
1261
1285
  };
1262
1286
  }
1263
1287
  return {
1264
1288
  code,
1265
1289
  severity: "error",
1266
1290
  source,
1267
- message: String(error)
1291
+ message: String(error2)
1268
1292
  };
1269
1293
  }
1270
1294
 
1271
1295
  // packages/core/dist/scene.js
1272
1296
  var AU_IN_KM = 1495978707e-1;
1273
1297
  var EARTH_RADIUS_IN_KM = 6371;
1298
+ var JUPITER_RADIUS_IN_KM = 71492;
1274
1299
  var SOLAR_RADIUS_IN_KM = 695700;
1300
+ var LY_IN_AU = 63241.077;
1301
+ var PC_IN_AU = 206264.806;
1302
+ var KPC_IN_AU = 206264806;
1275
1303
  var ISO_FLATTENING = 0.68;
1276
1304
  var MIN_ISO_MINOR_SCALE = 0.2;
1277
1305
  var ARC_SAMPLE_COUNT = 28;
@@ -1285,8 +1313,10 @@ var WorldOrbit = (() => {
1285
1313
  const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
1286
1314
  const spacingFactor = layoutPresetSpacing(layoutPreset);
1287
1315
  const systemId = document2.system?.id ?? null;
1288
- const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
1289
- const relationships = buildSceneRelationships(document2.objects, objectMap);
1316
+ const activeEventId = options.activeEventId ?? null;
1317
+ const effectiveObjects = createEffectiveObjects(document2.objects, document2.events ?? [], activeEventId);
1318
+ const objectMap = new Map(effectiveObjects.map((object) => [object.id, object]));
1319
+ const relationships = buildSceneRelationships(effectiveObjects, objectMap);
1290
1320
  const positions = /* @__PURE__ */ new Map();
1291
1321
  const orbitDrafts = [];
1292
1322
  const leaderDrafts = [];
@@ -1295,7 +1325,7 @@ var WorldOrbit = (() => {
1295
1325
  const atObjects = [];
1296
1326
  const surfaceChildren = /* @__PURE__ */ new Map();
1297
1327
  const orbitChildren = /* @__PURE__ */ new Map();
1298
- for (const object of document2.objects) {
1328
+ for (const object of effectiveObjects) {
1299
1329
  const placement = object.placement;
1300
1330
  if (!placement) {
1301
1331
  rootObjects.push(object);
@@ -1390,11 +1420,14 @@ var WorldOrbit = (() => {
1390
1420
  const objects = [...positions.values()].map((position) => createSceneObject(position, scaleModel, relationships));
1391
1421
  const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
1392
1422
  const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
1393
- const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
1394
- const layers = createSceneLayers(orbitVisuals, leaders, objects, labels);
1395
- const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
1423
+ const labels = createSceneLabels(objects, width, height, scaleModel.labelMultiplier);
1424
+ const relations = createSceneRelations(document2, objects);
1425
+ const events = createSceneEvents(document2.events ?? [], objects, activeEventId);
1426
+ const layers = createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels);
1427
+ const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, scaleModel.labelMultiplier);
1428
+ const semanticGroups = createSceneSemanticGroups(document2, objects);
1396
1429
  const viewpoints = createSceneViewpoints(document2, projection, frame.preset, relationships, objectMap);
1397
- const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
1430
+ const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, scaleModel.labelMultiplier);
1398
1431
  return {
1399
1432
  width,
1400
1433
  height,
@@ -1402,7 +1435,7 @@ var WorldOrbit = (() => {
1402
1435
  renderPreset: frame.preset,
1403
1436
  projection,
1404
1437
  scaleModel,
1405
- title: String(document2.system?.properties.title ?? document2.system?.id ?? "WorldOrbit") || "WorldOrbit",
1438
+ title: String(document2.system?.title ?? document2.system?.properties.title ?? document2.system?.id ?? "WorldOrbit") || "WorldOrbit",
1406
1439
  subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
1407
1440
  systemId,
1408
1441
  viewMode: projection,
@@ -1418,9 +1451,13 @@ var WorldOrbit = (() => {
1418
1451
  contentBounds,
1419
1452
  layers,
1420
1453
  groups,
1454
+ semanticGroups,
1421
1455
  viewpoints,
1456
+ events,
1457
+ activeEventId,
1422
1458
  objects,
1423
1459
  orbitVisuals,
1460
+ relations,
1424
1461
  leaders,
1425
1462
  labels
1426
1463
  };
@@ -1436,6 +1473,35 @@ var WorldOrbit = (() => {
1436
1473
  y: center.y + dx * sin + dy * cos
1437
1474
  };
1438
1475
  }
1476
+ function createEffectiveObjects(objects, events, activeEventId) {
1477
+ const cloned = objects.map((object) => structuredClone(object));
1478
+ if (!activeEventId) {
1479
+ return cloned;
1480
+ }
1481
+ const activeEvent = events.find((event) => event.id === activeEventId);
1482
+ if (!activeEvent) {
1483
+ return cloned;
1484
+ }
1485
+ const objectMap = new Map(cloned.map((object) => [object.id, object]));
1486
+ for (const pose of activeEvent.positions) {
1487
+ const object = objectMap.get(pose.objectId);
1488
+ if (!object) {
1489
+ continue;
1490
+ }
1491
+ object.placement = pose.placement ? structuredClone(pose.placement) : null;
1492
+ if (pose.inner) {
1493
+ object.properties.inner = { ...pose.inner };
1494
+ } else {
1495
+ delete object.properties.inner;
1496
+ }
1497
+ if (pose.outer) {
1498
+ object.properties.outer = { ...pose.outer };
1499
+ } else {
1500
+ delete object.properties.outer;
1501
+ }
1502
+ }
1503
+ return cloned;
1504
+ }
1439
1505
  function resolveLayoutPreset(document2) {
1440
1506
  const rawScale = String(document2.system?.properties.scale ?? "balanced").toLowerCase();
1441
1507
  switch (rawScale) {
@@ -1530,6 +1596,7 @@ var WorldOrbit = (() => {
1530
1596
  }
1531
1597
  function createSceneObject(position, scaleModel, relationships) {
1532
1598
  const { object, x, y, radius, sortKey, anchorX, anchorY } = position;
1599
+ const renderPriority = object.renderHints?.renderPriority ?? 0;
1533
1600
  return {
1534
1601
  renderId: createRenderId(object.id),
1535
1602
  objectId: object.id,
@@ -1538,11 +1605,12 @@ var WorldOrbit = (() => {
1538
1605
  ancestorIds: relationships.ancestorIds.get(object.id) ?? [],
1539
1606
  childIds: relationships.childIds.get(object.id) ?? [],
1540
1607
  groupId: relationships.groupIds.get(object.id) ?? null,
1608
+ semanticGroupIds: [...object.groups ?? []],
1541
1609
  x,
1542
1610
  y,
1543
1611
  radius,
1544
1612
  visualRadius: visualExtentForObject(object, radius, scaleModel),
1545
- sortKey,
1613
+ sortKey: sortKey + renderPriority * 1e-3,
1546
1614
  anchorX,
1547
1615
  anchorY,
1548
1616
  label: object.id,
@@ -1559,6 +1627,7 @@ var WorldOrbit = (() => {
1559
1627
  object: draft.object,
1560
1628
  parentId: draft.parentId,
1561
1629
  groupId,
1630
+ semanticGroupIds: [...draft.object.groups ?? []],
1562
1631
  kind: draft.kind,
1563
1632
  cx: draft.cx,
1564
1633
  cy: draft.cy,
@@ -1570,7 +1639,7 @@ var WorldOrbit = (() => {
1570
1639
  bandThickness: draft.bandThickness,
1571
1640
  frontArcPath: draft.frontArcPath,
1572
1641
  backArcPath: draft.backArcPath,
1573
- hidden: draft.object.properties.hidden === true
1642
+ hidden: draft.object.properties.hidden === true || draft.object.renderHints?.renderOrbit === false
1574
1643
  };
1575
1644
  }
1576
1645
  function createLeaderLine(draft) {
@@ -1579,6 +1648,7 @@ var WorldOrbit = (() => {
1579
1648
  objectId: draft.object.id,
1580
1649
  object: draft.object,
1581
1650
  groupId: draft.groupId,
1651
+ semanticGroupIds: [...draft.object.groups ?? []],
1582
1652
  x1: draft.x1,
1583
1653
  y1: draft.y1,
1584
1654
  x2: draft.x2,
@@ -1587,42 +1657,144 @@ var WorldOrbit = (() => {
1587
1657
  hidden: draft.object.properties.hidden === true
1588
1658
  };
1589
1659
  }
1590
- function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1660
+ function createSceneLabels(objects, sceneWidth, sceneHeight, labelMultiplier) {
1591
1661
  const labels = [];
1592
1662
  const occupied = [];
1593
- const visibleObjects = [...objects].filter((object) => !object.hidden).sort((left, right) => left.sortKey - right.sortKey);
1663
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1664
+ const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort(compareLabelPlacementOrder);
1594
1665
  for (const object of visibleObjects) {
1595
- const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1596
- const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
1597
- let labelY = object.y + direction * (object.radius + 18 * labelMultiplier);
1598
- let secondaryY = labelY + direction * (16 * labelMultiplier);
1599
- let bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1600
- let attempts = 0;
1601
- while (occupied.some((entry) => rectsOverlap(entry, bounds)) && attempts < 10) {
1602
- labelY += direction * 14 * labelMultiplier;
1603
- secondaryY += direction * 14 * labelMultiplier;
1604
- bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1605
- attempts += 1;
1606
- }
1607
- occupied.push(bounds);
1666
+ const placement = selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) ?? createLabelPlacement(object, defaultVerticalDirection(object, objectMap.get(object.parentId ?? "") ?? null, sceneHeight), 0, labelMultiplier);
1667
+ occupied.push(createLabelRect(object, placement, labelMultiplier));
1608
1668
  labels.push({
1609
1669
  renderId: `${object.renderId}-label`,
1610
1670
  objectId: object.objectId,
1611
1671
  object: object.object,
1612
1672
  groupId: object.groupId,
1673
+ semanticGroupIds: [...object.semanticGroupIds],
1613
1674
  label: object.label,
1614
1675
  secondaryLabel: object.secondaryLabel,
1615
- x: object.x,
1616
- y: labelY,
1617
- secondaryY,
1618
- textAnchor: "middle",
1619
- direction: direction < 0 ? "above" : "below",
1676
+ x: placement.x,
1677
+ y: placement.labelY,
1678
+ secondaryY: placement.secondaryY,
1679
+ textAnchor: placement.textAnchor,
1680
+ direction: placement.direction,
1620
1681
  hidden: object.hidden
1621
1682
  });
1622
1683
  }
1623
1684
  return labels;
1624
1685
  }
1625
- function createSceneLayers(orbitVisuals, leaders, objects, labels) {
1686
+ function compareLabelPlacementOrder(left, right) {
1687
+ const priorityDiff = labelPlacementPriority(left) - labelPlacementPriority(right);
1688
+ if (priorityDiff !== 0) {
1689
+ return priorityDiff;
1690
+ }
1691
+ const renderPriorityDiff = (right.object.renderHints?.renderPriority ?? 0) - (left.object.renderHints?.renderPriority ?? 0);
1692
+ if (renderPriorityDiff !== 0) {
1693
+ return renderPriorityDiff;
1694
+ }
1695
+ return left.sortKey - right.sortKey;
1696
+ }
1697
+ function labelPlacementPriority(object) {
1698
+ switch (object.object.type) {
1699
+ case "star":
1700
+ return 0;
1701
+ case "planet":
1702
+ return 1;
1703
+ case "moon":
1704
+ return 2;
1705
+ case "belt":
1706
+ case "ring":
1707
+ return 3;
1708
+ case "asteroid":
1709
+ case "comet":
1710
+ return 4;
1711
+ case "structure":
1712
+ case "phenomenon":
1713
+ return 5;
1714
+ }
1715
+ }
1716
+ function selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) {
1717
+ for (const direction of preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight)) {
1718
+ const maxAttempts = direction === "left" || direction === "right" ? 4 : 6;
1719
+ for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
1720
+ const placement = createLabelPlacement(object, direction, attempt, labelMultiplier);
1721
+ const rect = createLabelRect(object, placement, labelMultiplier);
1722
+ if (!occupied.some((entry) => rectsOverlap(entry, rect))) {
1723
+ return placement;
1724
+ }
1725
+ }
1726
+ }
1727
+ return null;
1728
+ }
1729
+ function preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight) {
1730
+ const parent = object.parentId ? objectMap.get(object.parentId) ?? null : null;
1731
+ const vertical = defaultVerticalDirection(object, parent, sceneHeight);
1732
+ const oppositeVertical = vertical === "below" ? "above" : "below";
1733
+ const horizontal = defaultHorizontalDirection(object, parent, sceneWidth);
1734
+ const oppositeHorizontal = horizontal === "right" ? "left" : "right";
1735
+ 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";
1736
+ return preferHorizontal ? [horizontal, vertical, oppositeHorizontal, oppositeVertical] : [vertical, horizontal, oppositeVertical, oppositeHorizontal];
1737
+ }
1738
+ function defaultVerticalDirection(object, parent, sceneHeight) {
1739
+ if (parent && Math.abs(object.y - parent.y) > 6) {
1740
+ return object.y >= parent.y ? "below" : "above";
1741
+ }
1742
+ return object.y > sceneHeight * 0.62 ? "above" : "below";
1743
+ }
1744
+ function defaultHorizontalDirection(object, parent, sceneWidth) {
1745
+ if (parent && Math.abs(object.x - parent.x) > 6) {
1746
+ return object.x >= parent.x ? "right" : "left";
1747
+ }
1748
+ return object.x >= sceneWidth / 2 ? "right" : "left";
1749
+ }
1750
+ function createLabelPlacement(object, direction, attempt, labelMultiplier) {
1751
+ const step = 14 * labelMultiplier;
1752
+ switch (direction) {
1753
+ case "above": {
1754
+ const labelY = object.y - (object.radius + 18 * labelMultiplier + attempt * step);
1755
+ return {
1756
+ x: object.x,
1757
+ labelY,
1758
+ secondaryY: labelY - 16 * labelMultiplier,
1759
+ textAnchor: "middle",
1760
+ direction
1761
+ };
1762
+ }
1763
+ case "below": {
1764
+ const labelY = object.y + object.radius + 18 * labelMultiplier + attempt * step;
1765
+ return {
1766
+ x: object.x,
1767
+ labelY,
1768
+ secondaryY: labelY + 16 * labelMultiplier,
1769
+ textAnchor: "middle",
1770
+ direction
1771
+ };
1772
+ }
1773
+ case "left": {
1774
+ const x = object.x - (object.visualRadius + 16 * labelMultiplier + attempt * step);
1775
+ const labelY = object.y - 4 * labelMultiplier;
1776
+ return {
1777
+ x,
1778
+ labelY,
1779
+ secondaryY: labelY + 16 * labelMultiplier,
1780
+ textAnchor: "end",
1781
+ direction
1782
+ };
1783
+ }
1784
+ case "right": {
1785
+ const x = object.x + object.visualRadius + 16 * labelMultiplier + attempt * step;
1786
+ const labelY = object.y - 4 * labelMultiplier;
1787
+ return {
1788
+ x,
1789
+ labelY,
1790
+ secondaryY: labelY + 16 * labelMultiplier,
1791
+ textAnchor: "start",
1792
+ direction
1793
+ };
1794
+ }
1795
+ }
1796
+ }
1797
+ function createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels) {
1626
1798
  const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1627
1799
  const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1628
1800
  return [
@@ -1633,6 +1805,14 @@ var WorldOrbit = (() => {
1633
1805
  },
1634
1806
  { id: "orbits-back", renderIds: backOrbitIds },
1635
1807
  { id: "orbits-front", renderIds: frontOrbitIds },
1808
+ {
1809
+ id: "relations",
1810
+ renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId)
1811
+ },
1812
+ {
1813
+ id: "events",
1814
+ renderIds: events.filter((event) => !event.hidden).map((event) => event.renderId)
1815
+ },
1636
1816
  {
1637
1817
  id: "objects",
1638
1818
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
@@ -1644,7 +1824,7 @@ var WorldOrbit = (() => {
1644
1824
  { id: "metadata", renderIds: ["wo-title", "wo-subtitle", "wo-meta"] }
1645
1825
  ];
1646
1826
  }
1647
- function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships) {
1827
+ function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, labelMultiplier) {
1648
1828
  const groups = /* @__PURE__ */ new Map();
1649
1829
  const ensureGroup = (groupId) => {
1650
1830
  if (!groupId) {
@@ -1693,10 +1873,63 @@ var WorldOrbit = (() => {
1693
1873
  }
1694
1874
  }
1695
1875
  for (const group of groups.values()) {
1696
- group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels);
1876
+ group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier);
1697
1877
  }
1698
1878
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1699
1879
  }
1880
+ function createSceneSemanticGroups(document2, objects) {
1881
+ return [...document2.groups].map((group) => ({
1882
+ id: group.id,
1883
+ label: group.label,
1884
+ summary: group.summary,
1885
+ color: group.color,
1886
+ tags: [...group.tags],
1887
+ hidden: group.hidden,
1888
+ objectIds: objects.filter((object) => !object.hidden && object.semanticGroupIds.includes(group.id)).map((object) => object.objectId)
1889
+ })).sort((left, right) => left.label.localeCompare(right.label));
1890
+ }
1891
+ function createSceneRelations(document2, objects) {
1892
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1893
+ return document2.relations.map((relation) => {
1894
+ const from = objectMap.get(relation.from);
1895
+ const to = objectMap.get(relation.to);
1896
+ return {
1897
+ renderId: `${createRenderId(relation.id)}-relation`,
1898
+ relationId: relation.id,
1899
+ relation,
1900
+ fromObjectId: relation.from,
1901
+ toObjectId: relation.to,
1902
+ x1: from?.x ?? 0,
1903
+ y1: from?.y ?? 0,
1904
+ x2: to?.x ?? 0,
1905
+ y2: to?.y ?? 0,
1906
+ hidden: relation.hidden || !from || !to || from.hidden || to.hidden
1907
+ };
1908
+ }).sort((left, right) => left.relation.id.localeCompare(right.relation.id));
1909
+ }
1910
+ function createSceneEvents(events, objects, activeEventId) {
1911
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1912
+ return events.map((event) => {
1913
+ const objectIds = [.../* @__PURE__ */ new Set([
1914
+ ...event.targetObjectId ? [event.targetObjectId] : [],
1915
+ ...event.participantObjectIds
1916
+ ])];
1917
+ const positions = objectIds.map((objectId) => objectMap.get(objectId)).filter(Boolean);
1918
+ const centroidX = positions.length > 0 ? positions.reduce((sum, object) => sum + object.x, 0) / positions.length : 0;
1919
+ const centroidY = positions.length > 0 ? positions.reduce((sum, object) => sum + object.y, 0) / positions.length : 0;
1920
+ return {
1921
+ renderId: `${createRenderId(event.id)}-event`,
1922
+ eventId: event.id,
1923
+ event,
1924
+ objectIds,
1925
+ participantIds: [...event.participantObjectIds],
1926
+ targetObjectId: event.targetObjectId,
1927
+ x: centroidX,
1928
+ y: centroidY,
1929
+ hidden: event.hidden || positions.length === 0 || positions.every((object) => object.hidden) || activeEventId !== null && event.id !== activeEventId
1930
+ };
1931
+ }).sort((left, right) => left.event.id.localeCompare(right.event.id));
1932
+ }
1700
1933
  function createSceneViewpoints(document2, projection, preset, relationships, objectMap) {
1701
1934
  const generatedOverview = createGeneratedOverviewViewpoint(document2, projection, preset);
1702
1935
  const drafts = /* @__PURE__ */ new Map();
@@ -1714,7 +1947,7 @@ var WorldOrbit = (() => {
1714
1947
  }
1715
1948
  const field = fieldParts.join(".").toLowerCase();
1716
1949
  const draft = drafts.get(id) ?? { id };
1717
- applyViewpointField(draft, field, value, projection, preset, relationships, objectMap);
1950
+ applyViewpointField(draft, field, value, document2, projection, preset, relationships, objectMap);
1718
1951
  drafts.set(id, draft);
1719
1952
  }
1720
1953
  const viewpoints = [...drafts.values()].map((draft) => finalizeViewpointDraft(draft, projection, preset, objectMap)).filter(Boolean);
@@ -1742,13 +1975,15 @@ var WorldOrbit = (() => {
1742
1975
  });
1743
1976
  }
1744
1977
  function createGeneratedOverviewViewpoint(document2, projection, preset) {
1745
- const label = document2.system?.properties.title ? `${String(document2.system.properties.title)} Overview` : "Overview";
1978
+ const title = document2.system?.title ?? document2.system?.properties.title;
1979
+ const label = title ? `${String(title)} Overview` : "Overview";
1746
1980
  return {
1747
1981
  id: "overview",
1748
1982
  label,
1749
1983
  summary: "Fit the whole system with the current atlas defaults.",
1750
1984
  objectId: null,
1751
1985
  selectedObjectId: null,
1986
+ eventIds: [],
1752
1987
  projection,
1753
1988
  preset,
1754
1989
  rotationDeg: 0,
@@ -1758,7 +1993,7 @@ var WorldOrbit = (() => {
1758
1993
  generated: true
1759
1994
  };
1760
1995
  }
1761
- function applyViewpointField(draft, field, value, projection, preset, relationships, objectMap) {
1996
+ function applyViewpointField(draft, field, value, document2, projection, preset, relationships, objectMap) {
1762
1997
  const normalizedValue = value.trim();
1763
1998
  switch (field) {
1764
1999
  case "label":
@@ -1785,6 +2020,9 @@ var WorldOrbit = (() => {
1785
2020
  draft.select = normalizedValue;
1786
2021
  }
1787
2022
  return;
2023
+ case "events":
2024
+ draft.eventIds = splitListValue(normalizedValue);
2025
+ return;
1788
2026
  case "projection":
1789
2027
  case "view":
1790
2028
  draft.projection = parseViewProjection(normalizedValue) ?? projection;
@@ -1825,7 +2063,7 @@ var WorldOrbit = (() => {
1825
2063
  case "groups":
1826
2064
  draft.filter = {
1827
2065
  ...draft.filter ?? createEmptyViewpointFilter(),
1828
- groupIds: parseViewpointGroups(normalizedValue, relationships, objectMap)
2066
+ groupIds: parseViewpointGroups(normalizedValue, document2, relationships, objectMap)
1829
2067
  };
1830
2068
  return;
1831
2069
  }
@@ -1841,6 +2079,7 @@ var WorldOrbit = (() => {
1841
2079
  summary: draft.summary?.trim() || createViewpointSummary(label, objectId, filter),
1842
2080
  objectId,
1843
2081
  selectedObjectId,
2082
+ eventIds: [...new Set(draft.eventIds ?? [])],
1844
2083
  projection: draft.projection ?? projection,
1845
2084
  preset: draft.preset ?? preset,
1846
2085
  rotationDeg: draft.rotationDeg ?? 0,
@@ -1898,7 +2137,7 @@ var WorldOrbit = (() => {
1898
2137
  next["orbits-front"] = enabled;
1899
2138
  continue;
1900
2139
  }
1901
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
2140
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "events" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1902
2141
  next[rawLayer] = enabled;
1903
2142
  }
1904
2143
  }
@@ -1907,8 +2146,11 @@ var WorldOrbit = (() => {
1907
2146
  function parseViewpointObjectTypes(value) {
1908
2147
  return splitListValue(value).filter((entry) => entry === "star" || entry === "planet" || entry === "moon" || entry === "belt" || entry === "asteroid" || entry === "comet" || entry === "ring" || entry === "structure" || entry === "phenomenon");
1909
2148
  }
1910
- function parseViewpointGroups(value, relationships, objectMap) {
2149
+ function parseViewpointGroups(value, document2, relationships, objectMap) {
1911
2150
  return splitListValue(value).map((entry) => {
2151
+ if (document2.schemaVersion === "2.1" || document2.groups.some((group) => group.id === entry)) {
2152
+ return entry;
2153
+ }
1912
2154
  if (entry.startsWith("wo-") && entry.endsWith("-group")) {
1913
2155
  return entry;
1914
2156
  }
@@ -1943,7 +2185,7 @@ var WorldOrbit = (() => {
1943
2185
  }
1944
2186
  return parts.join(" - ");
1945
2187
  }
1946
- function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels) {
2188
+ function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, labelMultiplier) {
1947
2189
  let minX = Number.POSITIVE_INFINITY;
1948
2190
  let minY = Number.POSITIVE_INFINITY;
1949
2191
  let maxX = Number.NEGATIVE_INFINITY;
@@ -1973,7 +2215,7 @@ var WorldOrbit = (() => {
1973
2215
  for (const label of labels) {
1974
2216
  if (label.hidden)
1975
2217
  continue;
1976
- includeLabelBounds(label, include);
2218
+ includeLabelBounds(label, include, labelMultiplier);
1977
2219
  }
1978
2220
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
1979
2221
  return createBounds(0, 0, width, height);
@@ -2011,13 +2253,10 @@ var WorldOrbit = (() => {
2011
2253
  include(object.x - object.visualRadius - 24, object.y - object.visualRadius - 16);
2012
2254
  include(object.x + object.visualRadius + 24, object.y + object.visualRadius + 36);
2013
2255
  }
2014
- function includeLabelBounds(label, include) {
2015
- const labelScale = 1;
2016
- const labelHalfWidth = estimateLabelHalfWidthFromText(label.label, label.secondaryLabel, labelScale);
2017
- include(label.x - labelHalfWidth, label.y - 18);
2018
- include(label.x + labelHalfWidth, label.y + 8);
2019
- include(label.x - labelHalfWidth, label.secondaryY - 14);
2020
- include(label.x + labelHalfWidth, label.secondaryY + 8);
2256
+ function includeLabelBounds(label, include, labelMultiplier) {
2257
+ const bounds = createLabelRectFromText(label.x, label.y, label.secondaryY, label.textAnchor, label.direction, label.label, label.secondaryLabel, labelMultiplier);
2258
+ include(bounds.left, bounds.top);
2259
+ include(bounds.right, bounds.bottom);
2021
2260
  }
2022
2261
  function placeObject(object, x, y, depth, positions, orbitDrafts, leaderDrafts, context) {
2023
2262
  if (positions.has(object.id)) {
@@ -2039,8 +2278,9 @@ var WorldOrbit = (() => {
2039
2278
  }
2040
2279
  const orbiting = [...context.orbitChildren.get(object.id) ?? []].sort(compareOrbiting);
2041
2280
  const orbitMetricContext = computeOrbitMetricContext(orbiting, parent.radius, context.spacingFactor, context.scaleModel);
2281
+ const orbitRadiiPx = resolveOrbitRadiiPx(orbiting, orbitMetricContext);
2042
2282
  orbiting.forEach((child, index) => {
2043
- const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, context);
2283
+ const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, orbitRadiiPx[index] ?? orbitMetricContext.innerPx, context);
2044
2284
  orbitDrafts.push({
2045
2285
  object: child,
2046
2286
  parentId: object.id,
@@ -2114,7 +2354,8 @@ var WorldOrbit = (() => {
2114
2354
  metricSpread: 0,
2115
2355
  innerPx,
2116
2356
  stepPx,
2117
- pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx)
2357
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
2358
+ minimumGapPx: stepPx * 0.42
2118
2359
  };
2119
2360
  }
2120
2361
  const minMetric = Math.min(...presentMetrics);
@@ -2127,10 +2368,11 @@ var WorldOrbit = (() => {
2127
2368
  metricSpread,
2128
2369
  innerPx,
2129
2370
  stepPx,
2130
- pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx)
2371
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
2372
+ minimumGapPx: stepPx * 0.42
2131
2373
  };
2132
2374
  }
2133
- function resolveOrbitGeometry(object, index, count, parent, metricContext, context) {
2375
+ function resolveOrbitGeometry(object, index, count, parent, metricContext, orbitRadiusPx, context) {
2134
2376
  const placement = object.placement;
2135
2377
  const band = object.type === "belt" || object.type === "ring";
2136
2378
  if (!placement || placement.mode !== "orbit") {
@@ -2148,7 +2390,7 @@ var WorldOrbit = (() => {
2148
2390
  };
2149
2391
  }
2150
2392
  const eccentricity = clampNumber(typeof placement.eccentricity === "number" ? placement.eccentricity : 0, 0, 0.92);
2151
- const semiMajor = resolveOrbitRadiusPx(object, index, metricContext);
2393
+ const semiMajor = orbitRadiusPx;
2152
2394
  const baseMinor = Math.max(semiMajor * Math.sqrt(1 - eccentricity * eccentricity), semiMajor * 0.18);
2153
2395
  const inclinationDeg = unitValueToDegrees(placement.inclination) ?? 0;
2154
2396
  const inclinationScale = context.projection === "isometric" ? Math.max(MIN_ISO_MINOR_SCALE, Math.cos(degreesToRadians(inclinationDeg))) * ISO_FLATTENING : 1;
@@ -2178,15 +2420,19 @@ var WorldOrbit = (() => {
2178
2420
  objectY: objectPoint.y
2179
2421
  };
2180
2422
  }
2181
- function resolveOrbitRadiusPx(object, index, metricContext) {
2182
- const metric = orbitMetric(object);
2183
- if (metric === null) {
2184
- return metricContext.innerPx + index * metricContext.stepPx;
2185
- }
2186
- if (metricContext.metricSpread > 0) {
2187
- return metricContext.innerPx + (metric - metricContext.minMetric) / metricContext.metricSpread * metricContext.pixelSpread;
2188
- }
2189
- return metricContext.innerPx + Math.log10(metric + 1) * metricContext.stepPx;
2423
+ function resolveOrbitRadiusPx(metric, metricContext) {
2424
+ return metricContext.innerPx + metricContext.stepPx * log2(Math.max(metric, 0) + 1);
2425
+ }
2426
+ function resolveOrbitRadiiPx(objects, metricContext) {
2427
+ const radii = [];
2428
+ objects.forEach((object, index) => {
2429
+ const metric = orbitMetric(object);
2430
+ const fallbackRadius = metricContext.innerPx + index * metricContext.stepPx;
2431
+ const baseRadius = metric === null ? fallbackRadius : resolveOrbitRadiusPx(metric, metricContext);
2432
+ const minimumRadius = index === 0 ? metricContext.innerPx : (radii[index - 1] ?? metricContext.innerPx) + metricContext.minimumGapPx;
2433
+ radii.push(Math.max(baseRadius, minimumRadius));
2434
+ });
2435
+ return radii;
2190
2436
  }
2191
2437
  function orbitMetric(object) {
2192
2438
  if (!object.placement || object.placement.mode !== "orbit") {
@@ -2194,6 +2440,9 @@ var WorldOrbit = (() => {
2194
2440
  }
2195
2441
  return toDistanceMetric(object.placement.semiMajor ?? object.placement.distance ?? null);
2196
2442
  }
2443
+ function log2(value) {
2444
+ return Math.log(value) / Math.log(2);
2445
+ }
2197
2446
  function resolveOrbitPhase(phase, index, count) {
2198
2447
  const degreeValue = phase ? unitValueToDegrees(phase) : null;
2199
2448
  if (degreeValue !== null) {
@@ -2397,7 +2646,7 @@ var WorldOrbit = (() => {
2397
2646
  return null;
2398
2647
  }
2399
2648
  }
2400
- function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels) {
2649
+ function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier) {
2401
2650
  let minX = Number.POSITIVE_INFINITY;
2402
2651
  let minY = Number.POSITIVE_INFINITY;
2403
2652
  let maxX = Number.NEGATIVE_INFINITY;
@@ -2426,7 +2675,7 @@ var WorldOrbit = (() => {
2426
2675
  }
2427
2676
  for (const label of labels) {
2428
2677
  if (!label.hidden && group.labelIds.includes(label.objectId)) {
2429
- includeLabelBounds(label, include);
2678
+ includeLabelBounds(label, include, labelMultiplier);
2430
2679
  }
2431
2680
  }
2432
2681
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
@@ -2451,12 +2700,28 @@ var WorldOrbit = (() => {
2451
2700
  }
2452
2701
  return current.id;
2453
2702
  }
2454
- function createLabelRect(x, labelY, secondaryY, labelHalfWidth, direction) {
2703
+ function createLabelRect(object, placement, labelMultiplier) {
2704
+ return createLabelRectFromText(placement.x, placement.labelY, placement.secondaryY, placement.textAnchor, placement.direction, object.label, object.secondaryLabel, labelMultiplier);
2705
+ }
2706
+ function createLabelRectFromText(x, labelY, secondaryY, textAnchor, direction, label, secondaryLabel, labelMultiplier) {
2707
+ const labelHalfWidth = estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier);
2708
+ const labelWidth = labelHalfWidth * 2;
2709
+ const topPadding = direction === "above" ? 18 : 12;
2710
+ const bottomPadding = direction === "above" ? 8 : 12;
2711
+ let left = x - labelHalfWidth;
2712
+ let right = x + labelHalfWidth;
2713
+ if (textAnchor === "start") {
2714
+ left = x;
2715
+ right = x + labelWidth;
2716
+ } else if (textAnchor === "end") {
2717
+ left = x - labelWidth;
2718
+ right = x;
2719
+ }
2455
2720
  return {
2456
- left: x - labelHalfWidth,
2457
- right: x + labelHalfWidth,
2458
- top: Math.min(labelY, secondaryY) - (direction < 0 ? 18 : 12),
2459
- bottom: Math.max(labelY, secondaryY) + (direction < 0 ? 8 : 12)
2721
+ left,
2722
+ right,
2723
+ top: Math.min(labelY, secondaryY) - topPadding,
2724
+ bottom: Math.max(labelY, secondaryY) + bottomPadding
2460
2725
  };
2461
2726
  }
2462
2727
  function rectsOverlap(left, right) {
@@ -2517,8 +2782,18 @@ var WorldOrbit = (() => {
2517
2782
  return value.value;
2518
2783
  case "km":
2519
2784
  return value.value / AU_IN_KM;
2785
+ case "m":
2786
+ return value.value / 1e3 / AU_IN_KM;
2787
+ case "ly":
2788
+ return value.value * LY_IN_AU;
2789
+ case "pc":
2790
+ return value.value * PC_IN_AU;
2791
+ case "kpc":
2792
+ return value.value * KPC_IN_AU;
2520
2793
  case "re":
2521
2794
  return value.value * EARTH_RADIUS_IN_KM / AU_IN_KM;
2795
+ case "rj":
2796
+ return value.value * JUPITER_RADIUS_IN_KM / AU_IN_KM;
2522
2797
  case "sol":
2523
2798
  return value.value * SOLAR_RADIUS_IN_KM / AU_IN_KM;
2524
2799
  default:
@@ -2633,11 +2908,6 @@ var WorldOrbit = (() => {
2633
2908
  function customColorFor(value) {
2634
2909
  return typeof value === "string" && value.trim() ? value : void 0;
2635
2910
  }
2636
- function estimateLabelHalfWidth(object, labelMultiplier) {
2637
- const primaryWidth = object.label.length * 4.6 * labelMultiplier + 18;
2638
- const secondaryWidth = object.secondaryLabel.length * 3.9 * labelMultiplier + 18;
2639
- return Math.max(primaryWidth, secondaryWidth, object.visualRadius + 18);
2640
- }
2641
2911
  function estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier) {
2642
2912
  const primaryWidth = label.length * 4.6 * labelMultiplier + 18;
2643
2913
  const secondaryWidth = secondaryLabel.length * 3.9 * labelMultiplier + 18;
@@ -2654,28 +2924,95 @@ var WorldOrbit = (() => {
2654
2924
  }
2655
2925
 
2656
2926
  // packages/core/dist/draft.js
2657
- function materializeAtlasDocument(document2) {
2927
+ function materializeAtlasDocument(document2, options = {}) {
2658
2928
  const system = document2.system ? {
2659
2929
  type: "system",
2660
2930
  id: document2.system.id,
2931
+ title: document2.system.title,
2932
+ description: document2.system.description,
2933
+ epoch: document2.system.epoch,
2934
+ referencePlane: document2.system.referencePlane,
2661
2935
  properties: materializeDraftSystemProperties(document2.system),
2662
2936
  info: materializeDraftSystemInfo(document2.system)
2663
2937
  } : null;
2938
+ const objects = document2.objects.map(cloneWorldOrbitObject);
2939
+ applyEventPoseOverrides(objects, document2.events ?? [], options.activeEventId ?? null);
2664
2940
  return {
2665
2941
  format: "worldorbit",
2666
2942
  version: "1.0",
2943
+ schemaVersion: document2.version,
2667
2944
  system,
2668
- objects: document2.objects.map(cloneWorldOrbitObject)
2945
+ groups: structuredClone(document2.groups ?? []),
2946
+ relations: structuredClone(document2.relations ?? []),
2947
+ events: document2.events.map(cloneWorldOrbitEvent),
2948
+ objects
2669
2949
  };
2670
2950
  }
2671
2951
  function cloneWorldOrbitObject(object) {
2672
2952
  return {
2673
2953
  ...object,
2954
+ groups: object.groups ? [...object.groups] : void 0,
2955
+ resonance: object.resonance ? { ...object.resonance } : object.resonance,
2956
+ renderHints: object.renderHints ? { ...object.renderHints } : object.renderHints,
2957
+ deriveRules: object.deriveRules ? object.deriveRules.map((rule) => ({ ...rule })) : void 0,
2958
+ validationRules: object.validationRules ? object.validationRules.map((rule) => ({ ...rule })) : void 0,
2959
+ lockedFields: object.lockedFields ? [...object.lockedFields] : void 0,
2960
+ tolerances: object.tolerances ? object.tolerances.map((entry) => ({
2961
+ field: entry.field,
2962
+ 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
2963
+ })) : void 0,
2964
+ typedBlocks: object.typedBlocks ? Object.fromEntries(Object.entries(object.typedBlocks).map(([key, block]) => [key, { ...block ?? {} }])) : void 0,
2674
2965
  properties: cloneProperties(object.properties),
2675
2966
  placement: object.placement ? structuredClone(object.placement) : null,
2676
2967
  info: { ...object.info }
2677
2968
  };
2678
2969
  }
2970
+ function cloneWorldOrbitEvent(event) {
2971
+ return {
2972
+ ...event,
2973
+ participantObjectIds: [...event.participantObjectIds],
2974
+ tags: [...event.tags],
2975
+ positions: event.positions.map(cloneWorldOrbitEventPose)
2976
+ };
2977
+ }
2978
+ function cloneWorldOrbitEventPose(pose) {
2979
+ return {
2980
+ objectId: pose.objectId,
2981
+ placement: clonePlacement(pose.placement),
2982
+ inner: pose.inner ? { ...pose.inner } : void 0,
2983
+ outer: pose.outer ? { ...pose.outer } : void 0
2984
+ };
2985
+ }
2986
+ function clonePlacement(placement) {
2987
+ return placement ? structuredClone(placement) : null;
2988
+ }
2989
+ function applyEventPoseOverrides(objects, events, activeEventId) {
2990
+ if (!activeEventId) {
2991
+ return;
2992
+ }
2993
+ const event = events.find((entry) => entry.id === activeEventId);
2994
+ if (!event) {
2995
+ return;
2996
+ }
2997
+ const objectMap = new Map(objects.map((object) => [object.id, object]));
2998
+ for (const pose of event.positions) {
2999
+ const object = objectMap.get(pose.objectId);
3000
+ if (!object) {
3001
+ continue;
3002
+ }
3003
+ object.placement = clonePlacement(pose.placement);
3004
+ if (pose.inner) {
3005
+ object.properties.inner = { ...pose.inner };
3006
+ } else {
3007
+ delete object.properties.inner;
3008
+ }
3009
+ if (pose.outer) {
3010
+ object.properties.outer = { ...pose.outer };
3011
+ } else {
3012
+ delete object.properties.outer;
3013
+ }
3014
+ }
3015
+ }
2679
3016
  function cloneProperties(properties) {
2680
3017
  const next = {};
2681
3018
  for (const [key, value] of Object.entries(properties)) {
@@ -2706,71 +3043,83 @@ var WorldOrbit = (() => {
2706
3043
  if (system.defaults.units) {
2707
3044
  properties.units = system.defaults.units;
2708
3045
  }
3046
+ if (system.description) {
3047
+ properties.description = system.description;
3048
+ }
3049
+ if (system.epoch) {
3050
+ properties.epoch = system.epoch;
3051
+ }
3052
+ if (system.referencePlane) {
3053
+ properties.referencePlane = system.referencePlane;
3054
+ }
2709
3055
  return properties;
2710
3056
  }
2711
3057
  function materializeDraftSystemInfo(system) {
2712
- const info = {
3058
+ const info2 = {
2713
3059
  ...system.atlasMetadata
2714
3060
  };
2715
3061
  if (system.defaults.theme) {
2716
- info["atlas.theme"] = system.defaults.theme;
3062
+ info2["atlas.theme"] = system.defaults.theme;
2717
3063
  }
2718
3064
  for (const viewpoint of system.viewpoints) {
2719
3065
  const prefix = `viewpoint.${viewpoint.id}`;
2720
- info[`${prefix}.label`] = viewpoint.label;
3066
+ info2[`${prefix}.label`] = viewpoint.label;
2721
3067
  if (viewpoint.summary) {
2722
- info[`${prefix}.summary`] = viewpoint.summary;
3068
+ info2[`${prefix}.summary`] = viewpoint.summary;
2723
3069
  }
2724
3070
  if (viewpoint.focusObjectId) {
2725
- info[`${prefix}.focus`] = viewpoint.focusObjectId;
3071
+ info2[`${prefix}.focus`] = viewpoint.focusObjectId;
2726
3072
  }
2727
3073
  if (viewpoint.selectedObjectId) {
2728
- info[`${prefix}.select`] = viewpoint.selectedObjectId;
3074
+ info2[`${prefix}.select`] = viewpoint.selectedObjectId;
2729
3075
  }
2730
3076
  if (viewpoint.projection) {
2731
- info[`${prefix}.projection`] = viewpoint.projection;
3077
+ info2[`${prefix}.projection`] = viewpoint.projection;
2732
3078
  }
2733
3079
  if (viewpoint.preset) {
2734
- info[`${prefix}.preset`] = viewpoint.preset;
3080
+ info2[`${prefix}.preset`] = viewpoint.preset;
2735
3081
  }
2736
3082
  if (viewpoint.zoom !== null) {
2737
- info[`${prefix}.zoom`] = String(viewpoint.zoom);
3083
+ info2[`${prefix}.zoom`] = String(viewpoint.zoom);
2738
3084
  }
2739
3085
  if (viewpoint.rotationDeg !== 0) {
2740
- info[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
3086
+ info2[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2741
3087
  }
2742
3088
  const serializedLayers = serializeViewpointLayers(viewpoint.layers);
2743
3089
  if (serializedLayers) {
2744
- info[`${prefix}.layers`] = serializedLayers;
3090
+ info2[`${prefix}.layers`] = serializedLayers;
2745
3091
  }
2746
3092
  if (viewpoint.filter?.query) {
2747
- info[`${prefix}.query`] = viewpoint.filter.query;
3093
+ info2[`${prefix}.query`] = viewpoint.filter.query;
2748
3094
  }
2749
3095
  if ((viewpoint.filter?.objectTypes.length ?? 0) > 0) {
2750
- info[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
3096
+ info2[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
2751
3097
  }
2752
3098
  if ((viewpoint.filter?.tags.length ?? 0) > 0) {
2753
- info[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
3099
+ info2[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
2754
3100
  }
2755
3101
  if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2756
- info[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
3102
+ info2[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
3103
+ }
3104
+ if (viewpoint.events.length > 0) {
3105
+ info2[`${prefix}.events`] = viewpoint.events.join(" ");
2757
3106
  }
2758
3107
  }
2759
3108
  for (const annotation of system.annotations) {
2760
3109
  const prefix = `annotation.${annotation.id}`;
2761
- info[`${prefix}.label`] = annotation.label;
3110
+ info2[`${prefix}.label`] = annotation.label;
2762
3111
  if (annotation.targetObjectId) {
2763
- info[`${prefix}.target`] = annotation.targetObjectId;
3112
+ info2[`${prefix}.target`] = annotation.targetObjectId;
2764
3113
  }
2765
- info[`${prefix}.body`] = annotation.body;
3114
+ info2[`${prefix}.body`] = annotation.body;
2766
3115
  if (annotation.tags.length > 0) {
2767
- info[`${prefix}.tags`] = annotation.tags.join(" ");
3116
+ info2[`${prefix}.tags`] = annotation.tags.join(" ");
2768
3117
  }
2769
3118
  if (annotation.sourceObjectId) {
2770
- info[`${prefix}.source`] = annotation.sourceObjectId;
3119
+ info2[`${prefix}.source`] = annotation.sourceObjectId;
2771
3120
  }
2772
3121
  }
2773
- return info;
3122
+ return info2;
2774
3123
  }
2775
3124
  function serializeViewpointLayers(layers) {
2776
3125
  const tokens = [];
@@ -2779,7 +3128,7 @@ var WorldOrbit = (() => {
2779
3128
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2780
3129
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2781
3130
  }
2782
- for (const key of ["background", "guides", "objects", "labels", "metadata"]) {
3131
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
2783
3132
  if (layers[key] !== void 0) {
2784
3133
  tokens.push(layers[key] ? key : `-${key}`);
2785
3134
  }
@@ -2787,171 +3136,838 @@ var WorldOrbit = (() => {
2787
3136
  return tokens.join(" ");
2788
3137
  }
2789
3138
 
2790
- // packages/core/dist/draft-parse.js
2791
- function parseWorldOrbitAtlas(source) {
2792
- return parseAtlasSource(source, "2.0");
3139
+ // packages/core/dist/atlas-utils.js
3140
+ 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)?$/;
3141
+ var BOOLEAN_VALUES2 = /* @__PURE__ */ new Map([
3142
+ ["true", true],
3143
+ ["false", false],
3144
+ ["yes", true],
3145
+ ["no", false]
3146
+ ]);
3147
+ var URL_SCHEME_PATTERN2 = /^[A-Za-z][A-Za-z0-9+.-]*:/;
3148
+ function normalizeIdentifier(value) {
3149
+ return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
2793
3150
  }
2794
- function parseAtlasSource(source, outputVersion) {
2795
- const lines = source.split(/\r?\n/);
2796
- let sawSchemaHeader = false;
2797
- let schemaVersion = "2.0";
2798
- let system = null;
2799
- let section = null;
2800
- const objectNodes = [];
2801
- let sawDefaults = false;
2802
- let sawAtlas = false;
2803
- const viewpointIds = /* @__PURE__ */ new Set();
2804
- const annotationIds = /* @__PURE__ */ new Set();
2805
- for (let index = 0; index < lines.length; index++) {
2806
- const rawLine = lines[index];
2807
- const lineNumber = index + 1;
2808
- if (!rawLine.trim()) {
2809
- continue;
2810
- }
2811
- const indent = getIndent(rawLine);
2812
- const tokens = tokenizeLineDetailed(rawLine.slice(indent), {
2813
- line: lineNumber,
2814
- columnOffset: indent
2815
- });
2816
- if (tokens.length === 0) {
2817
- continue;
2818
- }
2819
- if (!sawSchemaHeader) {
2820
- schemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
2821
- sawSchemaHeader = true;
2822
- continue;
2823
- }
2824
- if (indent === 0) {
2825
- section = startTopLevelSection(tokens, lineNumber, system, objectNodes, viewpointIds, annotationIds, {
2826
- sawDefaults,
2827
- sawAtlas
2828
- });
2829
- if (section.kind === "system") {
2830
- system = section.system;
2831
- } else if (section.kind === "defaults") {
2832
- sawDefaults = true;
2833
- } else if (section.kind === "atlas") {
2834
- sawAtlas = true;
2835
- }
2836
- continue;
2837
- }
2838
- if (!section) {
2839
- throw new WorldOrbitError("Indented line without parent atlas section", lineNumber, indent + 1);
2840
- }
2841
- handleSectionLine(section, indent, tokens, lineNumber);
2842
- }
2843
- if (!sawSchemaHeader) {
2844
- throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
3151
+ function humanizeIdentifier2(value) {
3152
+ return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
3153
+ }
3154
+ function parseAtlasUnitValue(input, location, fieldKey) {
3155
+ const match = input.match(UNIT_PATTERN2);
3156
+ if (!match) {
3157
+ throw WorldOrbitError.fromLocation(`Invalid unit value "${input}"`, location);
2845
3158
  }
2846
- const ast = {
2847
- type: "document",
2848
- objects: objectNodes
3159
+ const unitValue = {
3160
+ value: Number(match[1]),
3161
+ unit: match[2] ?? null
2849
3162
  };
2850
- const normalizedObjects = normalizeDocument(ast).objects;
2851
- validateDocument({
2852
- format: "worldorbit",
2853
- version: "1.0",
2854
- system: null,
2855
- objects: normalizedObjects
2856
- });
2857
- const diagnostics = schemaVersion === "2.0-draft" && outputVersion === "2.0" ? [
2858
- {
2859
- code: "load.schema.deprecatedDraft",
2860
- severity: "warning",
2861
- source: "upgrade",
2862
- message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".'
3163
+ if (fieldKey) {
3164
+ const schema = getFieldSchema(fieldKey);
3165
+ if (schema?.unitFamily && !unitFamilyAllowsUnit(schema.unitFamily, unitValue.unit)) {
3166
+ throw WorldOrbitError.fromLocation(`Unit "${unitValue.unit ?? "none"}" is not valid for "${fieldKey}"`, location);
2863
3167
  }
2864
- ] : [];
3168
+ }
3169
+ return unitValue;
3170
+ }
3171
+ function tryParseAtlasUnitValue(input) {
3172
+ const match = input.match(UNIT_PATTERN2);
3173
+ if (!match) {
3174
+ return null;
3175
+ }
2865
3176
  return {
2866
- format: "worldorbit",
2867
- version: outputVersion,
2868
- sourceVersion: "1.0",
2869
- system,
2870
- objects: normalizedObjects,
2871
- diagnostics
3177
+ value: Number(match[1]),
3178
+ unit: match[2] ?? null
2872
3179
  };
2873
3180
  }
2874
- function assertDraftSchemaHeader(tokens, line) {
2875
- if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || tokens[1].value.toLowerCase() !== "2.0-draft" && tokens[1].value.toLowerCase() !== "2.0") {
2876
- throw new WorldOrbitError('Expected atlas header "schema 2.0" or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
3181
+ function parseAtlasNumber(input, key, location) {
3182
+ const value = Number(input);
3183
+ if (!Number.isFinite(value)) {
3184
+ throw WorldOrbitError.fromLocation(`Invalid numeric value "${input}" for "${key}"`, location);
2877
3185
  }
2878
- return tokens[1].value.toLowerCase() === "2.0-draft" ? "2.0-draft" : "2.0";
3186
+ return value;
2879
3187
  }
2880
- function startTopLevelSection(tokens, line, system, objectNodes, viewpointIds, annotationIds, flags) {
2881
- const keyword = tokens[0]?.value.toLowerCase();
2882
- switch (keyword) {
2883
- case "system":
2884
- if (system) {
2885
- throw new WorldOrbitError('Atlas section "system" may only appear once', line, tokens[0].column);
2886
- }
2887
- return startSystemSection(tokens, line);
2888
- case "defaults":
2889
- if (!system) {
2890
- throw new WorldOrbitError('Atlas section "defaults" requires a preceding system declaration', line, tokens[0].column);
2891
- }
2892
- if (flags.sawDefaults) {
2893
- throw new WorldOrbitError('Atlas section "defaults" may only appear once', line, tokens[0].column);
2894
- }
2895
- return {
2896
- kind: "defaults",
2897
- system,
2898
- seenFields: /* @__PURE__ */ new Set()
2899
- };
2900
- case "atlas":
2901
- if (!system) {
2902
- throw new WorldOrbitError('Atlas section "atlas" requires a preceding system declaration', line, tokens[0].column);
2903
- }
2904
- if (flags.sawAtlas) {
2905
- throw new WorldOrbitError('Atlas section "atlas" may only appear once', line, tokens[0].column);
2906
- }
2907
- return {
2908
- kind: "atlas",
2909
- system,
2910
- inMetadata: false,
2911
- metadataIndent: null
2912
- };
2913
- case "viewpoint":
2914
- if (!system) {
2915
- throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
2916
- }
2917
- return startViewpointSection(tokens, line, system, viewpointIds);
2918
- case "annotation":
2919
- if (!system) {
2920
- throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
2921
- }
2922
- return startAnnotationSection(tokens, line, system, annotationIds);
2923
- case "object":
2924
- return startObjectSection(tokens, line, objectNodes);
2925
- default:
2926
- throw new WorldOrbitError(`Unknown atlas section "${tokens[0]?.value ?? ""}"`, line, tokens[0]?.column ?? 1);
3188
+ function parseAtlasBoolean(input, key, location) {
3189
+ const parsed = BOOLEAN_VALUES2.get(input.toLowerCase());
3190
+ if (parsed === void 0) {
3191
+ throw WorldOrbitError.fromLocation(`Invalid boolean value "${input}" for "${key}"`, location);
2927
3192
  }
3193
+ return parsed;
2928
3194
  }
2929
- function startSystemSection(tokens, line) {
2930
- if (tokens.length !== 2) {
2931
- throw new WorldOrbitError("Invalid atlas system declaration", line, tokens[0]?.column ?? 1);
3195
+ function parseAtlasAtReference(target, location) {
3196
+ if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
3197
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
2932
3198
  }
2933
- const system = {
2934
- type: "system",
2935
- id: tokens[1].value,
2936
- title: null,
2937
- defaults: {
2938
- view: "topdown",
2939
- scale: null,
2940
- units: null,
2941
- preset: null,
2942
- theme: null
2943
- },
2944
- atlasMetadata: {},
2945
- viewpoints: [],
3199
+ const pairedMatch = target.match(/^([A-Za-z0-9._-]+)-([A-Za-z0-9._-]+):(L[1-5])$/);
3200
+ if (pairedMatch) {
3201
+ return {
3202
+ kind: "lagrange",
3203
+ primary: pairedMatch[1],
3204
+ secondary: pairedMatch[2],
3205
+ point: pairedMatch[3]
3206
+ };
3207
+ }
3208
+ const simpleMatch = target.match(/^([A-Za-z0-9._-]+):(L[1-5])$/);
3209
+ if (simpleMatch) {
3210
+ return {
3211
+ kind: "lagrange",
3212
+ primary: simpleMatch[1],
3213
+ secondary: null,
3214
+ point: simpleMatch[2]
3215
+ };
3216
+ }
3217
+ if (/^[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
3218
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
3219
+ }
3220
+ const anchorMatch = target.match(/^([A-Za-z0-9._-]+):([A-Za-z0-9._-]+)$/);
3221
+ if (anchorMatch) {
3222
+ return {
3223
+ kind: "anchor",
3224
+ objectId: anchorMatch[1],
3225
+ anchor: anchorMatch[2]
3226
+ };
3227
+ }
3228
+ return {
3229
+ kind: "named",
3230
+ name: target
3231
+ };
3232
+ }
3233
+ function validateAtlasImageSource(value, location) {
3234
+ if (!value) {
3235
+ throw WorldOrbitError.fromLocation('Field "image" must not be empty', location);
3236
+ }
3237
+ if (value.startsWith("//")) {
3238
+ throw WorldOrbitError.fromLocation('Field "image" must use a relative path, root-relative path, or an http/https URL', location);
3239
+ }
3240
+ const schemeMatch = value.match(URL_SCHEME_PATTERN2);
3241
+ if (!schemeMatch) {
3242
+ return;
3243
+ }
3244
+ const scheme = schemeMatch[0].slice(0, -1).toLowerCase();
3245
+ if (scheme !== "http" && scheme !== "https") {
3246
+ throw WorldOrbitError.fromLocation(`Field "image" does not support the "${scheme}" scheme`, location);
3247
+ }
3248
+ }
3249
+ function normalizeLegacyScalarValue(key, values, location) {
3250
+ const schema = getFieldSchema(key);
3251
+ if (!schema) {
3252
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
3253
+ }
3254
+ if (schema.arity === "single" && values.length !== 1) {
3255
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
3256
+ }
3257
+ switch (schema.kind) {
3258
+ case "list":
3259
+ return values;
3260
+ case "boolean":
3261
+ return parseAtlasBoolean(singleAtlasValue(values, key, location), key, location);
3262
+ case "number":
3263
+ return parseAtlasNumber(singleAtlasValue(values, key, location), key, location);
3264
+ case "unit":
3265
+ return parseAtlasUnitValue(singleAtlasValue(values, key, location), location, key);
3266
+ case "string": {
3267
+ const value = values.join(" ").trim();
3268
+ if (key === "image") {
3269
+ validateAtlasImageSource(value, location);
3270
+ }
3271
+ return value;
3272
+ }
3273
+ }
3274
+ }
3275
+ function ensureAtlasFieldSupported(key, objectType, location) {
3276
+ const schema = getFieldSchema(key);
3277
+ if (!schema) {
3278
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
3279
+ }
3280
+ if (!schema.objectTypes.includes(objectType)) {
3281
+ throw WorldOrbitError.fromLocation(`Field "${key}" is not valid on "${objectType}"`, location);
3282
+ }
3283
+ }
3284
+ function singleAtlasValue(values, key, location) {
3285
+ if (values.length !== 1) {
3286
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
3287
+ }
3288
+ return values[0];
3289
+ }
3290
+
3291
+ // packages/core/dist/atlas-validate.js
3292
+ var SURFACE_TARGET_TYPES2 = /* @__PURE__ */ new Set(["star", "planet", "moon", "asteroid", "comet"]);
3293
+ var EARTH_MASSES_PER_SOLAR = 332946.0487;
3294
+ var JUPITER_MASSES_PER_SOLAR = 1047.3486;
3295
+ var AU_IN_KM2 = 1495978707e-1;
3296
+ var EARTH_RADIUS_IN_KM2 = 6371;
3297
+ var SOLAR_RADIUS_IN_KM2 = 695700;
3298
+ var LY_IN_AU2 = 63241.077;
3299
+ var PC_IN_AU2 = 206264.806;
3300
+ var KPC_IN_AU2 = 206264806;
3301
+ function collectAtlasDiagnostics(document2, sourceSchemaVersion) {
3302
+ const diagnostics = [];
3303
+ const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
3304
+ const groupIds = new Set(document2.groups.map((group) => group.id));
3305
+ const eventIds = new Set(document2.events.map((event) => event.id));
3306
+ if (!document2.system) {
3307
+ diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
3308
+ }
3309
+ const knownIds = /* @__PURE__ */ new Map();
3310
+ for (const [kind, ids] of [
3311
+ ["group", document2.groups.map((group) => group.id)],
3312
+ ["viewpoint", document2.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
3313
+ ["annotation", document2.system?.annotations.map((annotation) => annotation.id) ?? []],
3314
+ ["relation", document2.relations.map((relation) => relation.id)],
3315
+ ["event", document2.events.map((event) => event.id)],
3316
+ ["object", document2.objects.map((object) => object.id)]
3317
+ ]) {
3318
+ for (const id of ids) {
3319
+ const previous = knownIds.get(id);
3320
+ if (previous) {
3321
+ diagnostics.push(error("validate.id.duplicate", `Duplicate ${kind} id "${id}" already used by ${previous}.`));
3322
+ } else {
3323
+ knownIds.set(id, kind);
3324
+ }
3325
+ }
3326
+ }
3327
+ for (const relation of document2.relations) {
3328
+ validateRelation(relation, objectMap, diagnostics);
3329
+ }
3330
+ for (const viewpoint of document2.system?.viewpoints ?? []) {
3331
+ validateViewpoint(viewpoint.filter, viewpoint.events ?? [], groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpoint.id);
3332
+ }
3333
+ for (const object of document2.objects) {
3334
+ validateObject(object, document2.system, objectMap, groupIds, diagnostics);
3335
+ }
3336
+ for (const event of document2.events) {
3337
+ validateEvent(event, objectMap, diagnostics);
3338
+ }
3339
+ return diagnostics;
3340
+ }
3341
+ function validateRelation(relation, objectMap, diagnostics) {
3342
+ if (!relation.from) {
3343
+ diagnostics.push(error("validate.relation.from.required", `Relation "${relation.id}" is missing a "from" target.`));
3344
+ } else if (!objectMap.has(relation.from)) {
3345
+ diagnostics.push(error("validate.relation.from.unknown", `Unknown relation source "${relation.from}" on "${relation.id}".`));
3346
+ }
3347
+ if (!relation.to) {
3348
+ diagnostics.push(error("validate.relation.to.required", `Relation "${relation.id}" is missing a "to" target.`));
3349
+ } else if (!objectMap.has(relation.to)) {
3350
+ diagnostics.push(error("validate.relation.to.unknown", `Unknown relation target "${relation.to}" on "${relation.id}".`));
3351
+ }
3352
+ if (!relation.kind) {
3353
+ diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
3354
+ }
3355
+ }
3356
+ function validateViewpoint(filter, eventRefs, groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpointId) {
3357
+ if (sourceSchemaVersion === "2.1") {
3358
+ if (filter) {
3359
+ for (const groupId of filter.groupIds) {
3360
+ if (!groupIds.has(groupId)) {
3361
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.groups`));
3362
+ }
3363
+ }
3364
+ }
3365
+ for (const eventId of eventRefs) {
3366
+ if (!eventIds.has(eventId)) {
3367
+ diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.events`));
3368
+ }
3369
+ }
3370
+ }
3371
+ }
3372
+ function validateObject(object, system, objectMap, groupIds, diagnostics) {
3373
+ const placement = object.placement;
3374
+ const orbitPlacement = placement?.mode === "orbit" ? placement : null;
3375
+ const parentObject = placement?.mode === "orbit" ? objectMap.get(placement.target) ?? null : null;
3376
+ if (object.groups) {
3377
+ for (const groupId of object.groups) {
3378
+ if (!groupIds.has(groupId)) {
3379
+ diagnostics.push(warn("validate.group.unknown", `Unknown group "${groupId}" on "${object.id}".`, object.id, "groups"));
3380
+ }
3381
+ }
3382
+ }
3383
+ if (orbitPlacement) {
3384
+ if (!objectMap.has(orbitPlacement.target)) {
3385
+ diagnostics.push(error("validate.orbit.target.unknown", `Unknown placement target "${orbitPlacement.target}" on "${object.id}".`, object.id, "orbit"));
3386
+ }
3387
+ if (orbitPlacement.distance && orbitPlacement.semiMajor) {
3388
+ diagnostics.push(error("validate.orbit.distanceConflict", `Object "${object.id}" cannot declare both "distance" and "semiMajor".`, object.id, "distance"));
3389
+ }
3390
+ if (orbitPlacement.phase && !object.epoch && !system?.epoch) {
3391
+ diagnostics.push(warn("validate.phase.epochMissing", `Object "${object.id}" sets "phase" without an object or system epoch.`, object.id, "phase"));
3392
+ }
3393
+ if (orbitPlacement.inclination && !object.referencePlane && !system?.referencePlane) {
3394
+ diagnostics.push(warn("validate.inclination.referencePlaneMissing", `Object "${object.id}" sets "inclination" without an object or system reference plane.`, object.id, "inclination"));
3395
+ }
3396
+ if (orbitPlacement.period && !massInSolar(parentObject?.properties.mass)) {
3397
+ diagnostics.push(warn("validate.period.massMissing", `Object "${object.id}" sets "period" but its central mass cannot be derived.`, object.id, "period"));
3398
+ }
3399
+ }
3400
+ if (placement?.mode === "surface") {
3401
+ const target = objectMap.get(placement.target);
3402
+ if (!target) {
3403
+ diagnostics.push(error("validate.surface.target.unknown", `Unknown placement target "${placement.target}" on "${object.id}".`, object.id, "surface"));
3404
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
3405
+ diagnostics.push(error("validate.surface.target.invalid", `Surface target "${placement.target}" on "${object.id}" is not surface-capable.`, object.id, "surface"));
3406
+ }
3407
+ }
3408
+ if (placement?.mode === "at") {
3409
+ if (object.type !== "structure" && object.type !== "phenomenon") {
3410
+ diagnostics.push(error("validate.at.objectType", `Only structures and phenomena may use "at" placement; found "${object.type}" on "${object.id}".`, object.id, "at"));
3411
+ }
3412
+ if (!validateAtTarget(object, objectMap, diagnostics)) {
3413
+ diagnostics.push(error("validate.at.target.unknown", `Unknown at-reference target "${placement.target}" on "${object.id}".`, object.id, "at"));
3414
+ }
3415
+ }
3416
+ if (object.resonance) {
3417
+ const target = objectMap.get(object.resonance.targetObjectId);
3418
+ if (!target) {
3419
+ diagnostics.push(error("validate.resonance.target.unknown", `Unknown resonance target "${object.resonance.targetObjectId}" on "${object.id}".`, object.id, "resonance"));
3420
+ } else if (object.placement?.mode !== "orbit" || target.placement?.mode !== "orbit" || object.placement.target !== target.placement.target) {
3421
+ diagnostics.push(warn("validate.resonance.orbitMismatch", `Resonance target "${object.resonance.targetObjectId}" on "${object.id}" does not share a compatible orbital parent.`, object.id, "resonance"));
3422
+ }
3423
+ }
3424
+ for (const rule of object.deriveRules ?? []) {
3425
+ if (rule.field !== "period" || rule.strategy !== "kepler") {
3426
+ diagnostics.push(warn("validate.derive.unsupported", `Unsupported derive rule "${rule.field} ${rule.strategy}" on "${object.id}".`, object.id, "derive"));
3427
+ continue;
3428
+ }
3429
+ const derivedPeriodDays = keplerPeriodDays(object, parentObject);
3430
+ if (derivedPeriodDays === null) {
3431
+ diagnostics.push(warn("validate.derive.inputsMissing", `Object "${object.id}" requests "derive period kepler" but lacks enough input data.`, object.id, "derive"));
3432
+ continue;
3433
+ }
3434
+ if (!orbitPlacement?.period) {
3435
+ diagnostics.push(info("validate.derive.period.available", `Object "${object.id}" can derive a Kepler period of ${formatDays(derivedPeriodDays)}.`, object.id, "derive"));
3436
+ }
3437
+ }
3438
+ for (const rule of object.validationRules ?? []) {
3439
+ if (rule.rule !== "kepler") {
3440
+ diagnostics.push(warn("validate.rule.unsupported", `Unsupported validation rule "${rule.rule}" on "${object.id}".`, object.id, "validate"));
3441
+ continue;
3442
+ }
3443
+ const actualPeriodDays = durationInDays(orbitPlacement?.period);
3444
+ const derivedPeriodDays = keplerPeriodDays(object, parentObject);
3445
+ if (actualPeriodDays === null || derivedPeriodDays === null) {
3446
+ continue;
3447
+ }
3448
+ const toleranceDays = toleranceForField(object, "period");
3449
+ if (Math.abs(actualPeriodDays - derivedPeriodDays) > toleranceDays) {
3450
+ diagnostics.push(error("validate.kepler.mismatch", `Object "${object.id}" fails Kepler validation for "period".`, object.id, "validate"));
3451
+ }
3452
+ }
3453
+ }
3454
+ function validateEvent(event, objectMap, diagnostics) {
3455
+ const fieldPrefix = `event.${event.id}`;
3456
+ const referencedIds = /* @__PURE__ */ new Set();
3457
+ if (!event.kind.trim()) {
3458
+ diagnostics.push(error("validate.event.kind.required", `Event "${event.id}" is missing a "kind" value.`, void 0, `${fieldPrefix}.kind`));
3459
+ }
3460
+ if (!event.targetObjectId && event.participantObjectIds.length === 0) {
3461
+ diagnostics.push(error("validate.event.references.required", `Event "${event.id}" must define a "target" or at least one participant.`, void 0, `${fieldPrefix}.participants`));
3462
+ }
3463
+ if (event.targetObjectId) {
3464
+ referencedIds.add(event.targetObjectId);
3465
+ if (!objectMap.has(event.targetObjectId)) {
3466
+ diagnostics.push(error("validate.event.target.unknown", `Unknown event target "${event.targetObjectId}" on "${event.id}".`, void 0, `${fieldPrefix}.target`));
3467
+ }
3468
+ }
3469
+ const seenParticipants = /* @__PURE__ */ new Set();
3470
+ for (const participantId of event.participantObjectIds) {
3471
+ referencedIds.add(participantId);
3472
+ if (seenParticipants.has(participantId)) {
3473
+ diagnostics.push(warn("validate.event.participants.duplicate", `Event "${event.id}" repeats participant "${participantId}".`, void 0, `${fieldPrefix}.participants`));
3474
+ continue;
3475
+ }
3476
+ seenParticipants.add(participantId);
3477
+ if (!objectMap.has(participantId)) {
3478
+ diagnostics.push(error("validate.event.participants.unknown", `Unknown event participant "${participantId}" on "${event.id}".`, void 0, `${fieldPrefix}.participants`));
3479
+ }
3480
+ }
3481
+ if (event.targetObjectId && event.participantObjectIds.length > 0 && !event.participantObjectIds.includes(event.targetObjectId)) {
3482
+ diagnostics.push(warn("validate.event.target.notParticipant", `Event "${event.id}" defines a target outside its participants list.`, void 0, `${fieldPrefix}.target`));
3483
+ }
3484
+ if (event.positions.length === 0) {
3485
+ diagnostics.push(warn("validate.event.positions.missing", `Event "${event.id}" has no positions block and cannot drive a scene snapshot.`, void 0, `${fieldPrefix}.positions`));
3486
+ }
3487
+ if (/(?:^|[-_])(solar-eclipse|lunar-eclipse|transit|occultation)(?:$|[-_])/.test(event.kind) && referencedIds.size < 3) {
3488
+ 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`));
3489
+ }
3490
+ const poseIds = /* @__PURE__ */ new Set();
3491
+ for (const pose of event.positions) {
3492
+ const poseFieldPrefix = `${fieldPrefix}.pose.${pose.objectId}`;
3493
+ if (poseIds.has(pose.objectId)) {
3494
+ diagnostics.push(error("validate.event.pose.duplicate", `Event "${event.id}" defines "${pose.objectId}" more than once in positions.`, void 0, poseFieldPrefix));
3495
+ continue;
3496
+ }
3497
+ poseIds.add(pose.objectId);
3498
+ const object = objectMap.get(pose.objectId);
3499
+ if (!object) {
3500
+ diagnostics.push(error("validate.event.pose.object.unknown", `Unknown event pose object "${pose.objectId}" on "${event.id}".`, void 0, poseFieldPrefix));
3501
+ continue;
3502
+ }
3503
+ if (!referencedIds.has(pose.objectId)) {
3504
+ diagnostics.push(warn("validate.event.pose.unreferenced", `Event pose "${pose.objectId}" on "${event.id}" is not listed in target/participants.`, void 0, poseFieldPrefix));
3505
+ }
3506
+ validateEventPose(pose, object, objectMap, diagnostics, poseFieldPrefix, event.id);
3507
+ }
3508
+ }
3509
+ function validateEventPose(pose, object, objectMap, diagnostics, fieldPrefix, eventId) {
3510
+ const placement = pose.placement;
3511
+ if (!placement) {
3512
+ diagnostics.push(error("validate.event.pose.placement.required", `Event "${eventId}" pose "${pose.objectId}" is missing a placement mode.`, void 0, fieldPrefix));
3513
+ return;
3514
+ }
3515
+ if (placement.mode === "orbit") {
3516
+ if (!objectMap.has(placement.target)) {
3517
+ diagnostics.push(error("validate.event.pose.orbit.target.unknown", `Unknown event orbit target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.orbit`));
3518
+ }
3519
+ if (placement.distance && placement.semiMajor) {
3520
+ diagnostics.push(error("validate.event.pose.orbit.distanceConflict", `Event "${eventId}" pose "${pose.objectId}" cannot declare both "distance" and "semiMajor".`, void 0, `${fieldPrefix}.distance`));
3521
+ }
3522
+ return;
3523
+ }
3524
+ if (placement.mode === "surface") {
3525
+ const target = objectMap.get(placement.target);
3526
+ if (!target) {
3527
+ diagnostics.push(error("validate.event.pose.surface.target.unknown", `Unknown event surface target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.surface`));
3528
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
3529
+ 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`));
3530
+ }
3531
+ return;
3532
+ }
3533
+ if (placement.mode === "at") {
3534
+ if (object.type !== "structure" && object.type !== "phenomenon") {
3535
+ 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`));
3536
+ }
3537
+ const reference = placement.reference;
3538
+ if (reference.kind === "named" && !objectMap.has(reference.name)) {
3539
+ diagnostics.push(error("validate.event.pose.at.target.unknown", `Unknown event at-reference target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3540
+ } else if (reference.kind === "anchor" && !objectMap.has(reference.objectId)) {
3541
+ diagnostics.push(error("validate.event.pose.anchor.target.unknown", `Unknown event anchor target "${reference.objectId}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3542
+ } else if (reference.kind === "lagrange") {
3543
+ if (!objectMap.has(reference.primary)) {
3544
+ diagnostics.push(error("validate.event.pose.lagrange.primary.unknown", `Unknown event Lagrange target "${reference.primary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3545
+ } else if (reference.secondary && !objectMap.has(reference.secondary)) {
3546
+ diagnostics.push(error("validate.event.pose.lagrange.secondary.unknown", `Unknown event Lagrange target "${reference.secondary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3547
+ }
3548
+ }
3549
+ }
3550
+ }
3551
+ function validateAtTarget(object, objectMap, diagnostics) {
3552
+ const reference = object.placement?.mode === "at" ? object.placement.reference : null;
3553
+ if (!reference) {
3554
+ return true;
3555
+ }
3556
+ if (reference.kind === "named") {
3557
+ return objectMap.has(reference.name);
3558
+ }
3559
+ if (reference.kind === "anchor") {
3560
+ if (!objectMap.has(reference.objectId)) {
3561
+ diagnostics.push(error("validate.anchor.target.unknown", `Unknown anchor target "${reference.objectId}" on "${object.id}".`, object.id, "at"));
3562
+ return false;
3563
+ }
3564
+ return true;
3565
+ }
3566
+ if (!objectMap.has(reference.primary)) {
3567
+ diagnostics.push(error("validate.lagrange.primary.unknown", `Unknown Lagrange reference "${reference.primary}" on "${object.id}".`, object.id, "at"));
3568
+ return false;
3569
+ }
3570
+ if (reference.secondary && !objectMap.has(reference.secondary)) {
3571
+ diagnostics.push(error("validate.lagrange.secondary.unknown", `Unknown Lagrange reference "${reference.secondary}" on "${object.id}".`, object.id, "at"));
3572
+ return false;
3573
+ }
3574
+ return true;
3575
+ }
3576
+ function keplerPeriodDays(object, parentObject) {
3577
+ const placement = object.placement;
3578
+ if (!placement || placement.mode !== "orbit") {
3579
+ return null;
3580
+ }
3581
+ const semiMajorAu = distanceInAu(placement.semiMajor ?? placement.distance);
3582
+ const centralMassSolar = massInSolar(parentObject?.properties.mass);
3583
+ if (semiMajorAu === null || centralMassSolar === null || centralMassSolar <= 0) {
3584
+ return null;
3585
+ }
3586
+ const periodYears = Math.sqrt(semiMajorAu ** 3 / centralMassSolar);
3587
+ return periodYears * 365.25;
3588
+ }
3589
+ function distanceInAu(value) {
3590
+ if (!value)
3591
+ return null;
3592
+ switch (value.unit) {
3593
+ case null:
3594
+ case "au":
3595
+ return value.value;
3596
+ case "km":
3597
+ return value.value / AU_IN_KM2;
3598
+ case "m":
3599
+ return value.value / (AU_IN_KM2 * 1e3);
3600
+ case "ly":
3601
+ return value.value * LY_IN_AU2;
3602
+ case "pc":
3603
+ return value.value * PC_IN_AU2;
3604
+ case "kpc":
3605
+ return value.value * KPC_IN_AU2;
3606
+ case "re":
3607
+ return value.value * EARTH_RADIUS_IN_KM2 / AU_IN_KM2;
3608
+ case "sol":
3609
+ return value.value * SOLAR_RADIUS_IN_KM2 / AU_IN_KM2;
3610
+ default:
3611
+ return null;
3612
+ }
3613
+ }
3614
+ function massInSolar(value) {
3615
+ if (!value || typeof value !== "object" || !("value" in value)) {
3616
+ return null;
3617
+ }
3618
+ const unitValue = value;
3619
+ switch (unitValue.unit) {
3620
+ case null:
3621
+ case "sol":
3622
+ return unitValue.value;
3623
+ case "me":
3624
+ return unitValue.value / EARTH_MASSES_PER_SOLAR;
3625
+ case "mj":
3626
+ return unitValue.value / JUPITER_MASSES_PER_SOLAR;
3627
+ default:
3628
+ return null;
3629
+ }
3630
+ }
3631
+ function durationInDays(value) {
3632
+ if (!value)
3633
+ return null;
3634
+ switch (value.unit) {
3635
+ case null:
3636
+ case "d":
3637
+ return value.value;
3638
+ case "s":
3639
+ return value.value / 86400;
3640
+ case "min":
3641
+ return value.value / 1440;
3642
+ case "h":
3643
+ return value.value / 24;
3644
+ case "y":
3645
+ return value.value * 365.25;
3646
+ case "ky":
3647
+ return value.value * 365250;
3648
+ case "my":
3649
+ return value.value * 36525e4;
3650
+ case "gy":
3651
+ return value.value * 36525e7;
3652
+ default:
3653
+ return null;
3654
+ }
3655
+ }
3656
+ function toleranceForField(object, field) {
3657
+ const tolerance = object.tolerances?.find((entry) => entry.field === field)?.value;
3658
+ if (typeof tolerance === "number") {
3659
+ return tolerance;
3660
+ }
3661
+ if (tolerance && typeof tolerance === "object" && "value" in tolerance) {
3662
+ return durationInDays(tolerance) ?? 0;
3663
+ }
3664
+ return 0;
3665
+ }
3666
+ function formatDays(days) {
3667
+ return `${Math.round(days * 100) / 100}d`;
3668
+ }
3669
+ function error(code, message, objectId, field) {
3670
+ return { code, severity: "error", source: "validate", message, objectId, field };
3671
+ }
3672
+ function warn(code, message, objectId, field) {
3673
+ return { code, severity: "warning", source: "validate", message, objectId, field };
3674
+ }
3675
+ function info(code, message, objectId, field) {
3676
+ return { code, severity: "info", source: "validate", message, objectId, field };
3677
+ }
3678
+
3679
+ // packages/core/dist/draft-parse.js
3680
+ var STRUCTURED_TYPED_BLOCKS = /* @__PURE__ */ new Set([
3681
+ "climate",
3682
+ "habitability",
3683
+ "settlement"
3684
+ ]);
3685
+ var DRAFT_OBJECT_FIELD_SPECS = /* @__PURE__ */ new Map();
3686
+ for (const key of [
3687
+ "orbit",
3688
+ "distance",
3689
+ "semiMajor",
3690
+ "eccentricity",
3691
+ "period",
3692
+ "angle",
3693
+ "inclination",
3694
+ "phase",
3695
+ "at",
3696
+ "surface",
3697
+ "free",
3698
+ "kind",
3699
+ "class",
3700
+ "culture",
3701
+ "tags",
3702
+ "color",
3703
+ "image",
3704
+ "hidden",
3705
+ "radius",
3706
+ "mass",
3707
+ "density",
3708
+ "gravity",
3709
+ "temperature",
3710
+ "albedo",
3711
+ "atmosphere",
3712
+ "inner",
3713
+ "outer",
3714
+ "on",
3715
+ "source",
3716
+ "cycle"
3717
+ ]) {
3718
+ const schema = getFieldSchema(key);
3719
+ if (schema) {
3720
+ DRAFT_OBJECT_FIELD_SPECS.set(key, {
3721
+ key,
3722
+ version: "2.0",
3723
+ inlineMode: schema.arity === "multiple" ? "multiple" : "single",
3724
+ allowRepeat: false,
3725
+ legacySchema: schema
3726
+ });
3727
+ }
3728
+ }
3729
+ for (const spec of [
3730
+ { key: "groups", inlineMode: "multiple", allowRepeat: false },
3731
+ { key: "epoch", inlineMode: "single", allowRepeat: false },
3732
+ { key: "referencePlane", inlineMode: "single", allowRepeat: false },
3733
+ { key: "tidalLock", inlineMode: "single", allowRepeat: false },
3734
+ { key: "renderLabel", inlineMode: "single", allowRepeat: false },
3735
+ { key: "renderOrbit", inlineMode: "single", allowRepeat: false },
3736
+ { key: "renderPriority", inlineMode: "single", allowRepeat: false },
3737
+ { key: "resonance", inlineMode: "pair", allowRepeat: false },
3738
+ { key: "derive", inlineMode: "pair", allowRepeat: true },
3739
+ { key: "validate", inlineMode: "single", allowRepeat: true },
3740
+ { key: "locked", inlineMode: "multiple", allowRepeat: false },
3741
+ { key: "tolerance", inlineMode: "pair", allowRepeat: true }
3742
+ ]) {
3743
+ DRAFT_OBJECT_FIELD_SPECS.set(spec.key, {
3744
+ key: spec.key,
3745
+ version: "2.1",
3746
+ inlineMode: spec.inlineMode,
3747
+ allowRepeat: spec.allowRepeat
3748
+ });
3749
+ }
3750
+ var DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
3751
+ var EVENT_POSE_FIELD_KEYS = /* @__PURE__ */ new Set([
3752
+ "orbit",
3753
+ "distance",
3754
+ "semiMajor",
3755
+ "eccentricity",
3756
+ "period",
3757
+ "angle",
3758
+ "inclination",
3759
+ "phase",
3760
+ "at",
3761
+ "surface",
3762
+ "free",
3763
+ "inner",
3764
+ "outer"
3765
+ ]);
3766
+ function parseWorldOrbitAtlas(source) {
3767
+ return parseAtlasSource(source);
3768
+ }
3769
+ function parseAtlasSource(source, forcedOutputVersion) {
3770
+ const prepared = preprocessAtlasSource(source);
3771
+ const lines = prepared.source.split(/\r?\n/);
3772
+ const diagnostics = [];
3773
+ let sawSchemaHeader = false;
3774
+ let sourceSchemaVersion = "2.0";
3775
+ let system = null;
3776
+ let section = null;
3777
+ const objectNodes = [];
3778
+ const groups = [];
3779
+ const relations = [];
3780
+ const events = [];
3781
+ const eventPoseNodes = /* @__PURE__ */ new Map();
3782
+ let sawDefaults = false;
3783
+ let sawAtlas = false;
3784
+ const viewpointIds = /* @__PURE__ */ new Set();
3785
+ const annotationIds = /* @__PURE__ */ new Set();
3786
+ const groupIds = /* @__PURE__ */ new Set();
3787
+ const relationIds = /* @__PURE__ */ new Set();
3788
+ const eventIds = /* @__PURE__ */ new Set();
3789
+ for (let index = 0; index < lines.length; index++) {
3790
+ const rawLine = lines[index];
3791
+ const lineNumber = index + 1;
3792
+ if (!rawLine.trim()) {
3793
+ continue;
3794
+ }
3795
+ const indent = getIndent(rawLine);
3796
+ const tokens = tokenizeLineDetailed(rawLine.slice(indent), {
3797
+ line: lineNumber,
3798
+ columnOffset: indent
3799
+ });
3800
+ if (tokens.length === 0) {
3801
+ continue;
3802
+ }
3803
+ if (!sawSchemaHeader) {
3804
+ sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
3805
+ sawSchemaHeader = true;
3806
+ if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
3807
+ diagnostics.push({
3808
+ code: "parse.schema21.commentCompatibility",
3809
+ severity: "warning",
3810
+ source: "parse",
3811
+ message: `Comments require schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
3812
+ line: prepared.comments[0].line,
3813
+ column: prepared.comments[0].column
3814
+ });
3815
+ }
3816
+ continue;
3817
+ }
3818
+ if (indent === 0) {
3819
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, { sawDefaults, sawAtlas });
3820
+ if (section.kind === "system") {
3821
+ system = section.system;
3822
+ } else if (section.kind === "defaults") {
3823
+ sawDefaults = true;
3824
+ } else if (section.kind === "atlas") {
3825
+ sawAtlas = true;
3826
+ }
3827
+ continue;
3828
+ }
3829
+ if (!section) {
3830
+ throw new WorldOrbitError("Indented line without parent atlas section", lineNumber, indent + 1);
3831
+ }
3832
+ handleSectionLine(section, indent, tokens, lineNumber);
3833
+ }
3834
+ if (!sawSchemaHeader) {
3835
+ throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
3836
+ }
3837
+ const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
3838
+ const normalizedEvents = events.map((event) => normalizeDraftEvent(event, eventPoseNodes.get(event.id) ?? []));
3839
+ const outputVersion = forcedOutputVersion ?? (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
3840
+ const baseDocument = {
3841
+ format: "worldorbit",
3842
+ sourceVersion: "1.0",
3843
+ system,
3844
+ groups,
3845
+ relations,
3846
+ events: normalizedEvents,
3847
+ objects,
3848
+ diagnostics
3849
+ };
3850
+ if (outputVersion === "2.0-draft") {
3851
+ const document3 = {
3852
+ ...baseDocument,
3853
+ version: "2.0-draft",
3854
+ schemaVersion: "2.0-draft"
3855
+ };
3856
+ document3.diagnostics.push(...collectAtlasDiagnostics(document3, sourceSchemaVersion));
3857
+ return document3;
3858
+ }
3859
+ const document2 = {
3860
+ ...baseDocument,
3861
+ version: outputVersion,
3862
+ schemaVersion: outputVersion
3863
+ };
3864
+ if (sourceSchemaVersion === "2.0-draft") {
3865
+ document2.diagnostics.push({
3866
+ code: "load.schema.deprecatedDraft",
3867
+ severity: "warning",
3868
+ source: "upgrade",
3869
+ message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".'
3870
+ });
3871
+ }
3872
+ document2.diagnostics.push(...collectAtlasDiagnostics(document2, sourceSchemaVersion));
3873
+ return document2;
3874
+ }
3875
+ function assertDraftSchemaHeader(tokens, line) {
3876
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
3877
+ throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
3878
+ }
3879
+ const version = tokens[1].value.toLowerCase();
3880
+ return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
3881
+ }
3882
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, flags) {
3883
+ const keyword = tokens[0]?.value.toLowerCase();
3884
+ switch (keyword) {
3885
+ case "system":
3886
+ if (system) {
3887
+ throw new WorldOrbitError('Atlas section "system" may only appear once', line, tokens[0].column);
3888
+ }
3889
+ return startSystemSection(tokens, line, sourceSchemaVersion, diagnostics);
3890
+ case "defaults":
3891
+ if (!system) {
3892
+ throw new WorldOrbitError('Atlas section "defaults" requires a preceding system declaration', line, tokens[0].column);
3893
+ }
3894
+ if (flags.sawDefaults) {
3895
+ throw new WorldOrbitError('Atlas section "defaults" may only appear once', line, tokens[0].column);
3896
+ }
3897
+ return {
3898
+ kind: "defaults",
3899
+ system,
3900
+ seenFields: /* @__PURE__ */ new Set()
3901
+ };
3902
+ case "atlas":
3903
+ if (!system) {
3904
+ throw new WorldOrbitError('Atlas section "atlas" requires a preceding system declaration', line, tokens[0].column);
3905
+ }
3906
+ if (flags.sawAtlas) {
3907
+ throw new WorldOrbitError('Atlas section "atlas" may only appear once', line, tokens[0].column);
3908
+ }
3909
+ return {
3910
+ kind: "atlas",
3911
+ system,
3912
+ inMetadata: false,
3913
+ metadataIndent: null
3914
+ };
3915
+ case "viewpoint":
3916
+ if (!system) {
3917
+ throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
3918
+ }
3919
+ return startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics);
3920
+ case "annotation":
3921
+ if (!system) {
3922
+ throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
3923
+ }
3924
+ return startAnnotationSection(tokens, line, system, annotationIds);
3925
+ case "group":
3926
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "group", { line, column: tokens[0].column });
3927
+ return startGroupSection(tokens, line, groups, groupIds);
3928
+ case "relation":
3929
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
3930
+ return startRelationSection(tokens, line, relations, relationIds);
3931
+ case "event":
3932
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "event", { line, column: tokens[0].column });
3933
+ return startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics);
3934
+ case "object":
3935
+ return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
3936
+ default:
3937
+ throw new WorldOrbitError(`Unknown atlas section "${tokens[0]?.value ?? ""}"`, line, tokens[0]?.column ?? 1);
3938
+ }
3939
+ }
3940
+ function startSystemSection(tokens, line, sourceSchemaVersion, diagnostics) {
3941
+ if (tokens.length !== 2) {
3942
+ throw new WorldOrbitError("Invalid atlas system declaration", line, tokens[0]?.column ?? 1);
3943
+ }
3944
+ const system = {
3945
+ type: "system",
3946
+ id: tokens[1].value,
3947
+ title: null,
3948
+ description: null,
3949
+ epoch: null,
3950
+ referencePlane: null,
3951
+ defaults: {
3952
+ view: "topdown",
3953
+ scale: null,
3954
+ units: null,
3955
+ preset: null,
3956
+ theme: null
3957
+ },
3958
+ atlasMetadata: {},
3959
+ viewpoints: [],
2946
3960
  annotations: []
2947
3961
  };
2948
3962
  return {
2949
3963
  kind: "system",
2950
3964
  system,
3965
+ sourceSchemaVersion,
3966
+ diagnostics,
2951
3967
  seenFields: /* @__PURE__ */ new Set()
2952
3968
  };
2953
3969
  }
2954
- function startViewpointSection(tokens, line, system, viewpointIds) {
3970
+ function startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics) {
2955
3971
  if (tokens.length !== 2) {
2956
3972
  throw new WorldOrbitError("Invalid viewpoint declaration", line, tokens[0]?.column ?? 1);
2957
3973
  }
@@ -2968,6 +3984,7 @@ var WorldOrbit = (() => {
2968
3984
  summary: "",
2969
3985
  focusObjectId: null,
2970
3986
  selectedObjectId: null,
3987
+ events: [],
2971
3988
  projection: system.defaults.view,
2972
3989
  preset: system.defaults.preset,
2973
3990
  zoom: null,
@@ -2980,6 +3997,8 @@ var WorldOrbit = (() => {
2980
3997
  return {
2981
3998
  kind: "viewpoint",
2982
3999
  viewpoint,
4000
+ sourceSchemaVersion,
4001
+ diagnostics,
2983
4002
  seenFields: /* @__PURE__ */ new Set(),
2984
4003
  inFilter: false,
2985
4004
  filterIndent: null,
@@ -3013,7 +4032,107 @@ var WorldOrbit = (() => {
3013
4032
  seenFields: /* @__PURE__ */ new Set()
3014
4033
  };
3015
4034
  }
3016
- function startObjectSection(tokens, line, objectNodes) {
4035
+ function startGroupSection(tokens, line, groups, groupIds) {
4036
+ if (tokens.length !== 2) {
4037
+ throw new WorldOrbitError("Invalid group declaration", line, tokens[0]?.column ?? 1);
4038
+ }
4039
+ const id = normalizeIdentifier(tokens[1].value);
4040
+ if (!id) {
4041
+ throw new WorldOrbitError("Group id must not be empty", line, tokens[1].column);
4042
+ }
4043
+ if (groupIds.has(id)) {
4044
+ throw new WorldOrbitError(`Duplicate group id "${id}"`, line, tokens[1].column);
4045
+ }
4046
+ const group = {
4047
+ id,
4048
+ label: humanizeIdentifier2(id),
4049
+ summary: "",
4050
+ color: null,
4051
+ tags: [],
4052
+ hidden: false
4053
+ };
4054
+ groups.push(group);
4055
+ groupIds.add(id);
4056
+ return {
4057
+ kind: "group",
4058
+ group,
4059
+ seenFields: /* @__PURE__ */ new Set()
4060
+ };
4061
+ }
4062
+ function startRelationSection(tokens, line, relations, relationIds) {
4063
+ if (tokens.length !== 2) {
4064
+ throw new WorldOrbitError("Invalid relation declaration", line, tokens[0]?.column ?? 1);
4065
+ }
4066
+ const id = normalizeIdentifier(tokens[1].value);
4067
+ if (!id) {
4068
+ throw new WorldOrbitError("Relation id must not be empty", line, tokens[1].column);
4069
+ }
4070
+ if (relationIds.has(id)) {
4071
+ throw new WorldOrbitError(`Duplicate relation id "${id}"`, line, tokens[1].column);
4072
+ }
4073
+ const relation = {
4074
+ id,
4075
+ from: "",
4076
+ to: "",
4077
+ kind: "",
4078
+ label: null,
4079
+ summary: null,
4080
+ tags: [],
4081
+ color: null,
4082
+ hidden: false
4083
+ };
4084
+ relations.push(relation);
4085
+ relationIds.add(id);
4086
+ return {
4087
+ kind: "relation",
4088
+ relation,
4089
+ seenFields: /* @__PURE__ */ new Set()
4090
+ };
4091
+ }
4092
+ function startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics) {
4093
+ if (tokens.length !== 2) {
4094
+ throw new WorldOrbitError("Invalid event declaration", line, tokens[0]?.column ?? 1);
4095
+ }
4096
+ const id = normalizeIdentifier(tokens[1].value);
4097
+ if (!id) {
4098
+ throw new WorldOrbitError("Event id must not be empty", line, tokens[1].column);
4099
+ }
4100
+ if (eventIds.has(id)) {
4101
+ throw new WorldOrbitError(`Duplicate event id "${id}"`, line, tokens[1].column);
4102
+ }
4103
+ const event = {
4104
+ id,
4105
+ kind: "",
4106
+ label: humanizeIdentifier2(id),
4107
+ summary: null,
4108
+ targetObjectId: null,
4109
+ participantObjectIds: [],
4110
+ timing: null,
4111
+ visibility: null,
4112
+ tags: [],
4113
+ color: null,
4114
+ hidden: false,
4115
+ positions: []
4116
+ };
4117
+ const rawPoses = [];
4118
+ events.push(event);
4119
+ eventPoseNodes.set(id, rawPoses);
4120
+ eventIds.add(id);
4121
+ return {
4122
+ kind: "event",
4123
+ event,
4124
+ sourceSchemaVersion,
4125
+ diagnostics,
4126
+ seenFields: /* @__PURE__ */ new Set(),
4127
+ rawPoses,
4128
+ inPositions: false,
4129
+ positionsIndent: null,
4130
+ activePose: null,
4131
+ poseIndent: null,
4132
+ activePoseSeenFields: /* @__PURE__ */ new Set()
4133
+ };
4134
+ }
4135
+ function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
3017
4136
  if (tokens.length < 3) {
3018
4137
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
3019
4138
  }
@@ -3024,12 +4143,11 @@ var WorldOrbit = (() => {
3024
4143
  throw new WorldOrbitError(`Unknown object type "${objectTypeToken.value}"`, line, objectTypeToken.column);
3025
4144
  }
3026
4145
  const objectNode = {
3027
- type: "object",
3028
4146
  objectType,
3029
- name: idToken.value,
3030
- inlineFields: parseInlineFields2(tokens.slice(3), line),
3031
- blockFields: [],
4147
+ id: idToken.value,
4148
+ fields: parseInlineObjectFields(tokens.slice(3), line, objectType, sourceSchemaVersion, diagnostics),
3032
4149
  infoEntries: [],
4150
+ typedBlockEntries: {},
3033
4151
  location: {
3034
4152
  line,
3035
4153
  column: objectTypeToken.column
@@ -3039,8 +4157,12 @@ var WorldOrbit = (() => {
3039
4157
  return {
3040
4158
  kind: "object",
3041
4159
  objectNode,
3042
- inInfoBlock: false,
3043
- infoIndent: null
4160
+ sourceSchemaVersion,
4161
+ diagnostics,
4162
+ activeBlock: null,
4163
+ blockIndent: null,
4164
+ seenInfoKeys: /* @__PURE__ */ new Set(),
4165
+ seenTypedBlockKeys: {}
3044
4166
  };
3045
4167
  }
3046
4168
  function handleSectionLine(section, indent, tokens, line) {
@@ -3060,6 +4182,15 @@ var WorldOrbit = (() => {
3060
4182
  case "annotation":
3061
4183
  applyAnnotationField(section, tokens, line);
3062
4184
  return;
4185
+ case "group":
4186
+ applyGroupField(section, tokens, line);
4187
+ return;
4188
+ case "relation":
4189
+ applyRelationField(section, tokens, line);
4190
+ return;
4191
+ case "event":
4192
+ applyEventField(section, indent, tokens, line);
4193
+ return;
3063
4194
  case "object":
3064
4195
  applyObjectField(section, indent, tokens, line);
3065
4196
  return;
@@ -3067,10 +4198,35 @@ var WorldOrbit = (() => {
3067
4198
  }
3068
4199
  function applySystemField(section, tokens, line) {
3069
4200
  const key = requireUniqueField(tokens, section.seenFields, line);
3070
- if (key !== "title") {
3071
- throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
4201
+ const value = joinFieldValue(tokens, line);
4202
+ switch (key) {
4203
+ case "title":
4204
+ section.system.title = value;
4205
+ return;
4206
+ case "description":
4207
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
4208
+ line,
4209
+ column: tokens[0].column
4210
+ });
4211
+ section.system.description = value;
4212
+ return;
4213
+ case "epoch":
4214
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
4215
+ line,
4216
+ column: tokens[0].column
4217
+ });
4218
+ section.system.epoch = value;
4219
+ return;
4220
+ case "referenceplane":
4221
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "referencePlane", {
4222
+ line,
4223
+ column: tokens[0].column
4224
+ });
4225
+ section.system.referencePlane = value;
4226
+ return;
4227
+ default:
4228
+ throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
3072
4229
  }
3073
- section.system.title = joinFieldValue(tokens, line);
3074
4230
  }
3075
4231
  function applyDefaultsField(section, tokens, line) {
3076
4232
  const key = requireUniqueField(tokens, section.seenFields, line);
@@ -3101,14 +4257,11 @@ var WorldOrbit = (() => {
3101
4257
  section.metadataIndent = null;
3102
4258
  }
3103
4259
  if (section.inMetadata) {
3104
- if (tokens.length < 2) {
3105
- throw new WorldOrbitError("Invalid atlas metadata entry", line, tokens[0]?.column ?? 1);
3106
- }
3107
- const key = tokens[0].value;
3108
- if (key in section.system.atlasMetadata) {
3109
- throw new WorldOrbitError(`Duplicate atlas metadata key "${key}"`, line, tokens[0].column);
4260
+ const entry = parseInfoLikeEntry(tokens, line, "Invalid atlas metadata entry");
4261
+ if (entry.key in section.system.atlasMetadata) {
4262
+ throw new WorldOrbitError(`Duplicate atlas metadata key "${entry.key}"`, line, tokens[0].column);
3110
4263
  }
3111
- section.system.atlasMetadata[key] = joinFieldValue(tokens, line);
4264
+ section.system.atlasMetadata[entry.key] = entry.value;
3112
4265
  return;
3113
4266
  }
3114
4267
  if (tokens.length === 1 && tokens[0].value.toLowerCase() === "metadata") {
@@ -3164,7 +4317,14 @@ var WorldOrbit = (() => {
3164
4317
  section.viewpoint.rotationDeg = parseFiniteNumber2(value, line, tokens[0].column, "rotation");
3165
4318
  return;
3166
4319
  case "layers":
3167
- section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line);
4320
+ section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line, section.sourceSchemaVersion, section.diagnostics);
4321
+ return;
4322
+ case "events":
4323
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.events", {
4324
+ line,
4325
+ column: tokens[0].column
4326
+ });
4327
+ section.viewpoint.events = parseTokenList(tokens.slice(1), line, "events");
3168
4328
  return;
3169
4329
  default:
3170
4330
  throw new WorldOrbitError(`Unknown viewpoint field "${tokens[0].value}"`, line, tokens[0].column);
@@ -3210,21 +4370,202 @@ var WorldOrbit = (() => {
3210
4370
  throw new WorldOrbitError(`Unknown annotation field "${tokens[0].value}"`, line, tokens[0].column);
3211
4371
  }
3212
4372
  }
4373
+ function applyGroupField(section, tokens, line) {
4374
+ const key = requireUniqueField(tokens, section.seenFields, line);
4375
+ switch (key) {
4376
+ case "label":
4377
+ section.group.label = joinFieldValue(tokens, line);
4378
+ return;
4379
+ case "summary":
4380
+ section.group.summary = joinFieldValue(tokens, line);
4381
+ return;
4382
+ case "color":
4383
+ section.group.color = joinFieldValue(tokens, line);
4384
+ return;
4385
+ case "tags":
4386
+ section.group.tags = parseTokenList(tokens.slice(1), line, "tags");
4387
+ return;
4388
+ case "hidden":
4389
+ section.group.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4390
+ line,
4391
+ column: tokens[0].column
4392
+ });
4393
+ return;
4394
+ default:
4395
+ throw new WorldOrbitError(`Unknown group field "${tokens[0].value}"`, line, tokens[0].column);
4396
+ }
4397
+ }
4398
+ function applyRelationField(section, tokens, line) {
4399
+ const key = requireUniqueField(tokens, section.seenFields, line);
4400
+ switch (key) {
4401
+ case "from":
4402
+ section.relation.from = joinFieldValue(tokens, line);
4403
+ return;
4404
+ case "to":
4405
+ section.relation.to = joinFieldValue(tokens, line);
4406
+ return;
4407
+ case "kind":
4408
+ section.relation.kind = joinFieldValue(tokens, line);
4409
+ return;
4410
+ case "label":
4411
+ section.relation.label = joinFieldValue(tokens, line);
4412
+ return;
4413
+ case "summary":
4414
+ section.relation.summary = joinFieldValue(tokens, line);
4415
+ return;
4416
+ case "tags":
4417
+ section.relation.tags = parseTokenList(tokens.slice(1), line, "tags");
4418
+ return;
4419
+ case "color":
4420
+ section.relation.color = joinFieldValue(tokens, line);
4421
+ return;
4422
+ case "hidden":
4423
+ section.relation.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4424
+ line,
4425
+ column: tokens[0].column
4426
+ });
4427
+ return;
4428
+ default:
4429
+ throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
4430
+ }
4431
+ }
4432
+ function applyEventField(section, indent, tokens, line) {
4433
+ if (section.activePose && indent <= (section.poseIndent ?? 0)) {
4434
+ section.activePose = null;
4435
+ section.poseIndent = null;
4436
+ section.activePoseSeenFields.clear();
4437
+ }
4438
+ if (!section.activePose && section.inPositions && indent <= (section.positionsIndent ?? 0)) {
4439
+ section.inPositions = false;
4440
+ section.positionsIndent = null;
4441
+ }
4442
+ if (section.activePose) {
4443
+ section.activePose.fields.push(parseEventPoseField(tokens, line, section.activePoseSeenFields));
4444
+ return;
4445
+ }
4446
+ if (section.inPositions) {
4447
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "pose") {
4448
+ throw new WorldOrbitError(`Unknown event positions field "${tokens[0].value}"`, line, tokens[0]?.column ?? 1);
4449
+ }
4450
+ const objectId = tokens[1].value;
4451
+ if (!objectId.trim()) {
4452
+ throw new WorldOrbitError("Event pose object id must not be empty", line, tokens[1].column);
4453
+ }
4454
+ const rawPose = {
4455
+ objectId,
4456
+ fields: [],
4457
+ location: { line, column: tokens[0].column }
4458
+ };
4459
+ section.rawPoses.push(rawPose);
4460
+ section.activePose = rawPose;
4461
+ section.poseIndent = indent;
4462
+ section.activePoseSeenFields = /* @__PURE__ */ new Set();
4463
+ return;
4464
+ }
4465
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "positions") {
4466
+ if (section.seenFields.has("positions")) {
4467
+ throw new WorldOrbitError('Duplicate event field "positions"', line, tokens[0].column);
4468
+ }
4469
+ section.seenFields.add("positions");
4470
+ section.inPositions = true;
4471
+ section.positionsIndent = indent;
4472
+ return;
4473
+ }
4474
+ const key = requireUniqueField(tokens, section.seenFields, line);
4475
+ switch (key) {
4476
+ case "kind":
4477
+ section.event.kind = joinFieldValue(tokens, line);
4478
+ return;
4479
+ case "label":
4480
+ section.event.label = joinFieldValue(tokens, line);
4481
+ return;
4482
+ case "summary":
4483
+ section.event.summary = joinFieldValue(tokens, line);
4484
+ return;
4485
+ case "target":
4486
+ section.event.targetObjectId = joinFieldValue(tokens, line);
4487
+ return;
4488
+ case "participants":
4489
+ section.event.participantObjectIds = parseTokenList(tokens.slice(1), line, "participants");
4490
+ return;
4491
+ case "timing":
4492
+ section.event.timing = joinFieldValue(tokens, line);
4493
+ return;
4494
+ case "visibility":
4495
+ section.event.visibility = joinFieldValue(tokens, line);
4496
+ return;
4497
+ case "tags":
4498
+ section.event.tags = parseTokenList(tokens.slice(1), line, "tags");
4499
+ return;
4500
+ case "color":
4501
+ section.event.color = joinFieldValue(tokens, line);
4502
+ return;
4503
+ case "hidden":
4504
+ section.event.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4505
+ line,
4506
+ column: tokens[0].column
4507
+ });
4508
+ return;
4509
+ default:
4510
+ throw new WorldOrbitError(`Unknown event field "${tokens[0].value}"`, line, tokens[0].column);
4511
+ }
4512
+ }
4513
+ function parseEventPoseField(tokens, line, seenFields) {
4514
+ if (tokens.length < 2) {
4515
+ throw new WorldOrbitError("Invalid event pose field line", line, tokens[0]?.column ?? 1);
4516
+ }
4517
+ const key = tokens[0].value;
4518
+ if (!EVENT_POSE_FIELD_KEYS.has(key)) {
4519
+ throw new WorldOrbitError(`Unknown event pose field "${key}"`, line, tokens[0].column);
4520
+ }
4521
+ if (seenFields.has(key)) {
4522
+ throw new WorldOrbitError(`Duplicate event pose field "${key}"`, line, tokens[0].column);
4523
+ }
4524
+ seenFields.add(key);
4525
+ return {
4526
+ type: "field",
4527
+ key,
4528
+ values: tokens.slice(1).map((token) => token.value),
4529
+ location: { line, column: tokens[0].column }
4530
+ };
4531
+ }
3213
4532
  function applyObjectField(section, indent, tokens, line) {
3214
- if (tokens.length === 1 && tokens[0].value === "info") {
3215
- section.inInfoBlock = true;
3216
- section.infoIndent = indent;
3217
- return;
3218
- }
3219
- if (section.inInfoBlock && indent <= (section.infoIndent ?? 0)) {
3220
- section.inInfoBlock = false;
3221
- section.infoIndent = null;
4533
+ if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
4534
+ section.activeBlock = null;
4535
+ section.blockIndent = null;
4536
+ }
4537
+ if (tokens.length === 1) {
4538
+ const blockName = tokens[0].value.toLowerCase();
4539
+ if (blockName === "info" || STRUCTURED_TYPED_BLOCKS.has(blockName)) {
4540
+ if (blockName !== "info") {
4541
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, blockName, { line, column: tokens[0].column });
4542
+ }
4543
+ section.activeBlock = blockName;
4544
+ section.blockIndent = indent;
4545
+ return;
4546
+ }
3222
4547
  }
3223
- if (section.inInfoBlock) {
3224
- section.objectNode.infoEntries.push(parseInfoEntry2(tokens, line));
4548
+ if (section.activeBlock) {
4549
+ const entry = parseInfoLikeEntry(tokens, line, `Invalid ${section.activeBlock} entry`);
4550
+ if (section.activeBlock === "info") {
4551
+ if (section.seenInfoKeys.has(entry.key)) {
4552
+ throw new WorldOrbitError(`Duplicate info key "${entry.key}"`, line, tokens[0].column);
4553
+ }
4554
+ section.seenInfoKeys.add(entry.key);
4555
+ section.objectNode.infoEntries.push(entry);
4556
+ return;
4557
+ }
4558
+ const typedBlock = section.activeBlock;
4559
+ const seenKeys = section.seenTypedBlockKeys[typedBlock] ?? (section.seenTypedBlockKeys[typedBlock] = /* @__PURE__ */ new Set());
4560
+ if (seenKeys.has(entry.key)) {
4561
+ throw new WorldOrbitError(`Duplicate ${typedBlock} key "${entry.key}"`, line, tokens[0].column);
4562
+ }
4563
+ seenKeys.add(entry.key);
4564
+ const entries = section.objectNode.typedBlockEntries[typedBlock] ?? (section.objectNode.typedBlockEntries[typedBlock] = []);
4565
+ entries.push(entry);
3225
4566
  return;
3226
4567
  }
3227
- section.objectNode.blockFields.push(parseField2(tokens, line));
4568
+ section.objectNode.fields.push(parseObjectField(tokens, line, section.objectNode.objectType, section.sourceSchemaVersion, section.diagnostics));
3228
4569
  }
3229
4570
  function requireUniqueField(tokens, seenFields, line) {
3230
4571
  if (tokens.length < 2) {
@@ -3244,50 +4585,46 @@ var WorldOrbit = (() => {
3244
4585
  return tokens.slice(1).map((token) => token.value).join(" ").trim();
3245
4586
  }
3246
4587
  function parseObjectTypeTokens(tokens, line) {
3247
- if (tokens.length === 0) {
3248
- throw new WorldOrbitError("Missing value for atlas field", line);
3249
- }
3250
- return tokens.map((token) => {
3251
- const value = token.value;
3252
- if (value !== "star" && value !== "planet" && value !== "moon" && value !== "belt" && value !== "asteroid" && value !== "comet" && value !== "ring" && value !== "structure" && value !== "phenomenon") {
3253
- throw new WorldOrbitError(`Unknown viewpoint object type "${token.value}"`, line, token.column);
3254
- }
3255
- return value;
3256
- });
4588
+ 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");
3257
4589
  }
3258
- function parseTokenList(tokens, line, field) {
3259
- if (tokens.length === 0) {
3260
- throw new WorldOrbitError(`Missing value for field "${field}"`, line);
4590
+ function parseLayerTokens(tokens, line, sourceSchemaVersion, diagnostics) {
4591
+ const layers = {};
4592
+ for (const token of parseTokenList(tokens, line, "layers")) {
4593
+ const enabled = !token.startsWith("-") && !token.startsWith("!");
4594
+ const raw = token.replace(/^[-!]+/, "").toLowerCase();
4595
+ if (raw === "orbits") {
4596
+ layers["orbits-back"] = enabled;
4597
+ layers["orbits-front"] = enabled;
4598
+ continue;
4599
+ }
4600
+ if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "events" || raw === "objects" || raw === "labels" || raw === "metadata") {
4601
+ if (raw === "events" && sourceSchemaVersion && diagnostics) {
4602
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "layers.events", {
4603
+ line,
4604
+ column: tokens[0]?.column ?? 1
4605
+ });
4606
+ }
4607
+ layers[raw] = enabled;
4608
+ }
3261
4609
  }
3262
- return tokens.map((token) => token.value);
4610
+ return layers;
3263
4611
  }
3264
- function parseLayerTokens(tokens, line) {
4612
+ function parseTokenList(tokens, line, fieldName) {
3265
4613
  if (tokens.length === 0) {
3266
- throw new WorldOrbitError('Missing value for field "layers"', line);
4614
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, 1);
3267
4615
  }
3268
- const next = {};
3269
- for (const token of tokens) {
3270
- const enabled = !token.value.startsWith("-") && !token.value.startsWith("!");
3271
- const rawLayer = token.value.replace(/^[-!]+/, "").toLowerCase();
3272
- if (rawLayer === "orbits") {
3273
- next["orbits-back"] = enabled;
3274
- next["orbits-front"] = enabled;
3275
- continue;
3276
- }
3277
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
3278
- next[rawLayer] = enabled;
3279
- continue;
3280
- }
3281
- throw new WorldOrbitError(`Unknown layer token "${token.value}"`, line, token.column);
4616
+ const values = tokens.map((token) => token.value).filter(Boolean);
4617
+ if (values.length === 0) {
4618
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, tokens[0]?.column ?? 1);
3282
4619
  }
3283
- return next;
4620
+ return values;
3284
4621
  }
3285
4622
  function parseProjectionValue(value, line, column) {
3286
4623
  const normalized = value.toLowerCase();
3287
- if (normalized === "topdown" || normalized === "isometric") {
3288
- return normalized;
4624
+ if (normalized !== "topdown" && normalized !== "isometric") {
4625
+ throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
3289
4626
  }
3290
- throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
4627
+ return normalized;
3291
4628
  }
3292
4629
  function parsePresetValue(value, line, column) {
3293
4630
  const normalized = value.toLowerCase();
@@ -3297,16 +4634,16 @@ var WorldOrbit = (() => {
3297
4634
  throw new WorldOrbitError(`Unknown render preset "${value}"`, line, column);
3298
4635
  }
3299
4636
  function parsePositiveNumber2(value, line, column, field) {
3300
- const parsed = Number(value);
3301
- if (!Number.isFinite(parsed) || parsed <= 0) {
3302
- throw new WorldOrbitError(`Field "${field}" expects a positive number`, line, column);
4637
+ const parsed = parseFiniteNumber2(value, line, column, field);
4638
+ if (parsed <= 0) {
4639
+ throw new WorldOrbitError(`Field "${field}" must be greater than zero`, line, column);
3303
4640
  }
3304
4641
  return parsed;
3305
4642
  }
3306
4643
  function parseFiniteNumber2(value, line, column, field) {
3307
4644
  const parsed = Number(value);
3308
4645
  if (!Number.isFinite(parsed)) {
3309
- throw new WorldOrbitError(`Field "${field}" expects a finite number`, line, column);
4646
+ throw new WorldOrbitError(`Invalid numeric value "${value}" for "${field}"`, line, column);
3310
4647
  }
3311
4648
  return parsed;
3312
4649
  }
@@ -3318,28 +4655,43 @@ var WorldOrbit = (() => {
3318
4655
  groupIds: []
3319
4656
  };
3320
4657
  }
3321
- function parseInlineFields2(tokens, line) {
4658
+ function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
3322
4659
  const fields = [];
3323
4660
  let index = 0;
3324
4661
  while (index < tokens.length) {
3325
4662
  const keyToken = tokens[index];
3326
- const schema = getFieldSchema(keyToken.value);
3327
- if (!schema) {
4663
+ const spec = getDraftObjectFieldSpec(keyToken.value);
4664
+ if (!spec) {
3328
4665
  throw new WorldOrbitError(`Unknown field "${keyToken.value}"`, line, keyToken.column);
3329
4666
  }
4667
+ if (spec.version === "2.1") {
4668
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, keyToken.value, {
4669
+ line,
4670
+ column: keyToken.column
4671
+ });
4672
+ }
3330
4673
  index++;
3331
4674
  const valueTokens = [];
3332
- if (schema.arity === "multiple") {
3333
- while (index < tokens.length && !isKnownFieldKey(tokens[index].value)) {
3334
- valueTokens.push(tokens[index]);
3335
- index++;
3336
- }
3337
- } else {
4675
+ if (spec.inlineMode === "single") {
3338
4676
  const nextToken = tokens[index];
3339
4677
  if (nextToken) {
3340
4678
  valueTokens.push(nextToken);
3341
4679
  index++;
3342
4680
  }
4681
+ } else if (spec.inlineMode === "pair") {
4682
+ for (let count = 0; count < 2; count++) {
4683
+ const nextToken = tokens[index];
4684
+ if (!nextToken) {
4685
+ break;
4686
+ }
4687
+ valueTokens.push(nextToken);
4688
+ index++;
4689
+ }
4690
+ } else {
4691
+ while (index < tokens.length && !DRAFT_OBJECT_FIELD_KEYS.has(tokens[index].value)) {
4692
+ valueTokens.push(tokens[index]);
4693
+ index++;
4694
+ }
3343
4695
  }
3344
4696
  if (valueTokens.length === 0) {
3345
4697
  throw new WorldOrbitError(`Missing value for field "${keyToken.value}"`, line, keyToken.column);
@@ -3351,25 +4703,35 @@ var WorldOrbit = (() => {
3351
4703
  location: { line, column: keyToken.column }
3352
4704
  });
3353
4705
  }
4706
+ validateDraftObjectFieldCompatibility(fields, objectType);
3354
4707
  return fields;
3355
4708
  }
3356
- function parseField2(tokens, line) {
4709
+ function parseObjectField(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
3357
4710
  if (tokens.length < 2) {
3358
4711
  throw new WorldOrbitError("Invalid field line", line, tokens[0]?.column ?? 1);
3359
4712
  }
3360
- if (!getFieldSchema(tokens[0].value)) {
4713
+ const spec = getDraftObjectFieldSpec(tokens[0].value);
4714
+ if (!spec) {
3361
4715
  throw new WorldOrbitError(`Unknown field "${tokens[0].value}"`, line, tokens[0].column);
3362
4716
  }
3363
- return {
4717
+ if (spec.version === "2.1") {
4718
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, tokens[0].value, {
4719
+ line,
4720
+ column: tokens[0].column
4721
+ });
4722
+ }
4723
+ const field = {
3364
4724
  type: "field",
3365
4725
  key: tokens[0].value,
3366
4726
  values: tokens.slice(1).map((token) => token.value),
3367
4727
  location: { line, column: tokens[0].column }
3368
4728
  };
4729
+ validateDraftObjectFieldCompatibility([field], objectType);
4730
+ return field;
3369
4731
  }
3370
- function parseInfoEntry2(tokens, line) {
4732
+ function parseInfoLikeEntry(tokens, line, errorMessage) {
3371
4733
  if (tokens.length < 2) {
3372
- throw new WorldOrbitError("Invalid info entry", line, tokens[0]?.column ?? 1);
4734
+ throw new WorldOrbitError(errorMessage, line, tokens[0]?.column ?? 1);
3373
4735
  }
3374
4736
  return {
3375
4737
  type: "info-entry",
@@ -3378,18 +4740,366 @@ var WorldOrbit = (() => {
3378
4740
  location: { line, column: tokens[0].column }
3379
4741
  };
3380
4742
  }
3381
- function normalizeIdentifier(value) {
3382
- return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
4743
+ function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
4744
+ const fieldMap = collectDraftFields(node.fields);
4745
+ const placement = extractPlacementFromFieldMap(fieldMap);
4746
+ const properties = normalizeDraftProperties(node.objectType, fieldMap);
4747
+ const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
4748
+ const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
4749
+ const referencePlane = parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0]);
4750
+ const tidalLock = fieldMap.has("tidalLock") ? parseAtlasBoolean(singleFieldValue2(fieldMap.get("tidalLock")[0]), "tidalLock", fieldMap.get("tidalLock")[0].location) : void 0;
4751
+ const resonance = fieldMap.has("resonance") ? parseResonanceField(fieldMap.get("resonance")[0]) : void 0;
4752
+ const renderHints = extractRenderHints(fieldMap);
4753
+ const deriveRules = fieldMap.get("derive")?.map((field) => parseDeriveField(field));
4754
+ const validationRules = fieldMap.get("validate")?.map((field) => ({
4755
+ rule: singleFieldValue2(field)
4756
+ }));
4757
+ const lockedFields = fieldMap.has("locked") ? [...new Set(fieldMap.get("locked").flatMap((field) => field.values))] : void 0;
4758
+ const tolerances = fieldMap.get("tolerance")?.map((field) => parseToleranceField(field));
4759
+ const typedBlocks = normalizeTypedBlocks(node.typedBlockEntries);
4760
+ const info2 = normalizeInfoEntries(node.infoEntries, "info");
4761
+ const object = {
4762
+ type: node.objectType,
4763
+ id: node.id,
4764
+ properties,
4765
+ placement,
4766
+ info: info2
4767
+ };
4768
+ if (groups.length > 0)
4769
+ object.groups = groups;
4770
+ if (epoch)
4771
+ object.epoch = epoch;
4772
+ if (referencePlane)
4773
+ object.referencePlane = referencePlane;
4774
+ if (tidalLock !== void 0)
4775
+ object.tidalLock = tidalLock;
4776
+ if (resonance)
4777
+ object.resonance = resonance;
4778
+ if (renderHints)
4779
+ object.renderHints = renderHints;
4780
+ if (deriveRules?.length)
4781
+ object.deriveRules = deriveRules;
4782
+ if (validationRules?.length)
4783
+ object.validationRules = validationRules;
4784
+ if (lockedFields?.length)
4785
+ object.lockedFields = lockedFields;
4786
+ if (tolerances?.length)
4787
+ object.tolerances = tolerances;
4788
+ if (typedBlocks && Object.keys(typedBlocks).length > 0)
4789
+ object.typedBlocks = typedBlocks;
4790
+ if (sourceSchemaVersion !== "2.1") {
4791
+ 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) {
4792
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
4793
+ }
4794
+ }
4795
+ return object;
4796
+ }
4797
+ function normalizeDraftEvent(event, rawPoses) {
4798
+ return {
4799
+ ...event,
4800
+ participantObjectIds: [...new Set(event.participantObjectIds)],
4801
+ tags: [...new Set(event.tags)],
4802
+ positions: rawPoses.map((pose) => normalizeDraftEventPose(pose))
4803
+ };
3383
4804
  }
3384
- function humanizeIdentifier2(value) {
3385
- return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
4805
+ function normalizeDraftEventPose(rawPose) {
4806
+ const fieldMap = collectDraftFields(rawPose.fields);
4807
+ const placement = extractPlacementFromFieldMap(fieldMap);
4808
+ return {
4809
+ objectId: rawPose.objectId,
4810
+ placement,
4811
+ inner: parseOptionalUnitField(fieldMap.get("inner")?.[0], "inner"),
4812
+ outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer")
4813
+ };
4814
+ }
4815
+ function collectDraftFields(fields) {
4816
+ const grouped = /* @__PURE__ */ new Map();
4817
+ for (const field of fields) {
4818
+ const spec = getDraftObjectFieldSpec(field.key);
4819
+ if (!spec) {
4820
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
4821
+ }
4822
+ if (!spec.allowRepeat && grouped.has(field.key)) {
4823
+ throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
4824
+ }
4825
+ const existing = grouped.get(field.key) ?? [];
4826
+ existing.push(field);
4827
+ grouped.set(field.key, existing);
4828
+ }
4829
+ return grouped;
4830
+ }
4831
+ function extractPlacementFromFieldMap(fieldMap) {
4832
+ const orbitField = fieldMap.get("orbit")?.[0];
4833
+ const atField = fieldMap.get("at")?.[0];
4834
+ const surfaceField = fieldMap.get("surface")?.[0];
4835
+ const freeField = fieldMap.get("free")?.[0];
4836
+ const count = [orbitField, atField, surfaceField, freeField].filter(Boolean).length;
4837
+ if (count > 1) {
4838
+ const conflictingField = orbitField ?? atField ?? surfaceField ?? freeField;
4839
+ throw WorldOrbitError.fromLocation("Object has multiple placement modes", conflictingField?.location);
4840
+ }
4841
+ if (orbitField) {
4842
+ return {
4843
+ mode: "orbit",
4844
+ target: singleFieldValue2(orbitField),
4845
+ distance: parseOptionalUnitField(fieldMap.get("distance")?.[0], "distance"),
4846
+ semiMajor: parseOptionalUnitField(fieldMap.get("semiMajor")?.[0], "semiMajor"),
4847
+ eccentricity: parseOptionalNumberField(fieldMap.get("eccentricity")?.[0], "eccentricity"),
4848
+ period: parseOptionalUnitField(fieldMap.get("period")?.[0], "period"),
4849
+ angle: parseOptionalUnitField(fieldMap.get("angle")?.[0], "angle"),
4850
+ inclination: parseOptionalUnitField(fieldMap.get("inclination")?.[0], "inclination"),
4851
+ phase: parseOptionalUnitField(fieldMap.get("phase")?.[0], "phase")
4852
+ };
4853
+ }
4854
+ if (atField) {
4855
+ const target = singleFieldValue2(atField);
4856
+ return {
4857
+ mode: "at",
4858
+ target,
4859
+ reference: parseAtlasAtReference(target, atField.location)
4860
+ };
4861
+ }
4862
+ if (surfaceField) {
4863
+ return {
4864
+ mode: "surface",
4865
+ target: singleFieldValue2(surfaceField)
4866
+ };
4867
+ }
4868
+ if (freeField) {
4869
+ const raw = singleFieldValue2(freeField);
4870
+ const distance = tryParseAtlasUnitValue(raw);
4871
+ return {
4872
+ mode: "free",
4873
+ distance: distance ?? void 0,
4874
+ descriptor: distance ? void 0 : raw
4875
+ };
4876
+ }
4877
+ return null;
4878
+ }
4879
+ function normalizeDraftProperties(objectType, fieldMap) {
4880
+ const properties = {};
4881
+ for (const [key, fields] of fieldMap.entries()) {
4882
+ const field = fields[0];
4883
+ const spec = getDraftObjectFieldSpec(key);
4884
+ if (!field || !spec?.legacySchema || spec.legacySchema.placement) {
4885
+ continue;
4886
+ }
4887
+ ensureAtlasFieldSupported(key, objectType, field.location);
4888
+ properties[key] = normalizeLegacyScalarValue(key, field.values, field.location);
4889
+ }
4890
+ return properties;
4891
+ }
4892
+ function normalizeInfoEntries(entries, label) {
4893
+ const normalized = {};
4894
+ for (const entry of entries) {
4895
+ if (entry.key in normalized) {
4896
+ throw WorldOrbitError.fromLocation(`Duplicate ${label} key "${entry.key}"`, entry.location);
4897
+ }
4898
+ normalized[entry.key] = entry.value;
4899
+ }
4900
+ return normalized;
4901
+ }
4902
+ function normalizeTypedBlocks(typedBlockEntries) {
4903
+ const typedBlocks = {};
4904
+ for (const blockName of Object.keys(typedBlockEntries)) {
4905
+ const entries = typedBlockEntries[blockName];
4906
+ if (entries?.length) {
4907
+ typedBlocks[blockName] = normalizeInfoEntries(entries, blockName);
4908
+ }
4909
+ }
4910
+ return typedBlocks;
4911
+ }
4912
+ function extractRenderHints(fieldMap) {
4913
+ const renderHints = {};
4914
+ const renderLabelField = fieldMap.get("renderLabel")?.[0];
4915
+ const renderOrbitField = fieldMap.get("renderOrbit")?.[0];
4916
+ const renderPriorityField = fieldMap.get("renderPriority")?.[0];
4917
+ if (renderLabelField) {
4918
+ renderHints.renderLabel = parseAtlasBoolean(singleFieldValue2(renderLabelField), "renderLabel", renderLabelField.location);
4919
+ }
4920
+ if (renderOrbitField) {
4921
+ renderHints.renderOrbit = parseAtlasBoolean(singleFieldValue2(renderOrbitField), "renderOrbit", renderOrbitField.location);
4922
+ }
4923
+ if (renderPriorityField) {
4924
+ renderHints.renderPriority = parseAtlasNumber(singleFieldValue2(renderPriorityField), "renderPriority", renderPriorityField.location);
4925
+ }
4926
+ return Object.keys(renderHints).length > 0 ? renderHints : void 0;
4927
+ }
4928
+ function parseResonanceField(field) {
4929
+ if (field.values.length !== 2) {
4930
+ throw WorldOrbitError.fromLocation('Field "resonance" expects "<targetObjectId> <ratio>"', field.location);
4931
+ }
4932
+ const ratio = field.values[1];
4933
+ if (!/^\d+:\d+$/.test(ratio)) {
4934
+ throw WorldOrbitError.fromLocation(`Invalid resonance ratio "${ratio}"`, field.location);
4935
+ }
4936
+ return {
4937
+ targetObjectId: field.values[0],
4938
+ ratio
4939
+ };
4940
+ }
4941
+ function parseDeriveField(field) {
4942
+ if (field.values.length !== 2) {
4943
+ throw WorldOrbitError.fromLocation('Field "derive" expects "<field> <strategy>"', field.location);
4944
+ }
4945
+ return {
4946
+ field: field.values[0],
4947
+ strategy: field.values[1]
4948
+ };
4949
+ }
4950
+ function parseToleranceField(field) {
4951
+ if (field.values.length !== 2) {
4952
+ throw WorldOrbitError.fromLocation('Field "tolerance" expects "<field> <value>"', field.location);
4953
+ }
4954
+ const rawValue = field.values[1];
4955
+ const unitValue = tryParseAtlasUnitValue(rawValue);
4956
+ const numericValue2 = Number(rawValue);
4957
+ return {
4958
+ field: field.values[0],
4959
+ value: unitValue ?? (Number.isFinite(numericValue2) ? numericValue2 : rawValue)
4960
+ };
4961
+ }
4962
+ function parseOptionalTokenList(field) {
4963
+ return field ? [...new Set(field.values)] : [];
4964
+ }
4965
+ function parseOptionalJoinedValue(field) {
4966
+ if (!field) {
4967
+ return null;
4968
+ }
4969
+ return field.values.join(" ").trim() || null;
4970
+ }
4971
+ function parseOptionalUnitField(field, key) {
4972
+ return field ? parseAtlasUnitValue(singleFieldValue2(field), field.location, key) : void 0;
4973
+ }
4974
+ function parseOptionalNumberField(field, key) {
4975
+ return field ? parseAtlasNumber(singleFieldValue2(field), key, field.location) : void 0;
4976
+ }
4977
+ function singleFieldValue2(field) {
4978
+ return singleAtlasValue(field.values, field.key, field.location);
4979
+ }
4980
+ function getDraftObjectFieldSpec(key) {
4981
+ return DRAFT_OBJECT_FIELD_SPECS.get(key);
4982
+ }
4983
+ function validateDraftObjectFieldCompatibility(fields, objectType) {
4984
+ for (const field of fields) {
4985
+ const spec = getDraftObjectFieldSpec(field.key);
4986
+ if (!spec) {
4987
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
4988
+ }
4989
+ if (spec.legacySchema) {
4990
+ ensureAtlasFieldSupported(field.key, objectType, field.location);
4991
+ continue;
4992
+ }
4993
+ if ((field.key === "renderLabel" || field.key === "renderOrbit" || field.key === "tidalLock") && field.values.length !== 1) {
4994
+ throw WorldOrbitError.fromLocation(`Field "${field.key}" expects exactly one value`, field.location);
4995
+ }
4996
+ }
4997
+ }
4998
+ function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
4999
+ if (sourceSchemaVersion === "2.1") {
5000
+ return;
5001
+ }
5002
+ diagnostics.push({
5003
+ code: "parse.schema21.featureCompatibility",
5004
+ severity: "warning",
5005
+ source: "parse",
5006
+ message: `Feature "${featureName}" requires schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
5007
+ line: location.line,
5008
+ column: location.column
5009
+ });
5010
+ }
5011
+ function preprocessAtlasSource(source) {
5012
+ const chars = [...source];
5013
+ const comments = [];
5014
+ let inString = false;
5015
+ let inBlockComment = false;
5016
+ let blockCommentStart = null;
5017
+ let line = 1;
5018
+ let column = 1;
5019
+ for (let index = 0; index < chars.length; index++) {
5020
+ const ch = chars[index];
5021
+ const next = chars[index + 1];
5022
+ if (inBlockComment) {
5023
+ if (ch === "*" && next === "/") {
5024
+ chars[index] = " ";
5025
+ chars[index + 1] = " ";
5026
+ inBlockComment = false;
5027
+ blockCommentStart = null;
5028
+ index++;
5029
+ column += 2;
5030
+ continue;
5031
+ }
5032
+ if (ch !== "\n" && ch !== "\r") {
5033
+ chars[index] = " ";
5034
+ }
5035
+ if (ch === "\n") {
5036
+ line++;
5037
+ column = 1;
5038
+ } else {
5039
+ column++;
5040
+ }
5041
+ continue;
5042
+ }
5043
+ if (!inString && ch === "/" && next === "*") {
5044
+ comments.push({ kind: "block", line, column });
5045
+ chars[index] = " ";
5046
+ chars[index + 1] = " ";
5047
+ inBlockComment = true;
5048
+ blockCommentStart = { line, column };
5049
+ index++;
5050
+ column += 2;
5051
+ continue;
5052
+ }
5053
+ if (!inString && ch === "#" && !isHexColorLiteral(chars, index)) {
5054
+ comments.push({ kind: "line", line, column });
5055
+ chars[index] = " ";
5056
+ let inner = index + 1;
5057
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
5058
+ chars[inner] = " ";
5059
+ inner++;
5060
+ }
5061
+ column += inner - index;
5062
+ index = inner - 1;
5063
+ continue;
5064
+ }
5065
+ if (ch === '"' && chars[index - 1] !== "\\") {
5066
+ inString = !inString;
5067
+ }
5068
+ if (ch === "\n") {
5069
+ line++;
5070
+ column = 1;
5071
+ } else {
5072
+ column++;
5073
+ }
5074
+ }
5075
+ if (inBlockComment) {
5076
+ throw WorldOrbitError.fromLocation("Unclosed block comment", blockCommentStart ?? void 0);
5077
+ }
5078
+ return {
5079
+ source: chars.join(""),
5080
+ comments
5081
+ };
5082
+ }
5083
+ function isHexColorLiteral(chars, start) {
5084
+ let index = start + 1;
5085
+ let length = 0;
5086
+ while (index < chars.length && /[0-9a-f]/i.test(chars[index] ?? "")) {
5087
+ index++;
5088
+ length++;
5089
+ }
5090
+ if (![3, 4, 6, 8].includes(length)) {
5091
+ return false;
5092
+ }
5093
+ const next = chars[index];
5094
+ return next === void 0 || next === " " || next === " " || next === "\r" || next === "\n";
3386
5095
  }
3387
5096
 
3388
5097
  // packages/core/dist/load.js
3389
- var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0)?$/i;
5098
+ var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1)?$/i;
5099
+ var ATLAS_SCHEMA_21_PATTERN = /^schema\s+2\.1$/i;
3390
5100
  var LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
3391
5101
  function detectWorldOrbitSchemaVersion(source) {
3392
- for (const line of source.split(/\r?\n/)) {
5102
+ for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
3393
5103
  const trimmed = line.trim();
3394
5104
  if (!trimmed) {
3395
5105
  continue;
@@ -3397,6 +5107,9 @@ var WorldOrbit = (() => {
3397
5107
  if (LEGACY_DRAFT_SCHEMA_PATTERN.test(trimmed)) {
3398
5108
  return "2.0-draft";
3399
5109
  }
5110
+ if (ATLAS_SCHEMA_21_PATTERN.test(trimmed)) {
5111
+ return "2.1";
5112
+ }
3400
5113
  if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
3401
5114
  return "2.0";
3402
5115
  }
@@ -3404,6 +5117,49 @@ var WorldOrbit = (() => {
3404
5117
  }
3405
5118
  return "1.0";
3406
5119
  }
5120
+ function stripCommentsForSchemaDetection(source) {
5121
+ const chars = [...source];
5122
+ let inString = false;
5123
+ let inBlockComment = false;
5124
+ for (let index = 0; index < chars.length; index++) {
5125
+ const ch = chars[index];
5126
+ const next = chars[index + 1];
5127
+ if (inBlockComment) {
5128
+ if (ch === "*" && next === "/") {
5129
+ chars[index] = " ";
5130
+ chars[index + 1] = " ";
5131
+ inBlockComment = false;
5132
+ index++;
5133
+ continue;
5134
+ }
5135
+ if (ch !== "\n" && ch !== "\r") {
5136
+ chars[index] = " ";
5137
+ }
5138
+ continue;
5139
+ }
5140
+ if (!inString && ch === "/" && next === "*") {
5141
+ chars[index] = " ";
5142
+ chars[index + 1] = " ";
5143
+ inBlockComment = true;
5144
+ index++;
5145
+ continue;
5146
+ }
5147
+ if (!inString && ch === "#") {
5148
+ chars[index] = " ";
5149
+ let inner = index + 1;
5150
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
5151
+ chars[inner] = " ";
5152
+ inner++;
5153
+ }
5154
+ index = inner - 1;
5155
+ continue;
5156
+ }
5157
+ if (ch === '"' && chars[index - 1] !== "\\") {
5158
+ inString = !inString;
5159
+ }
5160
+ }
5161
+ return chars.join("");
5162
+ }
3407
5163
  function loadWorldOrbitSource(source) {
3408
5164
  const result = loadWorldOrbitSourceWithDiagnostics(source);
3409
5165
  if (!result.ok || !result.value) {
@@ -3414,36 +5170,36 @@ var WorldOrbit = (() => {
3414
5170
  }
3415
5171
  function loadWorldOrbitSourceWithDiagnostics(source) {
3416
5172
  const schemaVersion = detectWorldOrbitSchemaVersion(source);
3417
- if (schemaVersion === "2.0" || schemaVersion === "2.0-draft") {
5173
+ if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1") {
3418
5174
  return loadAtlasSourceWithDiagnostics(source, schemaVersion);
3419
5175
  }
3420
5176
  let ast;
3421
5177
  try {
3422
5178
  ast = parseWorldOrbit(source);
3423
- } catch (error) {
5179
+ } catch (error2) {
3424
5180
  return {
3425
5181
  ok: false,
3426
5182
  value: null,
3427
- diagnostics: [diagnosticFromError(error, "parse")]
5183
+ diagnostics: [diagnosticFromError(error2, "parse")]
3428
5184
  };
3429
5185
  }
3430
5186
  let document2;
3431
5187
  try {
3432
5188
  document2 = normalizeDocument(ast);
3433
- } catch (error) {
5189
+ } catch (error2) {
3434
5190
  return {
3435
5191
  ok: false,
3436
5192
  value: null,
3437
- diagnostics: [diagnosticFromError(error, "normalize")]
5193
+ diagnostics: [diagnosticFromError(error2, "normalize")]
3438
5194
  };
3439
5195
  }
3440
5196
  try {
3441
5197
  validateDocument(document2);
3442
- } catch (error) {
5198
+ } catch (error2) {
3443
5199
  return {
3444
5200
  ok: false,
3445
5201
  value: null,
3446
- diagnostics: [diagnosticFromError(error, "validate")]
5202
+ diagnostics: [diagnosticFromError(error2, "validate")]
3447
5203
  };
3448
5204
  }
3449
5205
  return {
@@ -3463,30 +5219,29 @@ var WorldOrbit = (() => {
3463
5219
  let atlasDocument;
3464
5220
  try {
3465
5221
  atlasDocument = parseWorldOrbitAtlas(source);
3466
- } catch (error) {
5222
+ } catch (error2) {
3467
5223
  return {
3468
5224
  ok: false,
3469
5225
  value: null,
3470
- diagnostics: [diagnosticFromError(error, "parse", "load.atlas.failed")]
5226
+ diagnostics: [diagnosticFromError(error2, "parse", "load.atlas.failed")]
3471
5227
  };
3472
5228
  }
3473
- let document2;
3474
- try {
3475
- document2 = materializeAtlasDocument(atlasDocument);
3476
- } catch (error) {
5229
+ const atlasDiagnostics = [...atlasDocument.diagnostics];
5230
+ if (atlasDiagnostics.some((diagnostic) => diagnostic.severity === "error")) {
3477
5231
  return {
3478
5232
  ok: false,
3479
5233
  value: null,
3480
- diagnostics: [diagnosticFromError(error, "normalize", "load.atlas.materialize.failed")]
5234
+ diagnostics: atlasDiagnostics
3481
5235
  };
3482
5236
  }
5237
+ let document2;
3483
5238
  try {
3484
- validateDocument(document2);
3485
- } catch (error) {
5239
+ document2 = materializeAtlasDocument(atlasDocument);
5240
+ } catch (error2) {
3486
5241
  return {
3487
5242
  ok: false,
3488
5243
  value: null,
3489
- diagnostics: [diagnosticFromError(error, "validate", "load.atlas.validate.failed")]
5244
+ diagnostics: [diagnosticFromError(error2, "normalize", "load.atlas.materialize.failed")]
3490
5245
  };
3491
5246
  }
3492
5247
  const loaded = {
@@ -3495,12 +5250,12 @@ var WorldOrbit = (() => {
3495
5250
  document: document2,
3496
5251
  atlasDocument,
3497
5252
  draftDocument: atlasDocument,
3498
- diagnostics: [...atlasDocument.diagnostics]
5253
+ diagnostics: atlasDiagnostics
3499
5254
  };
3500
5255
  return {
3501
5256
  ok: true,
3502
5257
  value: loaded,
3503
- diagnostics: [...atlasDocument.diagnostics]
5258
+ diagnostics: atlasDiagnostics
3504
5259
  };
3505
5260
  }
3506
5261
 
@@ -3683,6 +5438,8 @@ var WorldOrbit = (() => {
3683
5438
  const imageDefinitions = buildImageDefinitions(visibleObjects);
3684
5439
  const orbitMarkup = layers.orbits ? renderOrbitLayer(scene, visibleObjectIds, layers.structures) : { back: "", front: "" };
3685
5440
  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("") : "";
5441
+ 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("") : "";
5442
+ const eventMarkup = layers.events ? scene.events.filter((event) => !event.hidden).map((event) => renderSceneEventOverlay(scene, event, visibleObjectIds, theme)).join("") : "";
3686
5443
  const objectMarkup = layers.objects ? visibleObjects.map((object) => renderSceneObject(object, options.selectedObjectId ?? null, theme)).join("") : "";
3687
5444
  const labelMarkup = layers.labels ? visibleLabels.map((label) => renderSceneLabel(scene, label, options.selectedObjectId ?? null)).join("") : "";
3688
5445
  const metadataMarkup = layers.metadata ? `<text class="wo-title" x="56" y="64">${escapeXml(scene.title)}</text>
@@ -3717,6 +5474,10 @@ var WorldOrbit = (() => {
3717
5474
  .wo-orbit-back { opacity: 0.38; stroke-dasharray: 8 6; }
3718
5475
  .wo-orbit-front { opacity: 0.9; }
3719
5476
  .wo-orbit-band { stroke: ${theme.orbitBand}; stroke-linecap: round; }
5477
+ .wo-relation { stroke: ${theme.relation}; stroke-width: 2; stroke-dasharray: 10 6; }
5478
+ .wo-event-line { stroke: ${theme.accent}; stroke-width: 1.6; stroke-dasharray: 5 5; opacity: 0.72; }
5479
+ .wo-event-node { fill: ${theme.accent}; stroke: ${theme.selected}; stroke-width: 1.4; opacity: 0.92; }
5480
+ .wo-event-label { fill: ${theme.accent}; font-family: ${theme.fontFamily}; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
3720
5481
  .wo-leader { stroke: ${theme.leader}; stroke-width: 1.5; stroke-dasharray: 6 5; }
3721
5482
  .wo-label { fill: ${theme.ink}; font-family: ${theme.fontFamily}; font-weight: 600; letter-spacing: 0.02em; }
3722
5483
  .wo-label-secondary { fill: ${theme.muted}; font-family: ${theme.fontFamily}; font-weight: 500; }
@@ -3750,6 +5511,8 @@ var WorldOrbit = (() => {
3750
5511
  <g data-worldorbit-world-content="true">
3751
5512
  ${layers.orbits ? `<g data-layer-id="orbits-back">${orbitMarkup.back}</g>` : ""}
3752
5513
  ${layers.guides ? `<g data-layer-id="guides">${leaderMarkup}</g>` : ""}
5514
+ ${layers.relations ? `<g data-layer-id="relations">${relationMarkup}</g>` : ""}
5515
+ ${layers.events ? `<g data-layer-id="events">${eventMarkup}</g>` : ""}
3753
5516
  ${layers.objects ? `<g data-layer-id="objects">${objectMarkup}</g>` : ""}
3754
5517
  ${layers.orbits ? `<g data-layer-id="orbits-front">${orbitMarkup.front}</g>` : ""}
3755
5518
  ${layers.labels ? `<g data-layer-id="labels">${labelMarkup}</g>` : ""}
@@ -3757,6 +5520,20 @@ var WorldOrbit = (() => {
3757
5520
  </g>
3758
5521
  </g>
3759
5522
  </svg>`;
5523
+ }
5524
+ function renderSceneEventOverlay(scene, event, visibleObjectIds, theme) {
5525
+ const participants = event.objectIds.filter((objectId) => visibleObjectIds.has(objectId)).map((objectId) => scene.objects.find((object) => object.objectId === objectId && !object.hidden)).filter(Boolean);
5526
+ if (participants.length === 0) {
5527
+ return "";
5528
+ }
5529
+ const stroke = event.event.color || theme.accent;
5530
+ const label = event.event.label || event.event.id;
5531
+ 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("");
5532
+ return `<g class="wo-event" data-render-id="${escapeXml(event.renderId)}" data-event-id="${escapeAttribute(event.eventId)}">
5533
+ ${lineMarkup}
5534
+ <circle class="wo-event-node" cx="${event.x}" cy="${event.y}" r="5" fill="${escapeAttribute(stroke)}" />
5535
+ <text class="wo-event-label" x="${event.x}" y="${event.y - 10}" text-anchor="middle" font-size="10">${escapeXml(label)}</text>
5536
+ </g>`;
3760
5537
  }
3761
5538
  function renderDocumentToSvg(document2, options = {}) {
3762
5539
  return renderSceneToSvg(renderDocumentToScene(document2, options), options);
@@ -3810,10 +5587,11 @@ var WorldOrbit = (() => {
3810
5587
  function renderSceneObject(sceneObject, selectedObjectId, theme) {
3811
5588
  const { object, x, y, radius, visualRadius } = sceneObject;
3812
5589
  const selectionClass = selectedObjectId === sceneObject.objectId ? " wo-object-selected" : "";
5590
+ const kindClass = object.properties.kind ? ` wo-kind-${String(object.properties.kind).toLowerCase().replace(/[^a-z0-9-]/g, "-")}` : "";
3813
5591
  const palette = resolveObjectPalette(sceneObject, theme);
3814
5592
  const imageMarkup = renderObjectImage(sceneObject);
3815
5593
  const outlineMarkup = imageMarkup ? renderObjectBody(object, x, y, radius, palette, { outlineOnly: true }) : "";
3816
- 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}`)}">
5594
+ 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}`)}">
3817
5595
  <circle class="wo-selection-ring" cx="${x}" cy="${y}" r="${visualRadius + 8}" />
3818
5596
  ${renderAtmosphere(sceneObject, palette)}
3819
5597
  ${renderObjectBody(object, x, y, radius, palette)}
@@ -3847,8 +5625,33 @@ var WorldOrbit = (() => {
3847
5625
  <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
3848
5626
  case "structure":
3849
5627
  return `<polygon points="${diamondPoints(x, y, radius)}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
3850
- case "phenomenon":
5628
+ case "phenomenon": {
5629
+ const kind = String(object.properties.kind ?? "").toLowerCase().replace(/_/g, "-");
5630
+ if (options.outlineOnly) {
5631
+ if (kind === "black-hole" || kind === "nebula" || kind === "galaxy" || kind === "dwarf-galaxy") {
5632
+ return `<circle cx="${x}" cy="${y}" r="${radius}" fill="transparent" stroke="${palette.stroke}" stroke-width="1.4" />`;
5633
+ }
5634
+ return `<polygon points="${phenomenonPoints(x, y, radius)}" fill="transparent" stroke="${palette.stroke}" stroke-width="1.4" />`;
5635
+ }
5636
+ if (kind === "black-hole") {
5637
+ 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" />
5638
+ <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="2" />`;
5639
+ }
5640
+ if (kind === "galaxy") {
5641
+ return `<ellipse cx="${x}" cy="${y}" rx="${radius * 2.6}" ry="${radius}" fill="${palette.halo ?? "none"}" stroke="none" />
5642
+ <ellipse cx="${x}" cy="${y}" rx="${radius * 1.5}" ry="${radius * 0.42}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.2" />
5643
+ <circle cx="${x}" cy="${y}" r="${radius * 0.28}" fill="${palette.core ?? "#fff"}" stroke="none" />`;
5644
+ }
5645
+ if (kind === "dwarf-galaxy") {
5646
+ return `<ellipse cx="${x}" cy="${y}" rx="${radius * 1.6}" ry="${radius * 0.55}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1" />
5647
+ <circle cx="${x}" cy="${y}" r="${radius * 0.25}" fill="${palette.core ?? "#fff"}" stroke="none" />`;
5648
+ }
5649
+ if (kind === "nebula") {
5650
+ return `<circle cx="${x}" cy="${y}" r="${radius * 2.2}" fill="${palette.halo ?? "none"}" stroke="none" />
5651
+ <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1" />`;
5652
+ }
3851
5653
  return `<polygon points="${phenomenonPoints(x, y, radius)}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
5654
+ }
3852
5655
  }
3853
5656
  }
3854
5657
  function renderAtmosphere(sceneObject, palette) {
@@ -3917,7 +5720,8 @@ var WorldOrbit = (() => {
3917
5720
  }
3918
5721
  }
3919
5722
  function resolveObjectPalette(sceneObject, theme) {
3920
- const base = basePaletteForType(sceneObject.object.type, theme);
5723
+ const kind = String(sceneObject.object.properties.kind ?? "").toLowerCase().replace(/_/g, "-");
5724
+ const base = basePaletteForType(sceneObject.object.type, kind, theme);
3921
5725
  const customFill = sceneObject.fillColor && isColorLike(sceneObject.fillColor) ? sceneObject.fillColor : base.fill;
3922
5726
  const albedo = numericValue(sceneObject.object.properties.albedo);
3923
5727
  const temperature = numericValue(sceneObject.object.properties.temperature);
@@ -3933,7 +5737,7 @@ var WorldOrbit = (() => {
3933
5737
  tail: sceneObject.object.type === "comet" ? rgbaString(mixColors(fill, "#ffffff", 0.5) ?? fill, 0.72) : void 0
3934
5738
  };
3935
5739
  }
3936
- function basePaletteForType(type, theme) {
5740
+ function basePaletteForType(type, kind, theme) {
3937
5741
  switch (type) {
3938
5742
  case "star":
3939
5743
  return {
@@ -3955,8 +5759,26 @@ var WorldOrbit = (() => {
3955
5759
  case "structure":
3956
5760
  return { fill: theme.accentStrong, stroke: "#fff2ea" };
3957
5761
  case "phenomenon":
3958
- return { fill: "#78ffd7", stroke: "#e9fff7" };
5762
+ return kindPhenomenonPalette(kind);
5763
+ }
5764
+ }
5765
+ function kindPhenomenonPalette(kind) {
5766
+ if (kind === "galaxy") {
5767
+ return { fill: "rgba(165,125,255,0.55)", stroke: "rgba(210,185,255,0.75)", halo: "rgba(160,120,255,0.10)", core: "#ede0ff" };
5768
+ }
5769
+ if (kind === "dwarf-galaxy") {
5770
+ return { fill: "rgba(190,165,255,0.45)", stroke: "rgba(220,205,255,0.75)", core: "#ddd0ff" };
3959
5771
  }
5772
+ if (kind === "black-hole") {
5773
+ return { fill: "#040408", stroke: "#ff6a00", accentRing: "rgba(255,140,20,0.72)" };
5774
+ }
5775
+ if (kind === "nebula") {
5776
+ return { fill: "rgba(105,205,255,0.45)", stroke: "rgba(180,235,255,0.72)", halo: "rgba(100,200,255,0.08)" };
5777
+ }
5778
+ if (kind === "void") {
5779
+ return { fill: "#05080f", stroke: "rgba(130,160,255,0.4)" };
5780
+ }
5781
+ return { fill: "#78ffd7", stroke: "#e9fff7" };
3960
5782
  }
3961
5783
  function applyTemperatureAndAlbedo(baseColor, temperature, albedo, type) {
3962
5784
  let nextColor = baseColor;
@@ -4276,6 +6098,48 @@ var WorldOrbit = (() => {
4276
6098
  });
4277
6099
  }
4278
6100
  const placement = details.object.placement;
6101
+ if (details.object.groups?.length) {
6102
+ fields.set("groups", {
6103
+ key: "groups",
6104
+ label: "Groups",
6105
+ value: details.object.groups.join(", ")
6106
+ });
6107
+ }
6108
+ if (details.object.epoch) {
6109
+ fields.set("epoch", {
6110
+ key: "epoch",
6111
+ label: "Epoch",
6112
+ value: details.object.epoch
6113
+ });
6114
+ }
6115
+ if (details.object.referencePlane) {
6116
+ fields.set("referencePlane", {
6117
+ key: "referencePlane",
6118
+ label: "Reference Plane",
6119
+ value: details.object.referencePlane
6120
+ });
6121
+ }
6122
+ if (details.object.tidalLock !== void 0) {
6123
+ fields.set("tidalLock", {
6124
+ key: "tidalLock",
6125
+ label: "Tidal Lock",
6126
+ value: details.object.tidalLock ? "true" : "false"
6127
+ });
6128
+ }
6129
+ if (details.object.resonance) {
6130
+ fields.set("resonance", {
6131
+ key: "resonance",
6132
+ label: "Resonance",
6133
+ value: `${details.object.resonance.targetObjectId} ${details.object.resonance.ratio}`
6134
+ });
6135
+ }
6136
+ if (details.relatedEvents.length > 0) {
6137
+ fields.set("events", {
6138
+ key: "events",
6139
+ label: "Events",
6140
+ value: details.relatedEvents.map((event) => event.event.label || event.event.id).join(", ")
6141
+ });
6142
+ }
4279
6143
  if (placement?.mode === "at") {
4280
6144
  fields.set("placement", {
4281
6145
  key: "placement",
@@ -4445,6 +6309,9 @@ var WorldOrbit = (() => {
4445
6309
  touchPoints.set(event.pointerId, point);
4446
6310
  if (touchPoints.size === 2) {
4447
6311
  touchGesture = createTouchGestureState(scene, state, touchPoints);
6312
+ } else if (touchPoints.size === 1) {
6313
+ dragDistance = 0;
6314
+ suppressClick = false;
4448
6315
  }
4449
6316
  return;
4450
6317
  }
@@ -4462,7 +6329,9 @@ var WorldOrbit = (() => {
4462
6329
  if (!behavior.touch || !touchPoints.has(event.pointerId)) {
4463
6330
  return;
4464
6331
  }
4465
- touchPoints.set(event.pointerId, getViewportPointFromClient(event.clientX, event.clientY));
6332
+ const prevPoint = touchPoints.get(event.pointerId);
6333
+ const nextPoint2 = getViewportPointFromClient(event.clientX, event.clientY);
6334
+ touchPoints.set(event.pointerId, nextPoint2);
4466
6335
  if (touchPoints.size === 2) {
4467
6336
  if (!touchGesture) {
4468
6337
  touchGesture = createTouchGestureState(scene, state, touchPoints);
@@ -4473,6 +6342,14 @@ var WorldOrbit = (() => {
4473
6342
  const deltaX2 = current.center.x - touchGesture.startViewportCenter.x;
4474
6343
  const deltaY2 = current.center.y - touchGesture.startViewportCenter.y;
4475
6344
  updateState(panViewerState(zoomedState, deltaX2, deltaY2));
6345
+ } else if (touchPoints.size === 1) {
6346
+ const deltaX2 = nextPoint2.x - prevPoint.x;
6347
+ const deltaY2 = nextPoint2.y - prevPoint.y;
6348
+ dragDistance += Math.abs(deltaX2) + Math.abs(deltaY2);
6349
+ if (dragDistance > 2) {
6350
+ suppressClick = true;
6351
+ }
6352
+ updateState(panViewerState(state, deltaX2, deltaY2));
4476
6353
  }
4477
6354
  return;
4478
6355
  }
@@ -4677,6 +6554,12 @@ var WorldOrbit = (() => {
4677
6554
  emitAtlasStateChange();
4678
6555
  return true;
4679
6556
  },
6557
+ getActiveEventId() {
6558
+ return renderOptions.activeEventId ?? null;
6559
+ },
6560
+ setActiveEvent(id) {
6561
+ api.setRenderOptions({ activeEventId: id });
6562
+ },
4680
6563
  search(query, limit = 12) {
4681
6564
  return searchSceneObjects(scene, query, limit);
4682
6565
  },
@@ -4937,8 +6820,11 @@ var WorldOrbit = (() => {
4937
6820
  renderObject,
4938
6821
  label: scene.labels.find((label) => label.objectId === renderObject.objectId && !label.hidden) ?? null,
4939
6822
  group: scene.groups.find((group) => group.renderId === renderObject.groupId) ?? null,
6823
+ semanticGroups: scene.semanticGroups.filter((group) => renderObject.semanticGroupIds.includes(group.id)),
4940
6824
  orbit: scene.orbitVisuals.find((orbit) => orbit.objectId === renderObject.objectId && !orbit.hidden) ?? null,
4941
6825
  relatedOrbits: scene.orbitVisuals.filter((orbit) => !orbit.hidden && (orbit.objectId === renderObject.objectId || renderObject.ancestorIds.includes(orbit.objectId) || renderObject.childIds.includes(orbit.objectId))),
6826
+ relations: scene.relations.filter((relation) => !relation.hidden && (relation.fromObjectId === renderObject.objectId || relation.toObjectId === renderObject.objectId)),
6827
+ relatedEvents: scene.events.filter((event) => !event.hidden && (event.targetObjectId === renderObject.objectId || event.objectIds.includes(renderObject.objectId))),
4942
6828
  parent: getObjectById(renderObject.parentId),
4943
6829
  children: renderObject.childIds.map((childId) => getObjectById(childId)).filter(Boolean),
4944
6830
  ancestors: renderObject.ancestorIds.map((ancestorId) => getObjectById(ancestorId)).filter(Boolean),
@@ -5255,7 +7141,8 @@ var WorldOrbit = (() => {
5255
7141
  filter: renderOptions.filter ? { ...renderOptions.filter } : void 0,
5256
7142
  scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0,
5257
7143
  layers: renderOptions.layers ? { ...renderOptions.layers } : void 0,
5258
- theme: renderOptions.theme && typeof renderOptions.theme === "object" ? { ...renderOptions.theme } : renderOptions.theme
7144
+ theme: renderOptions.theme && typeof renderOptions.theme === "object" ? { ...renderOptions.theme } : renderOptions.theme,
7145
+ activeEventId: renderOptions.activeEventId ?? null
5259
7146
  };
5260
7147
  }
5261
7148
  function mergeRenderOptions(current, next) {
@@ -5275,7 +7162,7 @@ var WorldOrbit = (() => {
5275
7162
  };
5276
7163
  }
5277
7164
  function hasSceneAffectingRenderOptions(options) {
5278
- return options.width !== void 0 || options.height !== void 0 || options.padding !== void 0 || options.preset !== void 0 || options.projection !== void 0 || options.scaleModel !== void 0;
7165
+ return options.width !== void 0 || options.height !== void 0 || options.padding !== void 0 || options.preset !== void 0 || options.projection !== void 0 || options.scaleModel !== void 0 || options.activeEventId !== void 0;
5279
7166
  }
5280
7167
  function resolveSourceRenderOptions2(loaded, renderOptions) {
5281
7168
  const atlasDocument = loaded.atlasDocument ?? loaded.draftDocument;
@@ -5563,6 +7450,7 @@ var WorldOrbit = (() => {
5563
7450
  const controls = {
5564
7451
  search: options.controls?.search ?? true,
5565
7452
  typeFilter: options.controls?.typeFilter ?? true,
7453
+ groupFilter: options.controls?.groupFilter ?? true,
5566
7454
  viewpointSelect: options.controls?.viewpointSelect ?? true,
5567
7455
  inspector: options.controls?.inspector ?? true,
5568
7456
  bookmarks: options.controls?.bookmarks ?? true
@@ -5572,6 +7460,7 @@ var WorldOrbit = (() => {
5572
7460
  const toolbar = container.querySelector("[data-atlas-toolbar]");
5573
7461
  const searchInput = container.querySelector("[data-atlas-search]");
5574
7462
  const typeFilterSelect = container.querySelector("[data-atlas-type-filter]");
7463
+ const groupFilterSelect = container.querySelector("[data-atlas-group-filter]");
5575
7464
  const viewpointSelect = container.querySelector("[data-atlas-viewpoint]");
5576
7465
  const bookmarkButton = container.querySelector("[data-atlas-bookmark]");
5577
7466
  const bookmarkList = container.querySelector("[data-atlas-bookmarks]");
@@ -5584,6 +7473,7 @@ var WorldOrbit = (() => {
5584
7473
  const baseFilter = normalizeViewerFilter(options.initialFilter ?? null);
5585
7474
  let searchQuery = options.initialQuery?.trim() ?? baseFilter?.query ?? "";
5586
7475
  let objectTypeFilter = options.initialObjectType ?? (baseFilter?.objectTypes?.length === 1 ? baseFilter.objectTypes[0] : null);
7476
+ let groupFilter = baseFilter?.groupIds?.[0] ?? null;
5587
7477
  let bookmarks = [];
5588
7478
  let viewer;
5589
7479
  viewer = createInteractiveViewer(stage, {
@@ -5631,6 +7521,7 @@ var WorldOrbit = (() => {
5631
7521
  });
5632
7522
  applyCurrentFilter();
5633
7523
  populateViewpoints();
7524
+ populateGroups();
5634
7525
  syncControlsFromFilter(viewer.getFilter());
5635
7526
  renderBookmarks();
5636
7527
  updateSearchResults();
@@ -5643,6 +7534,10 @@ var WorldOrbit = (() => {
5643
7534
  objectTypeFilter = typeFilterSelect.value || null;
5644
7535
  applyCurrentFilter();
5645
7536
  });
7537
+ groupFilterSelect?.addEventListener("change", () => {
7538
+ groupFilter = groupFilterSelect.value || null;
7539
+ applyCurrentFilter();
7540
+ });
5646
7541
  viewpointSelect?.addEventListener("change", () => {
5647
7542
  const activeViewer = requireViewer();
5648
7543
  if (!viewpointSelect.value) {
@@ -5784,6 +7679,7 @@ var WorldOrbit = (() => {
5784
7679
  return api;
5785
7680
  function refreshAfterInputChange() {
5786
7681
  populateViewpoints();
7682
+ populateGroups();
5787
7683
  applyCurrentFilter();
5788
7684
  renderBookmarks();
5789
7685
  updateSearchResults();
@@ -5800,19 +7696,23 @@ var WorldOrbit = (() => {
5800
7696
  query: searchQuery || void 0,
5801
7697
  objectTypes: objectTypeFilter ? [objectTypeFilter] : void 0,
5802
7698
  tags: baseFilter?.tags,
5803
- groupIds: baseFilter?.groupIds,
7699
+ groupIds: groupFilter ? [groupFilter] : baseFilter?.groupIds,
5804
7700
  includeAncestors: baseFilter?.includeAncestors ?? true
5805
7701
  });
5806
7702
  }
5807
7703
  function syncControlsFromFilter(filter) {
5808
7704
  searchQuery = filter?.query?.trim() ?? "";
5809
7705
  objectTypeFilter = filter?.objectTypes?.length === 1 ? filter.objectTypes[0] : null;
7706
+ groupFilter = filter?.groupIds?.length === 1 ? filter.groupIds[0] : null;
5810
7707
  if (searchInput && document.activeElement !== searchInput) {
5811
7708
  searchInput.value = searchQuery;
5812
7709
  }
5813
7710
  if (typeFilterSelect) {
5814
7711
  typeFilterSelect.value = objectTypeFilter ?? "";
5815
7712
  }
7713
+ if (groupFilterSelect) {
7714
+ groupFilterSelect.value = groupFilter ?? "";
7715
+ }
5816
7716
  }
5817
7717
  function populateViewpoints() {
5818
7718
  if (!viewpointSelect) {
@@ -5826,6 +7726,17 @@ var WorldOrbit = (() => {
5826
7726
  ].join("");
5827
7727
  viewpointSelect.value = active;
5828
7728
  }
7729
+ function populateGroups() {
7730
+ if (!groupFilterSelect) {
7731
+ return;
7732
+ }
7733
+ const activeViewer = requireViewer();
7734
+ groupFilterSelect.innerHTML = [
7735
+ `<option value="">All groups</option>`,
7736
+ ...activeViewer.getScene().semanticGroups.map((group) => `<option value="${escapeHtml2(group.id)}">${escapeHtml2(group.label)}</option>`)
7737
+ ].join("");
7738
+ groupFilterSelect.value = groupFilter ?? "";
7739
+ }
5829
7740
  function syncViewpointControl() {
5830
7741
  if (!viewpointSelect) {
5831
7742
  return;
@@ -5859,6 +7770,9 @@ var WorldOrbit = (() => {
5859
7770
  projection: activeViewer.getScene().projection,
5860
7771
  renderPreset: activeViewer.getScene().renderPreset,
5861
7772
  groupCount: activeViewer.getScene().groups.length,
7773
+ semanticGroupCount: activeViewer.getScene().semanticGroups.length,
7774
+ relationCount: activeViewer.getScene().relations.length,
7775
+ eventCount: activeViewer.getScene().events.length,
5862
7776
  viewpointCount: activeViewer.getScene().viewpoints.length
5863
7777
  }
5864
7778
  };
@@ -5891,6 +7805,12 @@ var WorldOrbit = (() => {
5891
7805
  <option value="phenomenon">Phenomenon</option>
5892
7806
  </select>
5893
7807
  </label>` : "",
7808
+ controls.groupFilter ? `<label class="wo-atlas-field">
7809
+ <span>Group</span>
7810
+ <select data-atlas-group-filter>
7811
+ <option value="">All groups</option>
7812
+ </select>
7813
+ </label>` : "",
5894
7814
  controls.viewpointSelect ? `<label class="wo-atlas-field">
5895
7815
  <span>Viewpoint</span>
5896
7816
  <select data-atlas-viewpoint>
@@ -6034,5 +7954,5 @@ var WorldOrbit = (() => {
6034
7954
  function parseSource(source) {
6035
7955
  return loadWorldOrbitSource(source).document;
6036
7956
  }
6037
- return __toCommonJS(dist_exports);
7957
+ return __toCommonJS(index_exports);
6038
7958
  })();