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
@@ -4,6 +4,8 @@
4
4
  var DEFAULT_LAYERS = {
5
5
  background: true,
6
6
  guides: true,
7
+ relations: true,
8
+ events: true,
7
9
  orbits: true,
8
10
  objects: true,
9
11
  labels: true,
@@ -18,6 +20,7 @@
18
20
  backgroundGlow: "rgba(240, 180, 100, 0.18)",
19
21
  panel: "rgba(7, 17, 27, 0.9)",
20
22
  panelLine: "rgba(168, 207, 242, 0.18)",
23
+ relation: "rgba(240, 180, 100, 0.42)",
21
24
  orbit: "rgba(163, 209, 255, 0.24)",
22
25
  orbitBand: "rgba(255, 190, 120, 0.28)",
23
26
  guide: "rgba(255, 255, 255, 0.04)",
@@ -40,6 +43,7 @@
40
43
  backgroundGlow: "rgba(120, 255, 215, 0.16)",
41
44
  panel: "rgba(7, 20, 30, 0.9)",
42
45
  panelLine: "rgba(120, 255, 215, 0.16)",
46
+ relation: "rgba(156, 231, 255, 0.42)",
43
47
  orbit: "rgba(120, 255, 215, 0.2)",
44
48
  orbitBand: "rgba(137, 185, 255, 0.24)",
45
49
  guide: "rgba(255, 255, 255, 0.035)",
@@ -62,6 +66,7 @@
62
66
  backgroundGlow: "rgba(255, 127, 95, 0.18)",
63
67
  panel: "rgba(24, 9, 13, 0.9)",
64
68
  panelLine: "rgba(255, 166, 149, 0.16)",
69
+ relation: "rgba(255, 178, 125, 0.42)",
65
70
  orbit: "rgba(255, 188, 164, 0.22)",
66
71
  orbitBand: "rgba(255, 214, 139, 0.24)",
67
72
  guide: "rgba(255, 255, 255, 0.03)",
@@ -152,12 +157,14 @@
152
157
  return {
153
158
  version: "2.0",
154
159
  viewpointId,
160
+ activeEventId: renderOptions.activeEventId ?? null,
155
161
  viewerState: { ...viewerState },
156
162
  renderOptions: {
157
163
  preset: renderOptions.preset,
158
164
  projection: renderOptions.projection,
159
165
  layers: renderOptions.layers ? { ...renderOptions.layers } : void 0,
160
- scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0
166
+ scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0,
167
+ activeEventId: renderOptions.activeEventId ?? null
161
168
  },
162
169
  filter: normalizeViewerFilter(filter)
163
170
  };
@@ -170,6 +177,7 @@
170
177
  return {
171
178
  version: "2.0",
172
179
  viewpointId: raw.viewpointId ?? null,
180
+ activeEventId: raw.activeEventId ?? raw.renderOptions?.activeEventId ?? null,
173
181
  viewerState: {
174
182
  scale: raw.viewerState?.scale ?? 1,
175
183
  rotationDeg: raw.viewerState?.rotationDeg ?? 0,
@@ -181,7 +189,8 @@
181
189
  preset: raw.renderOptions?.preset,
182
190
  projection: raw.renderOptions?.projection,
183
191
  layers: raw.renderOptions?.layers ? { ...raw.renderOptions.layers } : void 0,
184
- scaleModel: raw.renderOptions?.scaleModel ? { ...raw.renderOptions.scaleModel } : void 0
192
+ scaleModel: raw.renderOptions?.scaleModel ? { ...raw.renderOptions.scaleModel } : void 0,
193
+ activeEventId: raw.activeEventId ?? raw.renderOptions?.activeEventId ?? null
185
194
  },
186
195
  filter: normalizeViewerFilter(raw.filter ?? null)
187
196
  };
@@ -197,7 +206,8 @@
197
206
  renderOptions: {
198
207
  ...atlasState.renderOptions,
199
208
  layers: atlasState.renderOptions.layers ? { ...atlasState.renderOptions.layers } : void 0,
200
- scaleModel: atlasState.renderOptions.scaleModel ? { ...atlasState.renderOptions.scaleModel } : void 0
209
+ scaleModel: atlasState.renderOptions.scaleModel ? { ...atlasState.renderOptions.scaleModel } : void 0,
210
+ activeEventId: atlasState.renderOptions.activeEventId ?? null
201
211
  },
202
212
  filter: atlasState.filter ? { ...atlasState.filter } : null
203
213
  }
@@ -214,6 +224,8 @@
214
224
  return {
215
225
  background: viewpoint.layers.background,
216
226
  guides: viewpoint.layers.guides,
227
+ relations: viewpoint.layers.relations,
228
+ events: viewpoint.layers.events,
217
229
  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,
218
230
  objects: viewpoint.layers.objects,
219
231
  labels: viewpoint.layers.labels,
@@ -251,7 +263,11 @@
251
263
  return false;
252
264
  }
253
265
  if (filter.groupIds?.length && (!object.groupId || !filter.groupIds.includes(object.groupId))) {
254
- return false;
266
+ const hasSemanticMatch = object.semanticGroupIds.length > 0 && filter.groupIds.some((groupId) => object.semanticGroupIds.includes(groupId));
267
+ const hasLegacyMatch = Boolean(object.groupId && filter.groupIds.includes(object.groupId));
268
+ if (!hasSemanticMatch && !hasLegacyMatch) {
269
+ return false;
270
+ }
255
271
  }
256
272
  if (filter.tags?.length) {
257
273
  const objectTags = Array.isArray(object.object.properties.tags) ? object.object.properties.tags.filter((entry) => typeof entry === "string") : [];
@@ -618,13 +634,13 @@
618
634
  function unitFamilyAllowsUnit(family, unit) {
619
635
  switch (family) {
620
636
  case "distance":
621
- return unit === null || ["au", "km", "re", "sol"].includes(unit);
637
+ return unit === null || ["au", "km", "m", "ly", "pc", "kpc", "re", "sol"].includes(unit);
622
638
  case "radius":
623
- return unit === null || ["km", "re", "sol"].includes(unit);
639
+ return unit === null || ["km", "m", "re", "rj", "sol"].includes(unit);
624
640
  case "mass":
625
- return unit === null || ["me", "sol"].includes(unit);
641
+ return unit === null || ["me", "mj", "sol"].includes(unit);
626
642
  case "duration":
627
- return unit === null || ["h", "d", "y"].includes(unit);
643
+ return unit === null || ["s", "min", "h", "d", "y", "ky", "my", "gy"].includes(unit);
628
644
  case "angle":
629
645
  return unit === null || unit === "deg";
630
646
  case "generic":
@@ -828,7 +844,7 @@
828
844
  }
829
845
 
830
846
  // packages/core/dist/normalize.js
831
- var UNIT_PATTERN = /^(-?\d+(?:\.\d+)?)(au|km|re|sol|me|d|y|h|deg)?$/;
847
+ 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)?$/;
832
848
  var BOOLEAN_VALUES = /* @__PURE__ */ new Map([
833
849
  ["true", true],
834
850
  ["false", false],
@@ -853,7 +869,11 @@
853
869
  return {
854
870
  format: "worldorbit",
855
871
  version: "1.0",
872
+ schemaVersion: "1.0",
856
873
  system,
874
+ groups: [],
875
+ relations: [],
876
+ events: [],
857
877
  objects
858
878
  };
859
879
  }
@@ -863,13 +883,17 @@
863
883
  const fieldMap = collectFields(mergedFields);
864
884
  const placement = extractPlacement(node.objectType, fieldMap);
865
885
  const properties = normalizeProperties(fieldMap);
866
- const info = normalizeInfo(node.infoEntries);
886
+ const info2 = normalizeInfo(node.infoEntries);
867
887
  if (node.objectType === "system") {
868
888
  return {
869
889
  type: "system",
870
890
  id: node.name,
891
+ title: typeof properties.title === "string" ? properties.title : null,
892
+ description: null,
893
+ epoch: null,
894
+ referencePlane: null,
871
895
  properties,
872
- info
896
+ info: info2
873
897
  };
874
898
  }
875
899
  return {
@@ -877,7 +901,7 @@
877
901
  id: node.name,
878
902
  properties,
879
903
  placement,
880
- info
904
+ info: info2
881
905
  };
882
906
  }
883
907
  function validateFieldCompatibility(objectType, fields) {
@@ -1007,14 +1031,14 @@
1007
1031
  }
1008
1032
  }
1009
1033
  function normalizeInfo(entries) {
1010
- const info = {};
1034
+ const info2 = {};
1011
1035
  for (const entry of entries) {
1012
- if (entry.key in info) {
1036
+ if (entry.key in info2) {
1013
1037
  throw WorldOrbitError.fromLocation(`Duplicate info key "${entry.key}"`, entry.location);
1014
1038
  }
1015
- info[entry.key] = entry.value;
1039
+ info2[entry.key] = entry.value;
1016
1040
  }
1017
- return info;
1041
+ return info2;
1018
1042
  }
1019
1043
  function parseAtReference(target, location) {
1020
1044
  if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
@@ -1185,37 +1209,41 @@
1185
1209
  }
1186
1210
 
1187
1211
  // packages/core/dist/diagnostics.js
1188
- function diagnosticFromError(error, source, code = `${source}.failed`) {
1189
- if (error instanceof WorldOrbitError) {
1212
+ function diagnosticFromError(error2, source, code = `${source}.failed`) {
1213
+ if (error2 instanceof WorldOrbitError) {
1190
1214
  return {
1191
1215
  code,
1192
1216
  severity: "error",
1193
1217
  source,
1194
- message: error.message,
1195
- line: error.line,
1196
- column: error.column
1218
+ message: error2.message,
1219
+ line: error2.line,
1220
+ column: error2.column
1197
1221
  };
1198
1222
  }
1199
- if (error instanceof Error) {
1223
+ if (error2 instanceof Error) {
1200
1224
  return {
1201
1225
  code,
1202
1226
  severity: "error",
1203
1227
  source,
1204
- message: error.message
1228
+ message: error2.message
1205
1229
  };
1206
1230
  }
1207
1231
  return {
1208
1232
  code,
1209
1233
  severity: "error",
1210
1234
  source,
1211
- message: String(error)
1235
+ message: String(error2)
1212
1236
  };
1213
1237
  }
1214
1238
 
1215
1239
  // packages/core/dist/scene.js
1216
1240
  var AU_IN_KM = 1495978707e-1;
1217
1241
  var EARTH_RADIUS_IN_KM = 6371;
1242
+ var JUPITER_RADIUS_IN_KM = 71492;
1218
1243
  var SOLAR_RADIUS_IN_KM = 695700;
1244
+ var LY_IN_AU = 63241.077;
1245
+ var PC_IN_AU = 206264.806;
1246
+ var KPC_IN_AU = 206264806;
1219
1247
  var ISO_FLATTENING = 0.68;
1220
1248
  var MIN_ISO_MINOR_SCALE = 0.2;
1221
1249
  var ARC_SAMPLE_COUNT = 28;
@@ -1229,8 +1257,10 @@
1229
1257
  const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
1230
1258
  const spacingFactor = layoutPresetSpacing(layoutPreset);
1231
1259
  const systemId = document2.system?.id ?? null;
1232
- const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
1233
- const relationships = buildSceneRelationships(document2.objects, objectMap);
1260
+ const activeEventId = options.activeEventId ?? null;
1261
+ const effectiveObjects = createEffectiveObjects(document2.objects, document2.events ?? [], activeEventId);
1262
+ const objectMap = new Map(effectiveObjects.map((object) => [object.id, object]));
1263
+ const relationships = buildSceneRelationships(effectiveObjects, objectMap);
1234
1264
  const positions = /* @__PURE__ */ new Map();
1235
1265
  const orbitDrafts = [];
1236
1266
  const leaderDrafts = [];
@@ -1239,7 +1269,7 @@
1239
1269
  const atObjects = [];
1240
1270
  const surfaceChildren = /* @__PURE__ */ new Map();
1241
1271
  const orbitChildren = /* @__PURE__ */ new Map();
1242
- for (const object of document2.objects) {
1272
+ for (const object of effectiveObjects) {
1243
1273
  const placement = object.placement;
1244
1274
  if (!placement) {
1245
1275
  rootObjects.push(object);
@@ -1334,11 +1364,14 @@
1334
1364
  const objects = [...positions.values()].map((position) => createSceneObject(position, scaleModel, relationships));
1335
1365
  const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
1336
1366
  const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
1337
- const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
1338
- const layers = createSceneLayers(orbitVisuals, leaders, objects, labels);
1339
- const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
1367
+ const labels = createSceneLabels(objects, width, height, scaleModel.labelMultiplier);
1368
+ const relations = createSceneRelations(document2, objects);
1369
+ const events = createSceneEvents(document2.events ?? [], objects, activeEventId);
1370
+ const layers = createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels);
1371
+ const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, scaleModel.labelMultiplier);
1372
+ const semanticGroups = createSceneSemanticGroups(document2, objects);
1340
1373
  const viewpoints = createSceneViewpoints(document2, projection, frame.preset, relationships, objectMap);
1341
- const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
1374
+ const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, scaleModel.labelMultiplier);
1342
1375
  return {
1343
1376
  width,
1344
1377
  height,
@@ -1346,7 +1379,7 @@
1346
1379
  renderPreset: frame.preset,
1347
1380
  projection,
1348
1381
  scaleModel,
1349
- title: String(document2.system?.properties.title ?? document2.system?.id ?? "WorldOrbit") || "WorldOrbit",
1382
+ title: String(document2.system?.title ?? document2.system?.properties.title ?? document2.system?.id ?? "WorldOrbit") || "WorldOrbit",
1350
1383
  subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
1351
1384
  systemId,
1352
1385
  viewMode: projection,
@@ -1362,9 +1395,13 @@
1362
1395
  contentBounds,
1363
1396
  layers,
1364
1397
  groups,
1398
+ semanticGroups,
1365
1399
  viewpoints,
1400
+ events,
1401
+ activeEventId,
1366
1402
  objects,
1367
1403
  orbitVisuals,
1404
+ relations,
1368
1405
  leaders,
1369
1406
  labels
1370
1407
  };
@@ -1380,6 +1417,35 @@
1380
1417
  y: center.y + dx * sin + dy * cos
1381
1418
  };
1382
1419
  }
1420
+ function createEffectiveObjects(objects, events, activeEventId) {
1421
+ const cloned = objects.map((object) => structuredClone(object));
1422
+ if (!activeEventId) {
1423
+ return cloned;
1424
+ }
1425
+ const activeEvent = events.find((event) => event.id === activeEventId);
1426
+ if (!activeEvent) {
1427
+ return cloned;
1428
+ }
1429
+ const objectMap = new Map(cloned.map((object) => [object.id, object]));
1430
+ for (const pose of activeEvent.positions) {
1431
+ const object = objectMap.get(pose.objectId);
1432
+ if (!object) {
1433
+ continue;
1434
+ }
1435
+ object.placement = pose.placement ? structuredClone(pose.placement) : null;
1436
+ if (pose.inner) {
1437
+ object.properties.inner = { ...pose.inner };
1438
+ } else {
1439
+ delete object.properties.inner;
1440
+ }
1441
+ if (pose.outer) {
1442
+ object.properties.outer = { ...pose.outer };
1443
+ } else {
1444
+ delete object.properties.outer;
1445
+ }
1446
+ }
1447
+ return cloned;
1448
+ }
1383
1449
  function resolveLayoutPreset(document2) {
1384
1450
  const rawScale = String(document2.system?.properties.scale ?? "balanced").toLowerCase();
1385
1451
  switch (rawScale) {
@@ -1474,6 +1540,7 @@
1474
1540
  }
1475
1541
  function createSceneObject(position, scaleModel, relationships) {
1476
1542
  const { object, x, y, radius, sortKey, anchorX, anchorY } = position;
1543
+ const renderPriority = object.renderHints?.renderPriority ?? 0;
1477
1544
  return {
1478
1545
  renderId: createRenderId(object.id),
1479
1546
  objectId: object.id,
@@ -1482,11 +1549,12 @@
1482
1549
  ancestorIds: relationships.ancestorIds.get(object.id) ?? [],
1483
1550
  childIds: relationships.childIds.get(object.id) ?? [],
1484
1551
  groupId: relationships.groupIds.get(object.id) ?? null,
1552
+ semanticGroupIds: [...object.groups ?? []],
1485
1553
  x,
1486
1554
  y,
1487
1555
  radius,
1488
1556
  visualRadius: visualExtentForObject(object, radius, scaleModel),
1489
- sortKey,
1557
+ sortKey: sortKey + renderPriority * 1e-3,
1490
1558
  anchorX,
1491
1559
  anchorY,
1492
1560
  label: object.id,
@@ -1503,6 +1571,7 @@
1503
1571
  object: draft.object,
1504
1572
  parentId: draft.parentId,
1505
1573
  groupId,
1574
+ semanticGroupIds: [...draft.object.groups ?? []],
1506
1575
  kind: draft.kind,
1507
1576
  cx: draft.cx,
1508
1577
  cy: draft.cy,
@@ -1514,7 +1583,7 @@
1514
1583
  bandThickness: draft.bandThickness,
1515
1584
  frontArcPath: draft.frontArcPath,
1516
1585
  backArcPath: draft.backArcPath,
1517
- hidden: draft.object.properties.hidden === true
1586
+ hidden: draft.object.properties.hidden === true || draft.object.renderHints?.renderOrbit === false
1518
1587
  };
1519
1588
  }
1520
1589
  function createLeaderLine(draft) {
@@ -1523,6 +1592,7 @@
1523
1592
  objectId: draft.object.id,
1524
1593
  object: draft.object,
1525
1594
  groupId: draft.groupId,
1595
+ semanticGroupIds: [...draft.object.groups ?? []],
1526
1596
  x1: draft.x1,
1527
1597
  y1: draft.y1,
1528
1598
  x2: draft.x2,
@@ -1531,42 +1601,144 @@
1531
1601
  hidden: draft.object.properties.hidden === true
1532
1602
  };
1533
1603
  }
1534
- function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1604
+ function createSceneLabels(objects, sceneWidth, sceneHeight, labelMultiplier) {
1535
1605
  const labels = [];
1536
1606
  const occupied = [];
1537
- const visibleObjects = [...objects].filter((object) => !object.hidden).sort((left, right) => left.sortKey - right.sortKey);
1607
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1608
+ const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort(compareLabelPlacementOrder);
1538
1609
  for (const object of visibleObjects) {
1539
- const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1540
- const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
1541
- let labelY = object.y + direction * (object.radius + 18 * labelMultiplier);
1542
- let secondaryY = labelY + direction * (16 * labelMultiplier);
1543
- let bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1544
- let attempts = 0;
1545
- while (occupied.some((entry) => rectsOverlap(entry, bounds)) && attempts < 10) {
1546
- labelY += direction * 14 * labelMultiplier;
1547
- secondaryY += direction * 14 * labelMultiplier;
1548
- bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1549
- attempts += 1;
1550
- }
1551
- occupied.push(bounds);
1610
+ const placement = selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) ?? createLabelPlacement(object, defaultVerticalDirection(object, objectMap.get(object.parentId ?? "") ?? null, sceneHeight), 0, labelMultiplier);
1611
+ occupied.push(createLabelRect(object, placement, labelMultiplier));
1552
1612
  labels.push({
1553
1613
  renderId: `${object.renderId}-label`,
1554
1614
  objectId: object.objectId,
1555
1615
  object: object.object,
1556
1616
  groupId: object.groupId,
1617
+ semanticGroupIds: [...object.semanticGroupIds],
1557
1618
  label: object.label,
1558
1619
  secondaryLabel: object.secondaryLabel,
1559
- x: object.x,
1560
- y: labelY,
1561
- secondaryY,
1562
- textAnchor: "middle",
1563
- direction: direction < 0 ? "above" : "below",
1620
+ x: placement.x,
1621
+ y: placement.labelY,
1622
+ secondaryY: placement.secondaryY,
1623
+ textAnchor: placement.textAnchor,
1624
+ direction: placement.direction,
1564
1625
  hidden: object.hidden
1565
1626
  });
1566
1627
  }
1567
1628
  return labels;
1568
1629
  }
1569
- function createSceneLayers(orbitVisuals, leaders, objects, labels) {
1630
+ function compareLabelPlacementOrder(left, right) {
1631
+ const priorityDiff = labelPlacementPriority(left) - labelPlacementPriority(right);
1632
+ if (priorityDiff !== 0) {
1633
+ return priorityDiff;
1634
+ }
1635
+ const renderPriorityDiff = (right.object.renderHints?.renderPriority ?? 0) - (left.object.renderHints?.renderPriority ?? 0);
1636
+ if (renderPriorityDiff !== 0) {
1637
+ return renderPriorityDiff;
1638
+ }
1639
+ return left.sortKey - right.sortKey;
1640
+ }
1641
+ function labelPlacementPriority(object) {
1642
+ switch (object.object.type) {
1643
+ case "star":
1644
+ return 0;
1645
+ case "planet":
1646
+ return 1;
1647
+ case "moon":
1648
+ return 2;
1649
+ case "belt":
1650
+ case "ring":
1651
+ return 3;
1652
+ case "asteroid":
1653
+ case "comet":
1654
+ return 4;
1655
+ case "structure":
1656
+ case "phenomenon":
1657
+ return 5;
1658
+ }
1659
+ }
1660
+ function selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) {
1661
+ for (const direction of preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight)) {
1662
+ const maxAttempts = direction === "left" || direction === "right" ? 4 : 6;
1663
+ for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
1664
+ const placement = createLabelPlacement(object, direction, attempt, labelMultiplier);
1665
+ const rect = createLabelRect(object, placement, labelMultiplier);
1666
+ if (!occupied.some((entry) => rectsOverlap(entry, rect))) {
1667
+ return placement;
1668
+ }
1669
+ }
1670
+ }
1671
+ return null;
1672
+ }
1673
+ function preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight) {
1674
+ const parent = object.parentId ? objectMap.get(object.parentId) ?? null : null;
1675
+ const vertical = defaultVerticalDirection(object, parent, sceneHeight);
1676
+ const oppositeVertical = vertical === "below" ? "above" : "below";
1677
+ const horizontal = defaultHorizontalDirection(object, parent, sceneWidth);
1678
+ const oppositeHorizontal = horizontal === "right" ? "left" : "right";
1679
+ 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";
1680
+ return preferHorizontal ? [horizontal, vertical, oppositeHorizontal, oppositeVertical] : [vertical, horizontal, oppositeVertical, oppositeHorizontal];
1681
+ }
1682
+ function defaultVerticalDirection(object, parent, sceneHeight) {
1683
+ if (parent && Math.abs(object.y - parent.y) > 6) {
1684
+ return object.y >= parent.y ? "below" : "above";
1685
+ }
1686
+ return object.y > sceneHeight * 0.62 ? "above" : "below";
1687
+ }
1688
+ function defaultHorizontalDirection(object, parent, sceneWidth) {
1689
+ if (parent && Math.abs(object.x - parent.x) > 6) {
1690
+ return object.x >= parent.x ? "right" : "left";
1691
+ }
1692
+ return object.x >= sceneWidth / 2 ? "right" : "left";
1693
+ }
1694
+ function createLabelPlacement(object, direction, attempt, labelMultiplier) {
1695
+ const step = 14 * labelMultiplier;
1696
+ switch (direction) {
1697
+ case "above": {
1698
+ const labelY = object.y - (object.radius + 18 * labelMultiplier + attempt * step);
1699
+ return {
1700
+ x: object.x,
1701
+ labelY,
1702
+ secondaryY: labelY - 16 * labelMultiplier,
1703
+ textAnchor: "middle",
1704
+ direction
1705
+ };
1706
+ }
1707
+ case "below": {
1708
+ const labelY = object.y + object.radius + 18 * labelMultiplier + attempt * step;
1709
+ return {
1710
+ x: object.x,
1711
+ labelY,
1712
+ secondaryY: labelY + 16 * labelMultiplier,
1713
+ textAnchor: "middle",
1714
+ direction
1715
+ };
1716
+ }
1717
+ case "left": {
1718
+ const x = object.x - (object.visualRadius + 16 * labelMultiplier + attempt * step);
1719
+ const labelY = object.y - 4 * labelMultiplier;
1720
+ return {
1721
+ x,
1722
+ labelY,
1723
+ secondaryY: labelY + 16 * labelMultiplier,
1724
+ textAnchor: "end",
1725
+ direction
1726
+ };
1727
+ }
1728
+ case "right": {
1729
+ const x = object.x + object.visualRadius + 16 * labelMultiplier + attempt * step;
1730
+ const labelY = object.y - 4 * labelMultiplier;
1731
+ return {
1732
+ x,
1733
+ labelY,
1734
+ secondaryY: labelY + 16 * labelMultiplier,
1735
+ textAnchor: "start",
1736
+ direction
1737
+ };
1738
+ }
1739
+ }
1740
+ }
1741
+ function createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels) {
1570
1742
  const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1571
1743
  const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1572
1744
  return [
@@ -1577,6 +1749,14 @@
1577
1749
  },
1578
1750
  { id: "orbits-back", renderIds: backOrbitIds },
1579
1751
  { id: "orbits-front", renderIds: frontOrbitIds },
1752
+ {
1753
+ id: "relations",
1754
+ renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId)
1755
+ },
1756
+ {
1757
+ id: "events",
1758
+ renderIds: events.filter((event) => !event.hidden).map((event) => event.renderId)
1759
+ },
1580
1760
  {
1581
1761
  id: "objects",
1582
1762
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
@@ -1588,7 +1768,7 @@
1588
1768
  { id: "metadata", renderIds: ["wo-title", "wo-subtitle", "wo-meta"] }
1589
1769
  ];
1590
1770
  }
1591
- function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships) {
1771
+ function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, labelMultiplier) {
1592
1772
  const groups = /* @__PURE__ */ new Map();
1593
1773
  const ensureGroup = (groupId) => {
1594
1774
  if (!groupId) {
@@ -1637,10 +1817,63 @@
1637
1817
  }
1638
1818
  }
1639
1819
  for (const group of groups.values()) {
1640
- group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels);
1820
+ group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier);
1641
1821
  }
1642
1822
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1643
1823
  }
1824
+ function createSceneSemanticGroups(document2, objects) {
1825
+ return [...document2.groups].map((group) => ({
1826
+ id: group.id,
1827
+ label: group.label,
1828
+ summary: group.summary,
1829
+ color: group.color,
1830
+ tags: [...group.tags],
1831
+ hidden: group.hidden,
1832
+ objectIds: objects.filter((object) => !object.hidden && object.semanticGroupIds.includes(group.id)).map((object) => object.objectId)
1833
+ })).sort((left, right) => left.label.localeCompare(right.label));
1834
+ }
1835
+ function createSceneRelations(document2, objects) {
1836
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1837
+ return document2.relations.map((relation) => {
1838
+ const from = objectMap.get(relation.from);
1839
+ const to = objectMap.get(relation.to);
1840
+ return {
1841
+ renderId: `${createRenderId(relation.id)}-relation`,
1842
+ relationId: relation.id,
1843
+ relation,
1844
+ fromObjectId: relation.from,
1845
+ toObjectId: relation.to,
1846
+ x1: from?.x ?? 0,
1847
+ y1: from?.y ?? 0,
1848
+ x2: to?.x ?? 0,
1849
+ y2: to?.y ?? 0,
1850
+ hidden: relation.hidden || !from || !to || from.hidden || to.hidden
1851
+ };
1852
+ }).sort((left, right) => left.relation.id.localeCompare(right.relation.id));
1853
+ }
1854
+ function createSceneEvents(events, objects, activeEventId) {
1855
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1856
+ return events.map((event) => {
1857
+ const objectIds = [.../* @__PURE__ */ new Set([
1858
+ ...event.targetObjectId ? [event.targetObjectId] : [],
1859
+ ...event.participantObjectIds
1860
+ ])];
1861
+ const positions = objectIds.map((objectId) => objectMap.get(objectId)).filter(Boolean);
1862
+ const centroidX = positions.length > 0 ? positions.reduce((sum, object) => sum + object.x, 0) / positions.length : 0;
1863
+ const centroidY = positions.length > 0 ? positions.reduce((sum, object) => sum + object.y, 0) / positions.length : 0;
1864
+ return {
1865
+ renderId: `${createRenderId(event.id)}-event`,
1866
+ eventId: event.id,
1867
+ event,
1868
+ objectIds,
1869
+ participantIds: [...event.participantObjectIds],
1870
+ targetObjectId: event.targetObjectId,
1871
+ x: centroidX,
1872
+ y: centroidY,
1873
+ hidden: event.hidden || positions.length === 0 || positions.every((object) => object.hidden) || activeEventId !== null && event.id !== activeEventId
1874
+ };
1875
+ }).sort((left, right) => left.event.id.localeCompare(right.event.id));
1876
+ }
1644
1877
  function createSceneViewpoints(document2, projection, preset, relationships, objectMap) {
1645
1878
  const generatedOverview = createGeneratedOverviewViewpoint(document2, projection, preset);
1646
1879
  const drafts = /* @__PURE__ */ new Map();
@@ -1658,7 +1891,7 @@
1658
1891
  }
1659
1892
  const field = fieldParts.join(".").toLowerCase();
1660
1893
  const draft = drafts.get(id) ?? { id };
1661
- applyViewpointField(draft, field, value, projection, preset, relationships, objectMap);
1894
+ applyViewpointField(draft, field, value, document2, projection, preset, relationships, objectMap);
1662
1895
  drafts.set(id, draft);
1663
1896
  }
1664
1897
  const viewpoints = [...drafts.values()].map((draft) => finalizeViewpointDraft(draft, projection, preset, objectMap)).filter(Boolean);
@@ -1686,13 +1919,15 @@
1686
1919
  });
1687
1920
  }
1688
1921
  function createGeneratedOverviewViewpoint(document2, projection, preset) {
1689
- const label = document2.system?.properties.title ? `${String(document2.system.properties.title)} Overview` : "Overview";
1922
+ const title = document2.system?.title ?? document2.system?.properties.title;
1923
+ const label = title ? `${String(title)} Overview` : "Overview";
1690
1924
  return {
1691
1925
  id: "overview",
1692
1926
  label,
1693
1927
  summary: "Fit the whole system with the current atlas defaults.",
1694
1928
  objectId: null,
1695
1929
  selectedObjectId: null,
1930
+ eventIds: [],
1696
1931
  projection,
1697
1932
  preset,
1698
1933
  rotationDeg: 0,
@@ -1702,7 +1937,7 @@
1702
1937
  generated: true
1703
1938
  };
1704
1939
  }
1705
- function applyViewpointField(draft, field, value, projection, preset, relationships, objectMap) {
1940
+ function applyViewpointField(draft, field, value, document2, projection, preset, relationships, objectMap) {
1706
1941
  const normalizedValue = value.trim();
1707
1942
  switch (field) {
1708
1943
  case "label":
@@ -1729,6 +1964,9 @@
1729
1964
  draft.select = normalizedValue;
1730
1965
  }
1731
1966
  return;
1967
+ case "events":
1968
+ draft.eventIds = splitListValue(normalizedValue);
1969
+ return;
1732
1970
  case "projection":
1733
1971
  case "view":
1734
1972
  draft.projection = parseViewProjection(normalizedValue) ?? projection;
@@ -1769,7 +2007,7 @@
1769
2007
  case "groups":
1770
2008
  draft.filter = {
1771
2009
  ...draft.filter ?? createEmptyViewpointFilter(),
1772
- groupIds: parseViewpointGroups(normalizedValue, relationships, objectMap)
2010
+ groupIds: parseViewpointGroups(normalizedValue, document2, relationships, objectMap)
1773
2011
  };
1774
2012
  return;
1775
2013
  }
@@ -1785,6 +2023,7 @@
1785
2023
  summary: draft.summary?.trim() || createViewpointSummary(label, objectId, filter),
1786
2024
  objectId,
1787
2025
  selectedObjectId,
2026
+ eventIds: [...new Set(draft.eventIds ?? [])],
1788
2027
  projection: draft.projection ?? projection,
1789
2028
  preset: draft.preset ?? preset,
1790
2029
  rotationDeg: draft.rotationDeg ?? 0,
@@ -1842,7 +2081,7 @@
1842
2081
  next["orbits-front"] = enabled;
1843
2082
  continue;
1844
2083
  }
1845
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
2084
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "events" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1846
2085
  next[rawLayer] = enabled;
1847
2086
  }
1848
2087
  }
@@ -1851,8 +2090,11 @@
1851
2090
  function parseViewpointObjectTypes(value) {
1852
2091
  return splitListValue(value).filter((entry) => entry === "star" || entry === "planet" || entry === "moon" || entry === "belt" || entry === "asteroid" || entry === "comet" || entry === "ring" || entry === "structure" || entry === "phenomenon");
1853
2092
  }
1854
- function parseViewpointGroups(value, relationships, objectMap) {
2093
+ function parseViewpointGroups(value, document2, relationships, objectMap) {
1855
2094
  return splitListValue(value).map((entry) => {
2095
+ if (document2.schemaVersion === "2.1" || document2.groups.some((group) => group.id === entry)) {
2096
+ return entry;
2097
+ }
1856
2098
  if (entry.startsWith("wo-") && entry.endsWith("-group")) {
1857
2099
  return entry;
1858
2100
  }
@@ -1887,7 +2129,7 @@
1887
2129
  }
1888
2130
  return parts.join(" - ");
1889
2131
  }
1890
- function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels) {
2132
+ function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, labelMultiplier) {
1891
2133
  let minX = Number.POSITIVE_INFINITY;
1892
2134
  let minY = Number.POSITIVE_INFINITY;
1893
2135
  let maxX = Number.NEGATIVE_INFINITY;
@@ -1917,7 +2159,7 @@
1917
2159
  for (const label of labels) {
1918
2160
  if (label.hidden)
1919
2161
  continue;
1920
- includeLabelBounds(label, include);
2162
+ includeLabelBounds(label, include, labelMultiplier);
1921
2163
  }
1922
2164
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
1923
2165
  return createBounds(0, 0, width, height);
@@ -1955,13 +2197,10 @@
1955
2197
  include(object.x - object.visualRadius - 24, object.y - object.visualRadius - 16);
1956
2198
  include(object.x + object.visualRadius + 24, object.y + object.visualRadius + 36);
1957
2199
  }
1958
- function includeLabelBounds(label, include) {
1959
- const labelScale = 1;
1960
- const labelHalfWidth = estimateLabelHalfWidthFromText(label.label, label.secondaryLabel, labelScale);
1961
- include(label.x - labelHalfWidth, label.y - 18);
1962
- include(label.x + labelHalfWidth, label.y + 8);
1963
- include(label.x - labelHalfWidth, label.secondaryY - 14);
1964
- include(label.x + labelHalfWidth, label.secondaryY + 8);
2200
+ function includeLabelBounds(label, include, labelMultiplier) {
2201
+ const bounds = createLabelRectFromText(label.x, label.y, label.secondaryY, label.textAnchor, label.direction, label.label, label.secondaryLabel, labelMultiplier);
2202
+ include(bounds.left, bounds.top);
2203
+ include(bounds.right, bounds.bottom);
1965
2204
  }
1966
2205
  function placeObject(object, x, y, depth, positions, orbitDrafts, leaderDrafts, context) {
1967
2206
  if (positions.has(object.id)) {
@@ -1983,8 +2222,9 @@
1983
2222
  }
1984
2223
  const orbiting = [...context.orbitChildren.get(object.id) ?? []].sort(compareOrbiting);
1985
2224
  const orbitMetricContext = computeOrbitMetricContext(orbiting, parent.radius, context.spacingFactor, context.scaleModel);
2225
+ const orbitRadiiPx = resolveOrbitRadiiPx(orbiting, orbitMetricContext);
1986
2226
  orbiting.forEach((child, index) => {
1987
- const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, context);
2227
+ const orbitGeometry = resolveOrbitGeometry(child, index, orbiting.length, parent, orbitMetricContext, orbitRadiiPx[index] ?? orbitMetricContext.innerPx, context);
1988
2228
  orbitDrafts.push({
1989
2229
  object: child,
1990
2230
  parentId: object.id,
@@ -2058,7 +2298,8 @@
2058
2298
  metricSpread: 0,
2059
2299
  innerPx,
2060
2300
  stepPx,
2061
- pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx)
2301
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
2302
+ minimumGapPx: stepPx * 0.42
2062
2303
  };
2063
2304
  }
2064
2305
  const minMetric = Math.min(...presentMetrics);
@@ -2071,10 +2312,11 @@
2071
2312
  metricSpread,
2072
2313
  innerPx,
2073
2314
  stepPx,
2074
- pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx)
2315
+ pixelSpread: Math.max(stepPx * Math.max(objects.length - 1, 1), stepPx),
2316
+ minimumGapPx: stepPx * 0.42
2075
2317
  };
2076
2318
  }
2077
- function resolveOrbitGeometry(object, index, count, parent, metricContext, context) {
2319
+ function resolveOrbitGeometry(object, index, count, parent, metricContext, orbitRadiusPx, context) {
2078
2320
  const placement = object.placement;
2079
2321
  const band = object.type === "belt" || object.type === "ring";
2080
2322
  if (!placement || placement.mode !== "orbit") {
@@ -2092,7 +2334,7 @@
2092
2334
  };
2093
2335
  }
2094
2336
  const eccentricity = clampNumber(typeof placement.eccentricity === "number" ? placement.eccentricity : 0, 0, 0.92);
2095
- const semiMajor = resolveOrbitRadiusPx(object, index, metricContext);
2337
+ const semiMajor = orbitRadiusPx;
2096
2338
  const baseMinor = Math.max(semiMajor * Math.sqrt(1 - eccentricity * eccentricity), semiMajor * 0.18);
2097
2339
  const inclinationDeg = unitValueToDegrees(placement.inclination) ?? 0;
2098
2340
  const inclinationScale = context.projection === "isometric" ? Math.max(MIN_ISO_MINOR_SCALE, Math.cos(degreesToRadians(inclinationDeg))) * ISO_FLATTENING : 1;
@@ -2122,15 +2364,19 @@
2122
2364
  objectY: objectPoint.y
2123
2365
  };
2124
2366
  }
2125
- function resolveOrbitRadiusPx(object, index, metricContext) {
2126
- const metric = orbitMetric(object);
2127
- if (metric === null) {
2128
- return metricContext.innerPx + index * metricContext.stepPx;
2129
- }
2130
- if (metricContext.metricSpread > 0) {
2131
- return metricContext.innerPx + (metric - metricContext.minMetric) / metricContext.metricSpread * metricContext.pixelSpread;
2132
- }
2133
- return metricContext.innerPx + Math.log10(metric + 1) * metricContext.stepPx;
2367
+ function resolveOrbitRadiusPx(metric, metricContext) {
2368
+ return metricContext.innerPx + metricContext.stepPx * log2(Math.max(metric, 0) + 1);
2369
+ }
2370
+ function resolveOrbitRadiiPx(objects, metricContext) {
2371
+ const radii = [];
2372
+ objects.forEach((object, index) => {
2373
+ const metric = orbitMetric(object);
2374
+ const fallbackRadius = metricContext.innerPx + index * metricContext.stepPx;
2375
+ const baseRadius = metric === null ? fallbackRadius : resolveOrbitRadiusPx(metric, metricContext);
2376
+ const minimumRadius = index === 0 ? metricContext.innerPx : (radii[index - 1] ?? metricContext.innerPx) + metricContext.minimumGapPx;
2377
+ radii.push(Math.max(baseRadius, minimumRadius));
2378
+ });
2379
+ return radii;
2134
2380
  }
2135
2381
  function orbitMetric(object) {
2136
2382
  if (!object.placement || object.placement.mode !== "orbit") {
@@ -2138,6 +2384,9 @@
2138
2384
  }
2139
2385
  return toDistanceMetric(object.placement.semiMajor ?? object.placement.distance ?? null);
2140
2386
  }
2387
+ function log2(value) {
2388
+ return Math.log(value) / Math.log(2);
2389
+ }
2141
2390
  function resolveOrbitPhase(phase, index, count) {
2142
2391
  const degreeValue = phase ? unitValueToDegrees(phase) : null;
2143
2392
  if (degreeValue !== null) {
@@ -2341,7 +2590,7 @@
2341
2590
  return null;
2342
2591
  }
2343
2592
  }
2344
- function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels) {
2593
+ function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier) {
2345
2594
  let minX = Number.POSITIVE_INFINITY;
2346
2595
  let minY = Number.POSITIVE_INFINITY;
2347
2596
  let maxX = Number.NEGATIVE_INFINITY;
@@ -2370,7 +2619,7 @@
2370
2619
  }
2371
2620
  for (const label of labels) {
2372
2621
  if (!label.hidden && group.labelIds.includes(label.objectId)) {
2373
- includeLabelBounds(label, include);
2622
+ includeLabelBounds(label, include, labelMultiplier);
2374
2623
  }
2375
2624
  }
2376
2625
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
@@ -2395,12 +2644,28 @@
2395
2644
  }
2396
2645
  return current.id;
2397
2646
  }
2398
- function createLabelRect(x, labelY, secondaryY, labelHalfWidth, direction) {
2647
+ function createLabelRect(object, placement, labelMultiplier) {
2648
+ return createLabelRectFromText(placement.x, placement.labelY, placement.secondaryY, placement.textAnchor, placement.direction, object.label, object.secondaryLabel, labelMultiplier);
2649
+ }
2650
+ function createLabelRectFromText(x, labelY, secondaryY, textAnchor, direction, label, secondaryLabel, labelMultiplier) {
2651
+ const labelHalfWidth = estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier);
2652
+ const labelWidth = labelHalfWidth * 2;
2653
+ const topPadding = direction === "above" ? 18 : 12;
2654
+ const bottomPadding = direction === "above" ? 8 : 12;
2655
+ let left = x - labelHalfWidth;
2656
+ let right = x + labelHalfWidth;
2657
+ if (textAnchor === "start") {
2658
+ left = x;
2659
+ right = x + labelWidth;
2660
+ } else if (textAnchor === "end") {
2661
+ left = x - labelWidth;
2662
+ right = x;
2663
+ }
2399
2664
  return {
2400
- left: x - labelHalfWidth,
2401
- right: x + labelHalfWidth,
2402
- top: Math.min(labelY, secondaryY) - (direction < 0 ? 18 : 12),
2403
- bottom: Math.max(labelY, secondaryY) + (direction < 0 ? 8 : 12)
2665
+ left,
2666
+ right,
2667
+ top: Math.min(labelY, secondaryY) - topPadding,
2668
+ bottom: Math.max(labelY, secondaryY) + bottomPadding
2404
2669
  };
2405
2670
  }
2406
2671
  function rectsOverlap(left, right) {
@@ -2461,8 +2726,18 @@
2461
2726
  return value.value;
2462
2727
  case "km":
2463
2728
  return value.value / AU_IN_KM;
2729
+ case "m":
2730
+ return value.value / 1e3 / AU_IN_KM;
2731
+ case "ly":
2732
+ return value.value * LY_IN_AU;
2733
+ case "pc":
2734
+ return value.value * PC_IN_AU;
2735
+ case "kpc":
2736
+ return value.value * KPC_IN_AU;
2464
2737
  case "re":
2465
2738
  return value.value * EARTH_RADIUS_IN_KM / AU_IN_KM;
2739
+ case "rj":
2740
+ return value.value * JUPITER_RADIUS_IN_KM / AU_IN_KM;
2466
2741
  case "sol":
2467
2742
  return value.value * SOLAR_RADIUS_IN_KM / AU_IN_KM;
2468
2743
  default:
@@ -2577,11 +2852,6 @@
2577
2852
  function customColorFor(value) {
2578
2853
  return typeof value === "string" && value.trim() ? value : void 0;
2579
2854
  }
2580
- function estimateLabelHalfWidth(object, labelMultiplier) {
2581
- const primaryWidth = object.label.length * 4.6 * labelMultiplier + 18;
2582
- const secondaryWidth = object.secondaryLabel.length * 3.9 * labelMultiplier + 18;
2583
- return Math.max(primaryWidth, secondaryWidth, object.visualRadius + 18);
2584
- }
2585
2855
  function estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier) {
2586
2856
  const primaryWidth = label.length * 4.6 * labelMultiplier + 18;
2587
2857
  const secondaryWidth = secondaryLabel.length * 3.9 * labelMultiplier + 18;
@@ -2598,28 +2868,95 @@
2598
2868
  }
2599
2869
 
2600
2870
  // packages/core/dist/draft.js
2601
- function materializeAtlasDocument(document2) {
2871
+ function materializeAtlasDocument(document2, options = {}) {
2602
2872
  const system = document2.system ? {
2603
2873
  type: "system",
2604
2874
  id: document2.system.id,
2875
+ title: document2.system.title,
2876
+ description: document2.system.description,
2877
+ epoch: document2.system.epoch,
2878
+ referencePlane: document2.system.referencePlane,
2605
2879
  properties: materializeDraftSystemProperties(document2.system),
2606
2880
  info: materializeDraftSystemInfo(document2.system)
2607
2881
  } : null;
2882
+ const objects = document2.objects.map(cloneWorldOrbitObject);
2883
+ applyEventPoseOverrides(objects, document2.events ?? [], options.activeEventId ?? null);
2608
2884
  return {
2609
2885
  format: "worldorbit",
2610
2886
  version: "1.0",
2887
+ schemaVersion: document2.version,
2611
2888
  system,
2612
- objects: document2.objects.map(cloneWorldOrbitObject)
2889
+ groups: structuredClone(document2.groups ?? []),
2890
+ relations: structuredClone(document2.relations ?? []),
2891
+ events: document2.events.map(cloneWorldOrbitEvent),
2892
+ objects
2613
2893
  };
2614
2894
  }
2615
2895
  function cloneWorldOrbitObject(object) {
2616
2896
  return {
2617
2897
  ...object,
2898
+ groups: object.groups ? [...object.groups] : void 0,
2899
+ resonance: object.resonance ? { ...object.resonance } : object.resonance,
2900
+ renderHints: object.renderHints ? { ...object.renderHints } : object.renderHints,
2901
+ deriveRules: object.deriveRules ? object.deriveRules.map((rule) => ({ ...rule })) : void 0,
2902
+ validationRules: object.validationRules ? object.validationRules.map((rule) => ({ ...rule })) : void 0,
2903
+ lockedFields: object.lockedFields ? [...object.lockedFields] : void 0,
2904
+ tolerances: object.tolerances ? object.tolerances.map((entry) => ({
2905
+ field: entry.field,
2906
+ 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
2907
+ })) : void 0,
2908
+ typedBlocks: object.typedBlocks ? Object.fromEntries(Object.entries(object.typedBlocks).map(([key, block]) => [key, { ...block ?? {} }])) : void 0,
2618
2909
  properties: cloneProperties(object.properties),
2619
2910
  placement: object.placement ? structuredClone(object.placement) : null,
2620
2911
  info: { ...object.info }
2621
2912
  };
2622
2913
  }
2914
+ function cloneWorldOrbitEvent(event) {
2915
+ return {
2916
+ ...event,
2917
+ participantObjectIds: [...event.participantObjectIds],
2918
+ tags: [...event.tags],
2919
+ positions: event.positions.map(cloneWorldOrbitEventPose)
2920
+ };
2921
+ }
2922
+ function cloneWorldOrbitEventPose(pose) {
2923
+ return {
2924
+ objectId: pose.objectId,
2925
+ placement: clonePlacement(pose.placement),
2926
+ inner: pose.inner ? { ...pose.inner } : void 0,
2927
+ outer: pose.outer ? { ...pose.outer } : void 0
2928
+ };
2929
+ }
2930
+ function clonePlacement(placement) {
2931
+ return placement ? structuredClone(placement) : null;
2932
+ }
2933
+ function applyEventPoseOverrides(objects, events, activeEventId) {
2934
+ if (!activeEventId) {
2935
+ return;
2936
+ }
2937
+ const event = events.find((entry) => entry.id === activeEventId);
2938
+ if (!event) {
2939
+ return;
2940
+ }
2941
+ const objectMap = new Map(objects.map((object) => [object.id, object]));
2942
+ for (const pose of event.positions) {
2943
+ const object = objectMap.get(pose.objectId);
2944
+ if (!object) {
2945
+ continue;
2946
+ }
2947
+ object.placement = clonePlacement(pose.placement);
2948
+ if (pose.inner) {
2949
+ object.properties.inner = { ...pose.inner };
2950
+ } else {
2951
+ delete object.properties.inner;
2952
+ }
2953
+ if (pose.outer) {
2954
+ object.properties.outer = { ...pose.outer };
2955
+ } else {
2956
+ delete object.properties.outer;
2957
+ }
2958
+ }
2959
+ }
2623
2960
  function cloneProperties(properties) {
2624
2961
  const next = {};
2625
2962
  for (const [key, value] of Object.entries(properties)) {
@@ -2650,71 +2987,83 @@
2650
2987
  if (system.defaults.units) {
2651
2988
  properties.units = system.defaults.units;
2652
2989
  }
2990
+ if (system.description) {
2991
+ properties.description = system.description;
2992
+ }
2993
+ if (system.epoch) {
2994
+ properties.epoch = system.epoch;
2995
+ }
2996
+ if (system.referencePlane) {
2997
+ properties.referencePlane = system.referencePlane;
2998
+ }
2653
2999
  return properties;
2654
3000
  }
2655
3001
  function materializeDraftSystemInfo(system) {
2656
- const info = {
3002
+ const info2 = {
2657
3003
  ...system.atlasMetadata
2658
3004
  };
2659
3005
  if (system.defaults.theme) {
2660
- info["atlas.theme"] = system.defaults.theme;
3006
+ info2["atlas.theme"] = system.defaults.theme;
2661
3007
  }
2662
3008
  for (const viewpoint of system.viewpoints) {
2663
3009
  const prefix = `viewpoint.${viewpoint.id}`;
2664
- info[`${prefix}.label`] = viewpoint.label;
3010
+ info2[`${prefix}.label`] = viewpoint.label;
2665
3011
  if (viewpoint.summary) {
2666
- info[`${prefix}.summary`] = viewpoint.summary;
3012
+ info2[`${prefix}.summary`] = viewpoint.summary;
2667
3013
  }
2668
3014
  if (viewpoint.focusObjectId) {
2669
- info[`${prefix}.focus`] = viewpoint.focusObjectId;
3015
+ info2[`${prefix}.focus`] = viewpoint.focusObjectId;
2670
3016
  }
2671
3017
  if (viewpoint.selectedObjectId) {
2672
- info[`${prefix}.select`] = viewpoint.selectedObjectId;
3018
+ info2[`${prefix}.select`] = viewpoint.selectedObjectId;
2673
3019
  }
2674
3020
  if (viewpoint.projection) {
2675
- info[`${prefix}.projection`] = viewpoint.projection;
3021
+ info2[`${prefix}.projection`] = viewpoint.projection;
2676
3022
  }
2677
3023
  if (viewpoint.preset) {
2678
- info[`${prefix}.preset`] = viewpoint.preset;
3024
+ info2[`${prefix}.preset`] = viewpoint.preset;
2679
3025
  }
2680
3026
  if (viewpoint.zoom !== null) {
2681
- info[`${prefix}.zoom`] = String(viewpoint.zoom);
3027
+ info2[`${prefix}.zoom`] = String(viewpoint.zoom);
2682
3028
  }
2683
3029
  if (viewpoint.rotationDeg !== 0) {
2684
- info[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
3030
+ info2[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2685
3031
  }
2686
3032
  const serializedLayers = serializeViewpointLayers(viewpoint.layers);
2687
3033
  if (serializedLayers) {
2688
- info[`${prefix}.layers`] = serializedLayers;
3034
+ info2[`${prefix}.layers`] = serializedLayers;
2689
3035
  }
2690
3036
  if (viewpoint.filter?.query) {
2691
- info[`${prefix}.query`] = viewpoint.filter.query;
3037
+ info2[`${prefix}.query`] = viewpoint.filter.query;
2692
3038
  }
2693
3039
  if ((viewpoint.filter?.objectTypes.length ?? 0) > 0) {
2694
- info[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
3040
+ info2[`${prefix}.types`] = viewpoint.filter?.objectTypes.join(" ") ?? "";
2695
3041
  }
2696
3042
  if ((viewpoint.filter?.tags.length ?? 0) > 0) {
2697
- info[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
3043
+ info2[`${prefix}.tags`] = viewpoint.filter?.tags.join(" ") ?? "";
2698
3044
  }
2699
3045
  if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2700
- info[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
3046
+ info2[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
3047
+ }
3048
+ if (viewpoint.events.length > 0) {
3049
+ info2[`${prefix}.events`] = viewpoint.events.join(" ");
2701
3050
  }
2702
3051
  }
2703
3052
  for (const annotation of system.annotations) {
2704
3053
  const prefix = `annotation.${annotation.id}`;
2705
- info[`${prefix}.label`] = annotation.label;
3054
+ info2[`${prefix}.label`] = annotation.label;
2706
3055
  if (annotation.targetObjectId) {
2707
- info[`${prefix}.target`] = annotation.targetObjectId;
3056
+ info2[`${prefix}.target`] = annotation.targetObjectId;
2708
3057
  }
2709
- info[`${prefix}.body`] = annotation.body;
3058
+ info2[`${prefix}.body`] = annotation.body;
2710
3059
  if (annotation.tags.length > 0) {
2711
- info[`${prefix}.tags`] = annotation.tags.join(" ");
3060
+ info2[`${prefix}.tags`] = annotation.tags.join(" ");
2712
3061
  }
2713
3062
  if (annotation.sourceObjectId) {
2714
- info[`${prefix}.source`] = annotation.sourceObjectId;
3063
+ info2[`${prefix}.source`] = annotation.sourceObjectId;
2715
3064
  }
2716
3065
  }
2717
- return info;
3066
+ return info2;
2718
3067
  }
2719
3068
  function serializeViewpointLayers(layers) {
2720
3069
  const tokens = [];
@@ -2723,7 +3072,7 @@
2723
3072
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2724
3073
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2725
3074
  }
2726
- for (const key of ["background", "guides", "objects", "labels", "metadata"]) {
3075
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
2727
3076
  if (layers[key] !== void 0) {
2728
3077
  tokens.push(layers[key] ? key : `-${key}`);
2729
3078
  }
@@ -2731,171 +3080,838 @@
2731
3080
  return tokens.join(" ");
2732
3081
  }
2733
3082
 
2734
- // packages/core/dist/draft-parse.js
2735
- function parseWorldOrbitAtlas(source) {
2736
- return parseAtlasSource(source, "2.0");
3083
+ // packages/core/dist/atlas-utils.js
3084
+ 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)?$/;
3085
+ var BOOLEAN_VALUES2 = /* @__PURE__ */ new Map([
3086
+ ["true", true],
3087
+ ["false", false],
3088
+ ["yes", true],
3089
+ ["no", false]
3090
+ ]);
3091
+ var URL_SCHEME_PATTERN2 = /^[A-Za-z][A-Za-z0-9+.-]*:/;
3092
+ function normalizeIdentifier(value) {
3093
+ return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
2737
3094
  }
2738
- function parseAtlasSource(source, outputVersion) {
2739
- const lines = source.split(/\r?\n/);
2740
- let sawSchemaHeader = false;
2741
- let schemaVersion = "2.0";
2742
- let system = null;
2743
- let section = null;
2744
- const objectNodes = [];
2745
- let sawDefaults = false;
2746
- let sawAtlas = false;
2747
- const viewpointIds = /* @__PURE__ */ new Set();
2748
- const annotationIds = /* @__PURE__ */ new Set();
2749
- for (let index = 0; index < lines.length; index++) {
2750
- const rawLine = lines[index];
2751
- const lineNumber = index + 1;
2752
- if (!rawLine.trim()) {
2753
- continue;
2754
- }
2755
- const indent = getIndent(rawLine);
2756
- const tokens = tokenizeLineDetailed(rawLine.slice(indent), {
2757
- line: lineNumber,
2758
- columnOffset: indent
2759
- });
2760
- if (tokens.length === 0) {
2761
- continue;
2762
- }
2763
- if (!sawSchemaHeader) {
2764
- schemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
2765
- sawSchemaHeader = true;
2766
- continue;
2767
- }
2768
- if (indent === 0) {
2769
- section = startTopLevelSection(tokens, lineNumber, system, objectNodes, viewpointIds, annotationIds, {
2770
- sawDefaults,
2771
- sawAtlas
2772
- });
2773
- if (section.kind === "system") {
2774
- system = section.system;
2775
- } else if (section.kind === "defaults") {
2776
- sawDefaults = true;
2777
- } else if (section.kind === "atlas") {
2778
- sawAtlas = true;
2779
- }
2780
- continue;
2781
- }
2782
- if (!section) {
2783
- throw new WorldOrbitError("Indented line without parent atlas section", lineNumber, indent + 1);
2784
- }
2785
- handleSectionLine(section, indent, tokens, lineNumber);
2786
- }
2787
- if (!sawSchemaHeader) {
2788
- throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
3095
+ function humanizeIdentifier2(value) {
3096
+ return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
3097
+ }
3098
+ function parseAtlasUnitValue(input, location, fieldKey) {
3099
+ const match = input.match(UNIT_PATTERN2);
3100
+ if (!match) {
3101
+ throw WorldOrbitError.fromLocation(`Invalid unit value "${input}"`, location);
2789
3102
  }
2790
- const ast = {
2791
- type: "document",
2792
- objects: objectNodes
3103
+ const unitValue = {
3104
+ value: Number(match[1]),
3105
+ unit: match[2] ?? null
2793
3106
  };
2794
- const normalizedObjects = normalizeDocument(ast).objects;
2795
- validateDocument({
2796
- format: "worldorbit",
2797
- version: "1.0",
2798
- system: null,
2799
- objects: normalizedObjects
2800
- });
2801
- const diagnostics = schemaVersion === "2.0-draft" && outputVersion === "2.0" ? [
2802
- {
2803
- code: "load.schema.deprecatedDraft",
2804
- severity: "warning",
2805
- source: "upgrade",
2806
- message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".'
3107
+ if (fieldKey) {
3108
+ const schema = getFieldSchema(fieldKey);
3109
+ if (schema?.unitFamily && !unitFamilyAllowsUnit(schema.unitFamily, unitValue.unit)) {
3110
+ throw WorldOrbitError.fromLocation(`Unit "${unitValue.unit ?? "none"}" is not valid for "${fieldKey}"`, location);
2807
3111
  }
2808
- ] : [];
3112
+ }
3113
+ return unitValue;
3114
+ }
3115
+ function tryParseAtlasUnitValue(input) {
3116
+ const match = input.match(UNIT_PATTERN2);
3117
+ if (!match) {
3118
+ return null;
3119
+ }
2809
3120
  return {
2810
- format: "worldorbit",
2811
- version: outputVersion,
2812
- sourceVersion: "1.0",
2813
- system,
2814
- objects: normalizedObjects,
2815
- diagnostics
3121
+ value: Number(match[1]),
3122
+ unit: match[2] ?? null
2816
3123
  };
2817
3124
  }
2818
- function assertDraftSchemaHeader(tokens, line) {
2819
- if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || tokens[1].value.toLowerCase() !== "2.0-draft" && tokens[1].value.toLowerCase() !== "2.0") {
2820
- throw new WorldOrbitError('Expected atlas header "schema 2.0" or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
3125
+ function parseAtlasNumber(input, key, location) {
3126
+ const value = Number(input);
3127
+ if (!Number.isFinite(value)) {
3128
+ throw WorldOrbitError.fromLocation(`Invalid numeric value "${input}" for "${key}"`, location);
2821
3129
  }
2822
- return tokens[1].value.toLowerCase() === "2.0-draft" ? "2.0-draft" : "2.0";
3130
+ return value;
2823
3131
  }
2824
- function startTopLevelSection(tokens, line, system, objectNodes, viewpointIds, annotationIds, flags) {
2825
- const keyword = tokens[0]?.value.toLowerCase();
2826
- switch (keyword) {
2827
- case "system":
2828
- if (system) {
2829
- throw new WorldOrbitError('Atlas section "system" may only appear once', line, tokens[0].column);
2830
- }
2831
- return startSystemSection(tokens, line);
2832
- case "defaults":
2833
- if (!system) {
2834
- throw new WorldOrbitError('Atlas section "defaults" requires a preceding system declaration', line, tokens[0].column);
2835
- }
2836
- if (flags.sawDefaults) {
2837
- throw new WorldOrbitError('Atlas section "defaults" may only appear once', line, tokens[0].column);
2838
- }
2839
- return {
2840
- kind: "defaults",
2841
- system,
2842
- seenFields: /* @__PURE__ */ new Set()
2843
- };
2844
- case "atlas":
2845
- if (!system) {
2846
- throw new WorldOrbitError('Atlas section "atlas" requires a preceding system declaration', line, tokens[0].column);
2847
- }
2848
- if (flags.sawAtlas) {
2849
- throw new WorldOrbitError('Atlas section "atlas" may only appear once', line, tokens[0].column);
2850
- }
2851
- return {
2852
- kind: "atlas",
2853
- system,
2854
- inMetadata: false,
2855
- metadataIndent: null
2856
- };
2857
- case "viewpoint":
2858
- if (!system) {
2859
- throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
2860
- }
2861
- return startViewpointSection(tokens, line, system, viewpointIds);
2862
- case "annotation":
2863
- if (!system) {
2864
- throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
2865
- }
2866
- return startAnnotationSection(tokens, line, system, annotationIds);
2867
- case "object":
2868
- return startObjectSection(tokens, line, objectNodes);
2869
- default:
2870
- throw new WorldOrbitError(`Unknown atlas section "${tokens[0]?.value ?? ""}"`, line, tokens[0]?.column ?? 1);
3132
+ function parseAtlasBoolean(input, key, location) {
3133
+ const parsed = BOOLEAN_VALUES2.get(input.toLowerCase());
3134
+ if (parsed === void 0) {
3135
+ throw WorldOrbitError.fromLocation(`Invalid boolean value "${input}" for "${key}"`, location);
2871
3136
  }
3137
+ return parsed;
2872
3138
  }
2873
- function startSystemSection(tokens, line) {
2874
- if (tokens.length !== 2) {
2875
- throw new WorldOrbitError("Invalid atlas system declaration", line, tokens[0]?.column ?? 1);
3139
+ function parseAtlasAtReference(target, location) {
3140
+ if (/^[A-Za-z0-9._-]+-[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
3141
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
2876
3142
  }
2877
- const system = {
2878
- type: "system",
2879
- id: tokens[1].value,
2880
- title: null,
2881
- defaults: {
2882
- view: "topdown",
2883
- scale: null,
2884
- units: null,
2885
- preset: null,
2886
- theme: null
2887
- },
2888
- atlasMetadata: {},
2889
- viewpoints: [],
3143
+ const pairedMatch = target.match(/^([A-Za-z0-9._-]+)-([A-Za-z0-9._-]+):(L[1-5])$/);
3144
+ if (pairedMatch) {
3145
+ return {
3146
+ kind: "lagrange",
3147
+ primary: pairedMatch[1],
3148
+ secondary: pairedMatch[2],
3149
+ point: pairedMatch[3]
3150
+ };
3151
+ }
3152
+ const simpleMatch = target.match(/^([A-Za-z0-9._-]+):(L[1-5])$/);
3153
+ if (simpleMatch) {
3154
+ return {
3155
+ kind: "lagrange",
3156
+ primary: simpleMatch[1],
3157
+ secondary: null,
3158
+ point: simpleMatch[2]
3159
+ };
3160
+ }
3161
+ if (/^[A-Za-z0-9._-]+:L\d+$/i.test(target)) {
3162
+ throw WorldOrbitError.fromLocation(`Invalid special position "${target}"`, location);
3163
+ }
3164
+ const anchorMatch = target.match(/^([A-Za-z0-9._-]+):([A-Za-z0-9._-]+)$/);
3165
+ if (anchorMatch) {
3166
+ return {
3167
+ kind: "anchor",
3168
+ objectId: anchorMatch[1],
3169
+ anchor: anchorMatch[2]
3170
+ };
3171
+ }
3172
+ return {
3173
+ kind: "named",
3174
+ name: target
3175
+ };
3176
+ }
3177
+ function validateAtlasImageSource(value, location) {
3178
+ if (!value) {
3179
+ throw WorldOrbitError.fromLocation('Field "image" must not be empty', location);
3180
+ }
3181
+ if (value.startsWith("//")) {
3182
+ throw WorldOrbitError.fromLocation('Field "image" must use a relative path, root-relative path, or an http/https URL', location);
3183
+ }
3184
+ const schemeMatch = value.match(URL_SCHEME_PATTERN2);
3185
+ if (!schemeMatch) {
3186
+ return;
3187
+ }
3188
+ const scheme = schemeMatch[0].slice(0, -1).toLowerCase();
3189
+ if (scheme !== "http" && scheme !== "https") {
3190
+ throw WorldOrbitError.fromLocation(`Field "image" does not support the "${scheme}" scheme`, location);
3191
+ }
3192
+ }
3193
+ function normalizeLegacyScalarValue(key, values, location) {
3194
+ const schema = getFieldSchema(key);
3195
+ if (!schema) {
3196
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
3197
+ }
3198
+ if (schema.arity === "single" && values.length !== 1) {
3199
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
3200
+ }
3201
+ switch (schema.kind) {
3202
+ case "list":
3203
+ return values;
3204
+ case "boolean":
3205
+ return parseAtlasBoolean(singleAtlasValue(values, key, location), key, location);
3206
+ case "number":
3207
+ return parseAtlasNumber(singleAtlasValue(values, key, location), key, location);
3208
+ case "unit":
3209
+ return parseAtlasUnitValue(singleAtlasValue(values, key, location), location, key);
3210
+ case "string": {
3211
+ const value = values.join(" ").trim();
3212
+ if (key === "image") {
3213
+ validateAtlasImageSource(value, location);
3214
+ }
3215
+ return value;
3216
+ }
3217
+ }
3218
+ }
3219
+ function ensureAtlasFieldSupported(key, objectType, location) {
3220
+ const schema = getFieldSchema(key);
3221
+ if (!schema) {
3222
+ throw WorldOrbitError.fromLocation(`Unknown field "${key}"`, location);
3223
+ }
3224
+ if (!schema.objectTypes.includes(objectType)) {
3225
+ throw WorldOrbitError.fromLocation(`Field "${key}" is not valid on "${objectType}"`, location);
3226
+ }
3227
+ }
3228
+ function singleAtlasValue(values, key, location) {
3229
+ if (values.length !== 1) {
3230
+ throw WorldOrbitError.fromLocation(`Field "${key}" expects exactly one value`, location);
3231
+ }
3232
+ return values[0];
3233
+ }
3234
+
3235
+ // packages/core/dist/atlas-validate.js
3236
+ var SURFACE_TARGET_TYPES2 = /* @__PURE__ */ new Set(["star", "planet", "moon", "asteroid", "comet"]);
3237
+ var EARTH_MASSES_PER_SOLAR = 332946.0487;
3238
+ var JUPITER_MASSES_PER_SOLAR = 1047.3486;
3239
+ var AU_IN_KM2 = 1495978707e-1;
3240
+ var EARTH_RADIUS_IN_KM2 = 6371;
3241
+ var SOLAR_RADIUS_IN_KM2 = 695700;
3242
+ var LY_IN_AU2 = 63241.077;
3243
+ var PC_IN_AU2 = 206264.806;
3244
+ var KPC_IN_AU2 = 206264806;
3245
+ function collectAtlasDiagnostics(document2, sourceSchemaVersion) {
3246
+ const diagnostics = [];
3247
+ const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
3248
+ const groupIds = new Set(document2.groups.map((group) => group.id));
3249
+ const eventIds = new Set(document2.events.map((event) => event.id));
3250
+ if (!document2.system) {
3251
+ diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
3252
+ }
3253
+ const knownIds = /* @__PURE__ */ new Map();
3254
+ for (const [kind, ids] of [
3255
+ ["group", document2.groups.map((group) => group.id)],
3256
+ ["viewpoint", document2.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
3257
+ ["annotation", document2.system?.annotations.map((annotation) => annotation.id) ?? []],
3258
+ ["relation", document2.relations.map((relation) => relation.id)],
3259
+ ["event", document2.events.map((event) => event.id)],
3260
+ ["object", document2.objects.map((object) => object.id)]
3261
+ ]) {
3262
+ for (const id of ids) {
3263
+ const previous = knownIds.get(id);
3264
+ if (previous) {
3265
+ diagnostics.push(error("validate.id.duplicate", `Duplicate ${kind} id "${id}" already used by ${previous}.`));
3266
+ } else {
3267
+ knownIds.set(id, kind);
3268
+ }
3269
+ }
3270
+ }
3271
+ for (const relation of document2.relations) {
3272
+ validateRelation(relation, objectMap, diagnostics);
3273
+ }
3274
+ for (const viewpoint of document2.system?.viewpoints ?? []) {
3275
+ validateViewpoint(viewpoint.filter, viewpoint.events ?? [], groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpoint.id);
3276
+ }
3277
+ for (const object of document2.objects) {
3278
+ validateObject(object, document2.system, objectMap, groupIds, diagnostics);
3279
+ }
3280
+ for (const event of document2.events) {
3281
+ validateEvent(event, objectMap, diagnostics);
3282
+ }
3283
+ return diagnostics;
3284
+ }
3285
+ function validateRelation(relation, objectMap, diagnostics) {
3286
+ if (!relation.from) {
3287
+ diagnostics.push(error("validate.relation.from.required", `Relation "${relation.id}" is missing a "from" target.`));
3288
+ } else if (!objectMap.has(relation.from)) {
3289
+ diagnostics.push(error("validate.relation.from.unknown", `Unknown relation source "${relation.from}" on "${relation.id}".`));
3290
+ }
3291
+ if (!relation.to) {
3292
+ diagnostics.push(error("validate.relation.to.required", `Relation "${relation.id}" is missing a "to" target.`));
3293
+ } else if (!objectMap.has(relation.to)) {
3294
+ diagnostics.push(error("validate.relation.to.unknown", `Unknown relation target "${relation.to}" on "${relation.id}".`));
3295
+ }
3296
+ if (!relation.kind) {
3297
+ diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
3298
+ }
3299
+ }
3300
+ function validateViewpoint(filter, eventRefs, groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpointId) {
3301
+ if (sourceSchemaVersion === "2.1") {
3302
+ if (filter) {
3303
+ for (const groupId of filter.groupIds) {
3304
+ if (!groupIds.has(groupId)) {
3305
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.groups`));
3306
+ }
3307
+ }
3308
+ }
3309
+ for (const eventId of eventRefs) {
3310
+ if (!eventIds.has(eventId)) {
3311
+ diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.events`));
3312
+ }
3313
+ }
3314
+ }
3315
+ }
3316
+ function validateObject(object, system, objectMap, groupIds, diagnostics) {
3317
+ const placement = object.placement;
3318
+ const orbitPlacement = placement?.mode === "orbit" ? placement : null;
3319
+ const parentObject = placement?.mode === "orbit" ? objectMap.get(placement.target) ?? null : null;
3320
+ if (object.groups) {
3321
+ for (const groupId of object.groups) {
3322
+ if (!groupIds.has(groupId)) {
3323
+ diagnostics.push(warn("validate.group.unknown", `Unknown group "${groupId}" on "${object.id}".`, object.id, "groups"));
3324
+ }
3325
+ }
3326
+ }
3327
+ if (orbitPlacement) {
3328
+ if (!objectMap.has(orbitPlacement.target)) {
3329
+ diagnostics.push(error("validate.orbit.target.unknown", `Unknown placement target "${orbitPlacement.target}" on "${object.id}".`, object.id, "orbit"));
3330
+ }
3331
+ if (orbitPlacement.distance && orbitPlacement.semiMajor) {
3332
+ diagnostics.push(error("validate.orbit.distanceConflict", `Object "${object.id}" cannot declare both "distance" and "semiMajor".`, object.id, "distance"));
3333
+ }
3334
+ if (orbitPlacement.phase && !object.epoch && !system?.epoch) {
3335
+ diagnostics.push(warn("validate.phase.epochMissing", `Object "${object.id}" sets "phase" without an object or system epoch.`, object.id, "phase"));
3336
+ }
3337
+ if (orbitPlacement.inclination && !object.referencePlane && !system?.referencePlane) {
3338
+ diagnostics.push(warn("validate.inclination.referencePlaneMissing", `Object "${object.id}" sets "inclination" without an object or system reference plane.`, object.id, "inclination"));
3339
+ }
3340
+ if (orbitPlacement.period && !massInSolar(parentObject?.properties.mass)) {
3341
+ diagnostics.push(warn("validate.period.massMissing", `Object "${object.id}" sets "period" but its central mass cannot be derived.`, object.id, "period"));
3342
+ }
3343
+ }
3344
+ if (placement?.mode === "surface") {
3345
+ const target = objectMap.get(placement.target);
3346
+ if (!target) {
3347
+ diagnostics.push(error("validate.surface.target.unknown", `Unknown placement target "${placement.target}" on "${object.id}".`, object.id, "surface"));
3348
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
3349
+ diagnostics.push(error("validate.surface.target.invalid", `Surface target "${placement.target}" on "${object.id}" is not surface-capable.`, object.id, "surface"));
3350
+ }
3351
+ }
3352
+ if (placement?.mode === "at") {
3353
+ if (object.type !== "structure" && object.type !== "phenomenon") {
3354
+ diagnostics.push(error("validate.at.objectType", `Only structures and phenomena may use "at" placement; found "${object.type}" on "${object.id}".`, object.id, "at"));
3355
+ }
3356
+ if (!validateAtTarget(object, objectMap, diagnostics)) {
3357
+ diagnostics.push(error("validate.at.target.unknown", `Unknown at-reference target "${placement.target}" on "${object.id}".`, object.id, "at"));
3358
+ }
3359
+ }
3360
+ if (object.resonance) {
3361
+ const target = objectMap.get(object.resonance.targetObjectId);
3362
+ if (!target) {
3363
+ diagnostics.push(error("validate.resonance.target.unknown", `Unknown resonance target "${object.resonance.targetObjectId}" on "${object.id}".`, object.id, "resonance"));
3364
+ } else if (object.placement?.mode !== "orbit" || target.placement?.mode !== "orbit" || object.placement.target !== target.placement.target) {
3365
+ diagnostics.push(warn("validate.resonance.orbitMismatch", `Resonance target "${object.resonance.targetObjectId}" on "${object.id}" does not share a compatible orbital parent.`, object.id, "resonance"));
3366
+ }
3367
+ }
3368
+ for (const rule of object.deriveRules ?? []) {
3369
+ if (rule.field !== "period" || rule.strategy !== "kepler") {
3370
+ diagnostics.push(warn("validate.derive.unsupported", `Unsupported derive rule "${rule.field} ${rule.strategy}" on "${object.id}".`, object.id, "derive"));
3371
+ continue;
3372
+ }
3373
+ const derivedPeriodDays = keplerPeriodDays(object, parentObject);
3374
+ if (derivedPeriodDays === null) {
3375
+ diagnostics.push(warn("validate.derive.inputsMissing", `Object "${object.id}" requests "derive period kepler" but lacks enough input data.`, object.id, "derive"));
3376
+ continue;
3377
+ }
3378
+ if (!orbitPlacement?.period) {
3379
+ diagnostics.push(info("validate.derive.period.available", `Object "${object.id}" can derive a Kepler period of ${formatDays(derivedPeriodDays)}.`, object.id, "derive"));
3380
+ }
3381
+ }
3382
+ for (const rule of object.validationRules ?? []) {
3383
+ if (rule.rule !== "kepler") {
3384
+ diagnostics.push(warn("validate.rule.unsupported", `Unsupported validation rule "${rule.rule}" on "${object.id}".`, object.id, "validate"));
3385
+ continue;
3386
+ }
3387
+ const actualPeriodDays = durationInDays(orbitPlacement?.period);
3388
+ const derivedPeriodDays = keplerPeriodDays(object, parentObject);
3389
+ if (actualPeriodDays === null || derivedPeriodDays === null) {
3390
+ continue;
3391
+ }
3392
+ const toleranceDays = toleranceForField(object, "period");
3393
+ if (Math.abs(actualPeriodDays - derivedPeriodDays) > toleranceDays) {
3394
+ diagnostics.push(error("validate.kepler.mismatch", `Object "${object.id}" fails Kepler validation for "period".`, object.id, "validate"));
3395
+ }
3396
+ }
3397
+ }
3398
+ function validateEvent(event, objectMap, diagnostics) {
3399
+ const fieldPrefix = `event.${event.id}`;
3400
+ const referencedIds = /* @__PURE__ */ new Set();
3401
+ if (!event.kind.trim()) {
3402
+ diagnostics.push(error("validate.event.kind.required", `Event "${event.id}" is missing a "kind" value.`, void 0, `${fieldPrefix}.kind`));
3403
+ }
3404
+ if (!event.targetObjectId && event.participantObjectIds.length === 0) {
3405
+ diagnostics.push(error("validate.event.references.required", `Event "${event.id}" must define a "target" or at least one participant.`, void 0, `${fieldPrefix}.participants`));
3406
+ }
3407
+ if (event.targetObjectId) {
3408
+ referencedIds.add(event.targetObjectId);
3409
+ if (!objectMap.has(event.targetObjectId)) {
3410
+ diagnostics.push(error("validate.event.target.unknown", `Unknown event target "${event.targetObjectId}" on "${event.id}".`, void 0, `${fieldPrefix}.target`));
3411
+ }
3412
+ }
3413
+ const seenParticipants = /* @__PURE__ */ new Set();
3414
+ for (const participantId of event.participantObjectIds) {
3415
+ referencedIds.add(participantId);
3416
+ if (seenParticipants.has(participantId)) {
3417
+ diagnostics.push(warn("validate.event.participants.duplicate", `Event "${event.id}" repeats participant "${participantId}".`, void 0, `${fieldPrefix}.participants`));
3418
+ continue;
3419
+ }
3420
+ seenParticipants.add(participantId);
3421
+ if (!objectMap.has(participantId)) {
3422
+ diagnostics.push(error("validate.event.participants.unknown", `Unknown event participant "${participantId}" on "${event.id}".`, void 0, `${fieldPrefix}.participants`));
3423
+ }
3424
+ }
3425
+ if (event.targetObjectId && event.participantObjectIds.length > 0 && !event.participantObjectIds.includes(event.targetObjectId)) {
3426
+ diagnostics.push(warn("validate.event.target.notParticipant", `Event "${event.id}" defines a target outside its participants list.`, void 0, `${fieldPrefix}.target`));
3427
+ }
3428
+ if (event.positions.length === 0) {
3429
+ diagnostics.push(warn("validate.event.positions.missing", `Event "${event.id}" has no positions block and cannot drive a scene snapshot.`, void 0, `${fieldPrefix}.positions`));
3430
+ }
3431
+ if (/(?:^|[-_])(solar-eclipse|lunar-eclipse|transit|occultation)(?:$|[-_])/.test(event.kind) && referencedIds.size < 3) {
3432
+ 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`));
3433
+ }
3434
+ const poseIds = /* @__PURE__ */ new Set();
3435
+ for (const pose of event.positions) {
3436
+ const poseFieldPrefix = `${fieldPrefix}.pose.${pose.objectId}`;
3437
+ if (poseIds.has(pose.objectId)) {
3438
+ diagnostics.push(error("validate.event.pose.duplicate", `Event "${event.id}" defines "${pose.objectId}" more than once in positions.`, void 0, poseFieldPrefix));
3439
+ continue;
3440
+ }
3441
+ poseIds.add(pose.objectId);
3442
+ const object = objectMap.get(pose.objectId);
3443
+ if (!object) {
3444
+ diagnostics.push(error("validate.event.pose.object.unknown", `Unknown event pose object "${pose.objectId}" on "${event.id}".`, void 0, poseFieldPrefix));
3445
+ continue;
3446
+ }
3447
+ if (!referencedIds.has(pose.objectId)) {
3448
+ diagnostics.push(warn("validate.event.pose.unreferenced", `Event pose "${pose.objectId}" on "${event.id}" is not listed in target/participants.`, void 0, poseFieldPrefix));
3449
+ }
3450
+ validateEventPose(pose, object, objectMap, diagnostics, poseFieldPrefix, event.id);
3451
+ }
3452
+ }
3453
+ function validateEventPose(pose, object, objectMap, diagnostics, fieldPrefix, eventId) {
3454
+ const placement = pose.placement;
3455
+ if (!placement) {
3456
+ diagnostics.push(error("validate.event.pose.placement.required", `Event "${eventId}" pose "${pose.objectId}" is missing a placement mode.`, void 0, fieldPrefix));
3457
+ return;
3458
+ }
3459
+ if (placement.mode === "orbit") {
3460
+ if (!objectMap.has(placement.target)) {
3461
+ diagnostics.push(error("validate.event.pose.orbit.target.unknown", `Unknown event orbit target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.orbit`));
3462
+ }
3463
+ if (placement.distance && placement.semiMajor) {
3464
+ diagnostics.push(error("validate.event.pose.orbit.distanceConflict", `Event "${eventId}" pose "${pose.objectId}" cannot declare both "distance" and "semiMajor".`, void 0, `${fieldPrefix}.distance`));
3465
+ }
3466
+ return;
3467
+ }
3468
+ if (placement.mode === "surface") {
3469
+ const target = objectMap.get(placement.target);
3470
+ if (!target) {
3471
+ diagnostics.push(error("validate.event.pose.surface.target.unknown", `Unknown event surface target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.surface`));
3472
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
3473
+ 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`));
3474
+ }
3475
+ return;
3476
+ }
3477
+ if (placement.mode === "at") {
3478
+ if (object.type !== "structure" && object.type !== "phenomenon") {
3479
+ 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`));
3480
+ }
3481
+ const reference = placement.reference;
3482
+ if (reference.kind === "named" && !objectMap.has(reference.name)) {
3483
+ diagnostics.push(error("validate.event.pose.at.target.unknown", `Unknown event at-reference target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3484
+ } else if (reference.kind === "anchor" && !objectMap.has(reference.objectId)) {
3485
+ diagnostics.push(error("validate.event.pose.anchor.target.unknown", `Unknown event anchor target "${reference.objectId}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3486
+ } else if (reference.kind === "lagrange") {
3487
+ if (!objectMap.has(reference.primary)) {
3488
+ diagnostics.push(error("validate.event.pose.lagrange.primary.unknown", `Unknown event Lagrange target "${reference.primary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3489
+ } else if (reference.secondary && !objectMap.has(reference.secondary)) {
3490
+ diagnostics.push(error("validate.event.pose.lagrange.secondary.unknown", `Unknown event Lagrange target "${reference.secondary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
3491
+ }
3492
+ }
3493
+ }
3494
+ }
3495
+ function validateAtTarget(object, objectMap, diagnostics) {
3496
+ const reference = object.placement?.mode === "at" ? object.placement.reference : null;
3497
+ if (!reference) {
3498
+ return true;
3499
+ }
3500
+ if (reference.kind === "named") {
3501
+ return objectMap.has(reference.name);
3502
+ }
3503
+ if (reference.kind === "anchor") {
3504
+ if (!objectMap.has(reference.objectId)) {
3505
+ diagnostics.push(error("validate.anchor.target.unknown", `Unknown anchor target "${reference.objectId}" on "${object.id}".`, object.id, "at"));
3506
+ return false;
3507
+ }
3508
+ return true;
3509
+ }
3510
+ if (!objectMap.has(reference.primary)) {
3511
+ diagnostics.push(error("validate.lagrange.primary.unknown", `Unknown Lagrange reference "${reference.primary}" on "${object.id}".`, object.id, "at"));
3512
+ return false;
3513
+ }
3514
+ if (reference.secondary && !objectMap.has(reference.secondary)) {
3515
+ diagnostics.push(error("validate.lagrange.secondary.unknown", `Unknown Lagrange reference "${reference.secondary}" on "${object.id}".`, object.id, "at"));
3516
+ return false;
3517
+ }
3518
+ return true;
3519
+ }
3520
+ function keplerPeriodDays(object, parentObject) {
3521
+ const placement = object.placement;
3522
+ if (!placement || placement.mode !== "orbit") {
3523
+ return null;
3524
+ }
3525
+ const semiMajorAu = distanceInAu(placement.semiMajor ?? placement.distance);
3526
+ const centralMassSolar = massInSolar(parentObject?.properties.mass);
3527
+ if (semiMajorAu === null || centralMassSolar === null || centralMassSolar <= 0) {
3528
+ return null;
3529
+ }
3530
+ const periodYears = Math.sqrt(semiMajorAu ** 3 / centralMassSolar);
3531
+ return periodYears * 365.25;
3532
+ }
3533
+ function distanceInAu(value) {
3534
+ if (!value)
3535
+ return null;
3536
+ switch (value.unit) {
3537
+ case null:
3538
+ case "au":
3539
+ return value.value;
3540
+ case "km":
3541
+ return value.value / AU_IN_KM2;
3542
+ case "m":
3543
+ return value.value / (AU_IN_KM2 * 1e3);
3544
+ case "ly":
3545
+ return value.value * LY_IN_AU2;
3546
+ case "pc":
3547
+ return value.value * PC_IN_AU2;
3548
+ case "kpc":
3549
+ return value.value * KPC_IN_AU2;
3550
+ case "re":
3551
+ return value.value * EARTH_RADIUS_IN_KM2 / AU_IN_KM2;
3552
+ case "sol":
3553
+ return value.value * SOLAR_RADIUS_IN_KM2 / AU_IN_KM2;
3554
+ default:
3555
+ return null;
3556
+ }
3557
+ }
3558
+ function massInSolar(value) {
3559
+ if (!value || typeof value !== "object" || !("value" in value)) {
3560
+ return null;
3561
+ }
3562
+ const unitValue = value;
3563
+ switch (unitValue.unit) {
3564
+ case null:
3565
+ case "sol":
3566
+ return unitValue.value;
3567
+ case "me":
3568
+ return unitValue.value / EARTH_MASSES_PER_SOLAR;
3569
+ case "mj":
3570
+ return unitValue.value / JUPITER_MASSES_PER_SOLAR;
3571
+ default:
3572
+ return null;
3573
+ }
3574
+ }
3575
+ function durationInDays(value) {
3576
+ if (!value)
3577
+ return null;
3578
+ switch (value.unit) {
3579
+ case null:
3580
+ case "d":
3581
+ return value.value;
3582
+ case "s":
3583
+ return value.value / 86400;
3584
+ case "min":
3585
+ return value.value / 1440;
3586
+ case "h":
3587
+ return value.value / 24;
3588
+ case "y":
3589
+ return value.value * 365.25;
3590
+ case "ky":
3591
+ return value.value * 365250;
3592
+ case "my":
3593
+ return value.value * 36525e4;
3594
+ case "gy":
3595
+ return value.value * 36525e7;
3596
+ default:
3597
+ return null;
3598
+ }
3599
+ }
3600
+ function toleranceForField(object, field) {
3601
+ const tolerance = object.tolerances?.find((entry) => entry.field === field)?.value;
3602
+ if (typeof tolerance === "number") {
3603
+ return tolerance;
3604
+ }
3605
+ if (tolerance && typeof tolerance === "object" && "value" in tolerance) {
3606
+ return durationInDays(tolerance) ?? 0;
3607
+ }
3608
+ return 0;
3609
+ }
3610
+ function formatDays(days) {
3611
+ return `${Math.round(days * 100) / 100}d`;
3612
+ }
3613
+ function error(code, message, objectId, field) {
3614
+ return { code, severity: "error", source: "validate", message, objectId, field };
3615
+ }
3616
+ function warn(code, message, objectId, field) {
3617
+ return { code, severity: "warning", source: "validate", message, objectId, field };
3618
+ }
3619
+ function info(code, message, objectId, field) {
3620
+ return { code, severity: "info", source: "validate", message, objectId, field };
3621
+ }
3622
+
3623
+ // packages/core/dist/draft-parse.js
3624
+ var STRUCTURED_TYPED_BLOCKS = /* @__PURE__ */ new Set([
3625
+ "climate",
3626
+ "habitability",
3627
+ "settlement"
3628
+ ]);
3629
+ var DRAFT_OBJECT_FIELD_SPECS = /* @__PURE__ */ new Map();
3630
+ for (const key of [
3631
+ "orbit",
3632
+ "distance",
3633
+ "semiMajor",
3634
+ "eccentricity",
3635
+ "period",
3636
+ "angle",
3637
+ "inclination",
3638
+ "phase",
3639
+ "at",
3640
+ "surface",
3641
+ "free",
3642
+ "kind",
3643
+ "class",
3644
+ "culture",
3645
+ "tags",
3646
+ "color",
3647
+ "image",
3648
+ "hidden",
3649
+ "radius",
3650
+ "mass",
3651
+ "density",
3652
+ "gravity",
3653
+ "temperature",
3654
+ "albedo",
3655
+ "atmosphere",
3656
+ "inner",
3657
+ "outer",
3658
+ "on",
3659
+ "source",
3660
+ "cycle"
3661
+ ]) {
3662
+ const schema = getFieldSchema(key);
3663
+ if (schema) {
3664
+ DRAFT_OBJECT_FIELD_SPECS.set(key, {
3665
+ key,
3666
+ version: "2.0",
3667
+ inlineMode: schema.arity === "multiple" ? "multiple" : "single",
3668
+ allowRepeat: false,
3669
+ legacySchema: schema
3670
+ });
3671
+ }
3672
+ }
3673
+ for (const spec of [
3674
+ { key: "groups", inlineMode: "multiple", allowRepeat: false },
3675
+ { key: "epoch", inlineMode: "single", allowRepeat: false },
3676
+ { key: "referencePlane", inlineMode: "single", allowRepeat: false },
3677
+ { key: "tidalLock", inlineMode: "single", allowRepeat: false },
3678
+ { key: "renderLabel", inlineMode: "single", allowRepeat: false },
3679
+ { key: "renderOrbit", inlineMode: "single", allowRepeat: false },
3680
+ { key: "renderPriority", inlineMode: "single", allowRepeat: false },
3681
+ { key: "resonance", inlineMode: "pair", allowRepeat: false },
3682
+ { key: "derive", inlineMode: "pair", allowRepeat: true },
3683
+ { key: "validate", inlineMode: "single", allowRepeat: true },
3684
+ { key: "locked", inlineMode: "multiple", allowRepeat: false },
3685
+ { key: "tolerance", inlineMode: "pair", allowRepeat: true }
3686
+ ]) {
3687
+ DRAFT_OBJECT_FIELD_SPECS.set(spec.key, {
3688
+ key: spec.key,
3689
+ version: "2.1",
3690
+ inlineMode: spec.inlineMode,
3691
+ allowRepeat: spec.allowRepeat
3692
+ });
3693
+ }
3694
+ var DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
3695
+ var EVENT_POSE_FIELD_KEYS = /* @__PURE__ */ new Set([
3696
+ "orbit",
3697
+ "distance",
3698
+ "semiMajor",
3699
+ "eccentricity",
3700
+ "period",
3701
+ "angle",
3702
+ "inclination",
3703
+ "phase",
3704
+ "at",
3705
+ "surface",
3706
+ "free",
3707
+ "inner",
3708
+ "outer"
3709
+ ]);
3710
+ function parseWorldOrbitAtlas(source) {
3711
+ return parseAtlasSource(source);
3712
+ }
3713
+ function parseAtlasSource(source, forcedOutputVersion) {
3714
+ const prepared = preprocessAtlasSource(source);
3715
+ const lines = prepared.source.split(/\r?\n/);
3716
+ const diagnostics = [];
3717
+ let sawSchemaHeader = false;
3718
+ let sourceSchemaVersion = "2.0";
3719
+ let system = null;
3720
+ let section = null;
3721
+ const objectNodes = [];
3722
+ const groups = [];
3723
+ const relations = [];
3724
+ const events = [];
3725
+ const eventPoseNodes = /* @__PURE__ */ new Map();
3726
+ let sawDefaults = false;
3727
+ let sawAtlas = false;
3728
+ const viewpointIds = /* @__PURE__ */ new Set();
3729
+ const annotationIds = /* @__PURE__ */ new Set();
3730
+ const groupIds = /* @__PURE__ */ new Set();
3731
+ const relationIds = /* @__PURE__ */ new Set();
3732
+ const eventIds = /* @__PURE__ */ new Set();
3733
+ for (let index = 0; index < lines.length; index++) {
3734
+ const rawLine = lines[index];
3735
+ const lineNumber = index + 1;
3736
+ if (!rawLine.trim()) {
3737
+ continue;
3738
+ }
3739
+ const indent = getIndent(rawLine);
3740
+ const tokens = tokenizeLineDetailed(rawLine.slice(indent), {
3741
+ line: lineNumber,
3742
+ columnOffset: indent
3743
+ });
3744
+ if (tokens.length === 0) {
3745
+ continue;
3746
+ }
3747
+ if (!sawSchemaHeader) {
3748
+ sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
3749
+ sawSchemaHeader = true;
3750
+ if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
3751
+ diagnostics.push({
3752
+ code: "parse.schema21.commentCompatibility",
3753
+ severity: "warning",
3754
+ source: "parse",
3755
+ message: `Comments require schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
3756
+ line: prepared.comments[0].line,
3757
+ column: prepared.comments[0].column
3758
+ });
3759
+ }
3760
+ continue;
3761
+ }
3762
+ if (indent === 0) {
3763
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, { sawDefaults, sawAtlas });
3764
+ if (section.kind === "system") {
3765
+ system = section.system;
3766
+ } else if (section.kind === "defaults") {
3767
+ sawDefaults = true;
3768
+ } else if (section.kind === "atlas") {
3769
+ sawAtlas = true;
3770
+ }
3771
+ continue;
3772
+ }
3773
+ if (!section) {
3774
+ throw new WorldOrbitError("Indented line without parent atlas section", lineNumber, indent + 1);
3775
+ }
3776
+ handleSectionLine(section, indent, tokens, lineNumber);
3777
+ }
3778
+ if (!sawSchemaHeader) {
3779
+ throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
3780
+ }
3781
+ const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
3782
+ const normalizedEvents = events.map((event) => normalizeDraftEvent(event, eventPoseNodes.get(event.id) ?? []));
3783
+ const outputVersion = forcedOutputVersion ?? (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
3784
+ const baseDocument = {
3785
+ format: "worldorbit",
3786
+ sourceVersion: "1.0",
3787
+ system,
3788
+ groups,
3789
+ relations,
3790
+ events: normalizedEvents,
3791
+ objects,
3792
+ diagnostics
3793
+ };
3794
+ if (outputVersion === "2.0-draft") {
3795
+ const document3 = {
3796
+ ...baseDocument,
3797
+ version: "2.0-draft",
3798
+ schemaVersion: "2.0-draft"
3799
+ };
3800
+ document3.diagnostics.push(...collectAtlasDiagnostics(document3, sourceSchemaVersion));
3801
+ return document3;
3802
+ }
3803
+ const document2 = {
3804
+ ...baseDocument,
3805
+ version: outputVersion,
3806
+ schemaVersion: outputVersion
3807
+ };
3808
+ if (sourceSchemaVersion === "2.0-draft") {
3809
+ document2.diagnostics.push({
3810
+ code: "load.schema.deprecatedDraft",
3811
+ severity: "warning",
3812
+ source: "upgrade",
3813
+ message: 'Source header "schema 2.0-draft" is deprecated; canonical v2 documents now use "schema 2.0".'
3814
+ });
3815
+ }
3816
+ document2.diagnostics.push(...collectAtlasDiagnostics(document2, sourceSchemaVersion));
3817
+ return document2;
3818
+ }
3819
+ function assertDraftSchemaHeader(tokens, line) {
3820
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
3821
+ throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
3822
+ }
3823
+ const version = tokens[1].value.toLowerCase();
3824
+ return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
3825
+ }
3826
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, flags) {
3827
+ const keyword = tokens[0]?.value.toLowerCase();
3828
+ switch (keyword) {
3829
+ case "system":
3830
+ if (system) {
3831
+ throw new WorldOrbitError('Atlas section "system" may only appear once', line, tokens[0].column);
3832
+ }
3833
+ return startSystemSection(tokens, line, sourceSchemaVersion, diagnostics);
3834
+ case "defaults":
3835
+ if (!system) {
3836
+ throw new WorldOrbitError('Atlas section "defaults" requires a preceding system declaration', line, tokens[0].column);
3837
+ }
3838
+ if (flags.sawDefaults) {
3839
+ throw new WorldOrbitError('Atlas section "defaults" may only appear once', line, tokens[0].column);
3840
+ }
3841
+ return {
3842
+ kind: "defaults",
3843
+ system,
3844
+ seenFields: /* @__PURE__ */ new Set()
3845
+ };
3846
+ case "atlas":
3847
+ if (!system) {
3848
+ throw new WorldOrbitError('Atlas section "atlas" requires a preceding system declaration', line, tokens[0].column);
3849
+ }
3850
+ if (flags.sawAtlas) {
3851
+ throw new WorldOrbitError('Atlas section "atlas" may only appear once', line, tokens[0].column);
3852
+ }
3853
+ return {
3854
+ kind: "atlas",
3855
+ system,
3856
+ inMetadata: false,
3857
+ metadataIndent: null
3858
+ };
3859
+ case "viewpoint":
3860
+ if (!system) {
3861
+ throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
3862
+ }
3863
+ return startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics);
3864
+ case "annotation":
3865
+ if (!system) {
3866
+ throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
3867
+ }
3868
+ return startAnnotationSection(tokens, line, system, annotationIds);
3869
+ case "group":
3870
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "group", { line, column: tokens[0].column });
3871
+ return startGroupSection(tokens, line, groups, groupIds);
3872
+ case "relation":
3873
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
3874
+ return startRelationSection(tokens, line, relations, relationIds);
3875
+ case "event":
3876
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "event", { line, column: tokens[0].column });
3877
+ return startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics);
3878
+ case "object":
3879
+ return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
3880
+ default:
3881
+ throw new WorldOrbitError(`Unknown atlas section "${tokens[0]?.value ?? ""}"`, line, tokens[0]?.column ?? 1);
3882
+ }
3883
+ }
3884
+ function startSystemSection(tokens, line, sourceSchemaVersion, diagnostics) {
3885
+ if (tokens.length !== 2) {
3886
+ throw new WorldOrbitError("Invalid atlas system declaration", line, tokens[0]?.column ?? 1);
3887
+ }
3888
+ const system = {
3889
+ type: "system",
3890
+ id: tokens[1].value,
3891
+ title: null,
3892
+ description: null,
3893
+ epoch: null,
3894
+ referencePlane: null,
3895
+ defaults: {
3896
+ view: "topdown",
3897
+ scale: null,
3898
+ units: null,
3899
+ preset: null,
3900
+ theme: null
3901
+ },
3902
+ atlasMetadata: {},
3903
+ viewpoints: [],
2890
3904
  annotations: []
2891
3905
  };
2892
3906
  return {
2893
3907
  kind: "system",
2894
3908
  system,
3909
+ sourceSchemaVersion,
3910
+ diagnostics,
2895
3911
  seenFields: /* @__PURE__ */ new Set()
2896
3912
  };
2897
3913
  }
2898
- function startViewpointSection(tokens, line, system, viewpointIds) {
3914
+ function startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics) {
2899
3915
  if (tokens.length !== 2) {
2900
3916
  throw new WorldOrbitError("Invalid viewpoint declaration", line, tokens[0]?.column ?? 1);
2901
3917
  }
@@ -2912,6 +3928,7 @@
2912
3928
  summary: "",
2913
3929
  focusObjectId: null,
2914
3930
  selectedObjectId: null,
3931
+ events: [],
2915
3932
  projection: system.defaults.view,
2916
3933
  preset: system.defaults.preset,
2917
3934
  zoom: null,
@@ -2924,6 +3941,8 @@
2924
3941
  return {
2925
3942
  kind: "viewpoint",
2926
3943
  viewpoint,
3944
+ sourceSchemaVersion,
3945
+ diagnostics,
2927
3946
  seenFields: /* @__PURE__ */ new Set(),
2928
3947
  inFilter: false,
2929
3948
  filterIndent: null,
@@ -2957,7 +3976,107 @@
2957
3976
  seenFields: /* @__PURE__ */ new Set()
2958
3977
  };
2959
3978
  }
2960
- function startObjectSection(tokens, line, objectNodes) {
3979
+ function startGroupSection(tokens, line, groups, groupIds) {
3980
+ if (tokens.length !== 2) {
3981
+ throw new WorldOrbitError("Invalid group declaration", line, tokens[0]?.column ?? 1);
3982
+ }
3983
+ const id = normalizeIdentifier(tokens[1].value);
3984
+ if (!id) {
3985
+ throw new WorldOrbitError("Group id must not be empty", line, tokens[1].column);
3986
+ }
3987
+ if (groupIds.has(id)) {
3988
+ throw new WorldOrbitError(`Duplicate group id "${id}"`, line, tokens[1].column);
3989
+ }
3990
+ const group = {
3991
+ id,
3992
+ label: humanizeIdentifier2(id),
3993
+ summary: "",
3994
+ color: null,
3995
+ tags: [],
3996
+ hidden: false
3997
+ };
3998
+ groups.push(group);
3999
+ groupIds.add(id);
4000
+ return {
4001
+ kind: "group",
4002
+ group,
4003
+ seenFields: /* @__PURE__ */ new Set()
4004
+ };
4005
+ }
4006
+ function startRelationSection(tokens, line, relations, relationIds) {
4007
+ if (tokens.length !== 2) {
4008
+ throw new WorldOrbitError("Invalid relation declaration", line, tokens[0]?.column ?? 1);
4009
+ }
4010
+ const id = normalizeIdentifier(tokens[1].value);
4011
+ if (!id) {
4012
+ throw new WorldOrbitError("Relation id must not be empty", line, tokens[1].column);
4013
+ }
4014
+ if (relationIds.has(id)) {
4015
+ throw new WorldOrbitError(`Duplicate relation id "${id}"`, line, tokens[1].column);
4016
+ }
4017
+ const relation = {
4018
+ id,
4019
+ from: "",
4020
+ to: "",
4021
+ kind: "",
4022
+ label: null,
4023
+ summary: null,
4024
+ tags: [],
4025
+ color: null,
4026
+ hidden: false
4027
+ };
4028
+ relations.push(relation);
4029
+ relationIds.add(id);
4030
+ return {
4031
+ kind: "relation",
4032
+ relation,
4033
+ seenFields: /* @__PURE__ */ new Set()
4034
+ };
4035
+ }
4036
+ function startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics) {
4037
+ if (tokens.length !== 2) {
4038
+ throw new WorldOrbitError("Invalid event declaration", line, tokens[0]?.column ?? 1);
4039
+ }
4040
+ const id = normalizeIdentifier(tokens[1].value);
4041
+ if (!id) {
4042
+ throw new WorldOrbitError("Event id must not be empty", line, tokens[1].column);
4043
+ }
4044
+ if (eventIds.has(id)) {
4045
+ throw new WorldOrbitError(`Duplicate event id "${id}"`, line, tokens[1].column);
4046
+ }
4047
+ const event = {
4048
+ id,
4049
+ kind: "",
4050
+ label: humanizeIdentifier2(id),
4051
+ summary: null,
4052
+ targetObjectId: null,
4053
+ participantObjectIds: [],
4054
+ timing: null,
4055
+ visibility: null,
4056
+ tags: [],
4057
+ color: null,
4058
+ hidden: false,
4059
+ positions: []
4060
+ };
4061
+ const rawPoses = [];
4062
+ events.push(event);
4063
+ eventPoseNodes.set(id, rawPoses);
4064
+ eventIds.add(id);
4065
+ return {
4066
+ kind: "event",
4067
+ event,
4068
+ sourceSchemaVersion,
4069
+ diagnostics,
4070
+ seenFields: /* @__PURE__ */ new Set(),
4071
+ rawPoses,
4072
+ inPositions: false,
4073
+ positionsIndent: null,
4074
+ activePose: null,
4075
+ poseIndent: null,
4076
+ activePoseSeenFields: /* @__PURE__ */ new Set()
4077
+ };
4078
+ }
4079
+ function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
2961
4080
  if (tokens.length < 3) {
2962
4081
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
2963
4082
  }
@@ -2968,12 +4087,11 @@
2968
4087
  throw new WorldOrbitError(`Unknown object type "${objectTypeToken.value}"`, line, objectTypeToken.column);
2969
4088
  }
2970
4089
  const objectNode = {
2971
- type: "object",
2972
4090
  objectType,
2973
- name: idToken.value,
2974
- inlineFields: parseInlineFields2(tokens.slice(3), line),
2975
- blockFields: [],
4091
+ id: idToken.value,
4092
+ fields: parseInlineObjectFields(tokens.slice(3), line, objectType, sourceSchemaVersion, diagnostics),
2976
4093
  infoEntries: [],
4094
+ typedBlockEntries: {},
2977
4095
  location: {
2978
4096
  line,
2979
4097
  column: objectTypeToken.column
@@ -2983,8 +4101,12 @@
2983
4101
  return {
2984
4102
  kind: "object",
2985
4103
  objectNode,
2986
- inInfoBlock: false,
2987
- infoIndent: null
4104
+ sourceSchemaVersion,
4105
+ diagnostics,
4106
+ activeBlock: null,
4107
+ blockIndent: null,
4108
+ seenInfoKeys: /* @__PURE__ */ new Set(),
4109
+ seenTypedBlockKeys: {}
2988
4110
  };
2989
4111
  }
2990
4112
  function handleSectionLine(section, indent, tokens, line) {
@@ -3004,6 +4126,15 @@
3004
4126
  case "annotation":
3005
4127
  applyAnnotationField(section, tokens, line);
3006
4128
  return;
4129
+ case "group":
4130
+ applyGroupField(section, tokens, line);
4131
+ return;
4132
+ case "relation":
4133
+ applyRelationField(section, tokens, line);
4134
+ return;
4135
+ case "event":
4136
+ applyEventField(section, indent, tokens, line);
4137
+ return;
3007
4138
  case "object":
3008
4139
  applyObjectField(section, indent, tokens, line);
3009
4140
  return;
@@ -3011,10 +4142,35 @@
3011
4142
  }
3012
4143
  function applySystemField(section, tokens, line) {
3013
4144
  const key = requireUniqueField(tokens, section.seenFields, line);
3014
- if (key !== "title") {
3015
- throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
4145
+ const value = joinFieldValue(tokens, line);
4146
+ switch (key) {
4147
+ case "title":
4148
+ section.system.title = value;
4149
+ return;
4150
+ case "description":
4151
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
4152
+ line,
4153
+ column: tokens[0].column
4154
+ });
4155
+ section.system.description = value;
4156
+ return;
4157
+ case "epoch":
4158
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, key, {
4159
+ line,
4160
+ column: tokens[0].column
4161
+ });
4162
+ section.system.epoch = value;
4163
+ return;
4164
+ case "referenceplane":
4165
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "referencePlane", {
4166
+ line,
4167
+ column: tokens[0].column
4168
+ });
4169
+ section.system.referencePlane = value;
4170
+ return;
4171
+ default:
4172
+ throw new WorldOrbitError(`Unknown system atlas field "${tokens[0].value}"`, line, tokens[0].column);
3016
4173
  }
3017
- section.system.title = joinFieldValue(tokens, line);
3018
4174
  }
3019
4175
  function applyDefaultsField(section, tokens, line) {
3020
4176
  const key = requireUniqueField(tokens, section.seenFields, line);
@@ -3045,14 +4201,11 @@
3045
4201
  section.metadataIndent = null;
3046
4202
  }
3047
4203
  if (section.inMetadata) {
3048
- if (tokens.length < 2) {
3049
- throw new WorldOrbitError("Invalid atlas metadata entry", line, tokens[0]?.column ?? 1);
3050
- }
3051
- const key = tokens[0].value;
3052
- if (key in section.system.atlasMetadata) {
3053
- throw new WorldOrbitError(`Duplicate atlas metadata key "${key}"`, line, tokens[0].column);
4204
+ const entry = parseInfoLikeEntry(tokens, line, "Invalid atlas metadata entry");
4205
+ if (entry.key in section.system.atlasMetadata) {
4206
+ throw new WorldOrbitError(`Duplicate atlas metadata key "${entry.key}"`, line, tokens[0].column);
3054
4207
  }
3055
- section.system.atlasMetadata[key] = joinFieldValue(tokens, line);
4208
+ section.system.atlasMetadata[entry.key] = entry.value;
3056
4209
  return;
3057
4210
  }
3058
4211
  if (tokens.length === 1 && tokens[0].value.toLowerCase() === "metadata") {
@@ -3108,7 +4261,14 @@
3108
4261
  section.viewpoint.rotationDeg = parseFiniteNumber2(value, line, tokens[0].column, "rotation");
3109
4262
  return;
3110
4263
  case "layers":
3111
- section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line);
4264
+ section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line, section.sourceSchemaVersion, section.diagnostics);
4265
+ return;
4266
+ case "events":
4267
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.events", {
4268
+ line,
4269
+ column: tokens[0].column
4270
+ });
4271
+ section.viewpoint.events = parseTokenList(tokens.slice(1), line, "events");
3112
4272
  return;
3113
4273
  default:
3114
4274
  throw new WorldOrbitError(`Unknown viewpoint field "${tokens[0].value}"`, line, tokens[0].column);
@@ -3154,21 +4314,202 @@
3154
4314
  throw new WorldOrbitError(`Unknown annotation field "${tokens[0].value}"`, line, tokens[0].column);
3155
4315
  }
3156
4316
  }
4317
+ function applyGroupField(section, tokens, line) {
4318
+ const key = requireUniqueField(tokens, section.seenFields, line);
4319
+ switch (key) {
4320
+ case "label":
4321
+ section.group.label = joinFieldValue(tokens, line);
4322
+ return;
4323
+ case "summary":
4324
+ section.group.summary = joinFieldValue(tokens, line);
4325
+ return;
4326
+ case "color":
4327
+ section.group.color = joinFieldValue(tokens, line);
4328
+ return;
4329
+ case "tags":
4330
+ section.group.tags = parseTokenList(tokens.slice(1), line, "tags");
4331
+ return;
4332
+ case "hidden":
4333
+ section.group.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4334
+ line,
4335
+ column: tokens[0].column
4336
+ });
4337
+ return;
4338
+ default:
4339
+ throw new WorldOrbitError(`Unknown group field "${tokens[0].value}"`, line, tokens[0].column);
4340
+ }
4341
+ }
4342
+ function applyRelationField(section, tokens, line) {
4343
+ const key = requireUniqueField(tokens, section.seenFields, line);
4344
+ switch (key) {
4345
+ case "from":
4346
+ section.relation.from = joinFieldValue(tokens, line);
4347
+ return;
4348
+ case "to":
4349
+ section.relation.to = joinFieldValue(tokens, line);
4350
+ return;
4351
+ case "kind":
4352
+ section.relation.kind = joinFieldValue(tokens, line);
4353
+ return;
4354
+ case "label":
4355
+ section.relation.label = joinFieldValue(tokens, line);
4356
+ return;
4357
+ case "summary":
4358
+ section.relation.summary = joinFieldValue(tokens, line);
4359
+ return;
4360
+ case "tags":
4361
+ section.relation.tags = parseTokenList(tokens.slice(1), line, "tags");
4362
+ return;
4363
+ case "color":
4364
+ section.relation.color = joinFieldValue(tokens, line);
4365
+ return;
4366
+ case "hidden":
4367
+ section.relation.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4368
+ line,
4369
+ column: tokens[0].column
4370
+ });
4371
+ return;
4372
+ default:
4373
+ throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
4374
+ }
4375
+ }
4376
+ function applyEventField(section, indent, tokens, line) {
4377
+ if (section.activePose && indent <= (section.poseIndent ?? 0)) {
4378
+ section.activePose = null;
4379
+ section.poseIndent = null;
4380
+ section.activePoseSeenFields.clear();
4381
+ }
4382
+ if (!section.activePose && section.inPositions && indent <= (section.positionsIndent ?? 0)) {
4383
+ section.inPositions = false;
4384
+ section.positionsIndent = null;
4385
+ }
4386
+ if (section.activePose) {
4387
+ section.activePose.fields.push(parseEventPoseField(tokens, line, section.activePoseSeenFields));
4388
+ return;
4389
+ }
4390
+ if (section.inPositions) {
4391
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "pose") {
4392
+ throw new WorldOrbitError(`Unknown event positions field "${tokens[0].value}"`, line, tokens[0]?.column ?? 1);
4393
+ }
4394
+ const objectId = tokens[1].value;
4395
+ if (!objectId.trim()) {
4396
+ throw new WorldOrbitError("Event pose object id must not be empty", line, tokens[1].column);
4397
+ }
4398
+ const rawPose = {
4399
+ objectId,
4400
+ fields: [],
4401
+ location: { line, column: tokens[0].column }
4402
+ };
4403
+ section.rawPoses.push(rawPose);
4404
+ section.activePose = rawPose;
4405
+ section.poseIndent = indent;
4406
+ section.activePoseSeenFields = /* @__PURE__ */ new Set();
4407
+ return;
4408
+ }
4409
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "positions") {
4410
+ if (section.seenFields.has("positions")) {
4411
+ throw new WorldOrbitError('Duplicate event field "positions"', line, tokens[0].column);
4412
+ }
4413
+ section.seenFields.add("positions");
4414
+ section.inPositions = true;
4415
+ section.positionsIndent = indent;
4416
+ return;
4417
+ }
4418
+ const key = requireUniqueField(tokens, section.seenFields, line);
4419
+ switch (key) {
4420
+ case "kind":
4421
+ section.event.kind = joinFieldValue(tokens, line);
4422
+ return;
4423
+ case "label":
4424
+ section.event.label = joinFieldValue(tokens, line);
4425
+ return;
4426
+ case "summary":
4427
+ section.event.summary = joinFieldValue(tokens, line);
4428
+ return;
4429
+ case "target":
4430
+ section.event.targetObjectId = joinFieldValue(tokens, line);
4431
+ return;
4432
+ case "participants":
4433
+ section.event.participantObjectIds = parseTokenList(tokens.slice(1), line, "participants");
4434
+ return;
4435
+ case "timing":
4436
+ section.event.timing = joinFieldValue(tokens, line);
4437
+ return;
4438
+ case "visibility":
4439
+ section.event.visibility = joinFieldValue(tokens, line);
4440
+ return;
4441
+ case "tags":
4442
+ section.event.tags = parseTokenList(tokens.slice(1), line, "tags");
4443
+ return;
4444
+ case "color":
4445
+ section.event.color = joinFieldValue(tokens, line);
4446
+ return;
4447
+ case "hidden":
4448
+ section.event.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
4449
+ line,
4450
+ column: tokens[0].column
4451
+ });
4452
+ return;
4453
+ default:
4454
+ throw new WorldOrbitError(`Unknown event field "${tokens[0].value}"`, line, tokens[0].column);
4455
+ }
4456
+ }
4457
+ function parseEventPoseField(tokens, line, seenFields) {
4458
+ if (tokens.length < 2) {
4459
+ throw new WorldOrbitError("Invalid event pose field line", line, tokens[0]?.column ?? 1);
4460
+ }
4461
+ const key = tokens[0].value;
4462
+ if (!EVENT_POSE_FIELD_KEYS.has(key)) {
4463
+ throw new WorldOrbitError(`Unknown event pose field "${key}"`, line, tokens[0].column);
4464
+ }
4465
+ if (seenFields.has(key)) {
4466
+ throw new WorldOrbitError(`Duplicate event pose field "${key}"`, line, tokens[0].column);
4467
+ }
4468
+ seenFields.add(key);
4469
+ return {
4470
+ type: "field",
4471
+ key,
4472
+ values: tokens.slice(1).map((token) => token.value),
4473
+ location: { line, column: tokens[0].column }
4474
+ };
4475
+ }
3157
4476
  function applyObjectField(section, indent, tokens, line) {
3158
- if (tokens.length === 1 && tokens[0].value === "info") {
3159
- section.inInfoBlock = true;
3160
- section.infoIndent = indent;
3161
- return;
3162
- }
3163
- if (section.inInfoBlock && indent <= (section.infoIndent ?? 0)) {
3164
- section.inInfoBlock = false;
3165
- section.infoIndent = null;
4477
+ if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
4478
+ section.activeBlock = null;
4479
+ section.blockIndent = null;
4480
+ }
4481
+ if (tokens.length === 1) {
4482
+ const blockName = tokens[0].value.toLowerCase();
4483
+ if (blockName === "info" || STRUCTURED_TYPED_BLOCKS.has(blockName)) {
4484
+ if (blockName !== "info") {
4485
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, blockName, { line, column: tokens[0].column });
4486
+ }
4487
+ section.activeBlock = blockName;
4488
+ section.blockIndent = indent;
4489
+ return;
4490
+ }
3166
4491
  }
3167
- if (section.inInfoBlock) {
3168
- section.objectNode.infoEntries.push(parseInfoEntry2(tokens, line));
4492
+ if (section.activeBlock) {
4493
+ const entry = parseInfoLikeEntry(tokens, line, `Invalid ${section.activeBlock} entry`);
4494
+ if (section.activeBlock === "info") {
4495
+ if (section.seenInfoKeys.has(entry.key)) {
4496
+ throw new WorldOrbitError(`Duplicate info key "${entry.key}"`, line, tokens[0].column);
4497
+ }
4498
+ section.seenInfoKeys.add(entry.key);
4499
+ section.objectNode.infoEntries.push(entry);
4500
+ return;
4501
+ }
4502
+ const typedBlock = section.activeBlock;
4503
+ const seenKeys = section.seenTypedBlockKeys[typedBlock] ?? (section.seenTypedBlockKeys[typedBlock] = /* @__PURE__ */ new Set());
4504
+ if (seenKeys.has(entry.key)) {
4505
+ throw new WorldOrbitError(`Duplicate ${typedBlock} key "${entry.key}"`, line, tokens[0].column);
4506
+ }
4507
+ seenKeys.add(entry.key);
4508
+ const entries = section.objectNode.typedBlockEntries[typedBlock] ?? (section.objectNode.typedBlockEntries[typedBlock] = []);
4509
+ entries.push(entry);
3169
4510
  return;
3170
4511
  }
3171
- section.objectNode.blockFields.push(parseField2(tokens, line));
4512
+ section.objectNode.fields.push(parseObjectField(tokens, line, section.objectNode.objectType, section.sourceSchemaVersion, section.diagnostics));
3172
4513
  }
3173
4514
  function requireUniqueField(tokens, seenFields, line) {
3174
4515
  if (tokens.length < 2) {
@@ -3188,50 +4529,46 @@
3188
4529
  return tokens.slice(1).map((token) => token.value).join(" ").trim();
3189
4530
  }
3190
4531
  function parseObjectTypeTokens(tokens, line) {
3191
- if (tokens.length === 0) {
3192
- throw new WorldOrbitError("Missing value for atlas field", line);
3193
- }
3194
- return tokens.map((token) => {
3195
- const value = token.value;
3196
- if (value !== "star" && value !== "planet" && value !== "moon" && value !== "belt" && value !== "asteroid" && value !== "comet" && value !== "ring" && value !== "structure" && value !== "phenomenon") {
3197
- throw new WorldOrbitError(`Unknown viewpoint object type "${token.value}"`, line, token.column);
3198
- }
3199
- return value;
3200
- });
4532
+ 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");
3201
4533
  }
3202
- function parseTokenList(tokens, line, field) {
3203
- if (tokens.length === 0) {
3204
- throw new WorldOrbitError(`Missing value for field "${field}"`, line);
4534
+ function parseLayerTokens(tokens, line, sourceSchemaVersion, diagnostics) {
4535
+ const layers = {};
4536
+ for (const token of parseTokenList(tokens, line, "layers")) {
4537
+ const enabled = !token.startsWith("-") && !token.startsWith("!");
4538
+ const raw = token.replace(/^[-!]+/, "").toLowerCase();
4539
+ if (raw === "orbits") {
4540
+ layers["orbits-back"] = enabled;
4541
+ layers["orbits-front"] = enabled;
4542
+ continue;
4543
+ }
4544
+ if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "events" || raw === "objects" || raw === "labels" || raw === "metadata") {
4545
+ if (raw === "events" && sourceSchemaVersion && diagnostics) {
4546
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "layers.events", {
4547
+ line,
4548
+ column: tokens[0]?.column ?? 1
4549
+ });
4550
+ }
4551
+ layers[raw] = enabled;
4552
+ }
3205
4553
  }
3206
- return tokens.map((token) => token.value);
4554
+ return layers;
3207
4555
  }
3208
- function parseLayerTokens(tokens, line) {
4556
+ function parseTokenList(tokens, line, fieldName) {
3209
4557
  if (tokens.length === 0) {
3210
- throw new WorldOrbitError('Missing value for field "layers"', line);
4558
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, 1);
3211
4559
  }
3212
- const next = {};
3213
- for (const token of tokens) {
3214
- const enabled = !token.value.startsWith("-") && !token.value.startsWith("!");
3215
- const rawLayer = token.value.replace(/^[-!]+/, "").toLowerCase();
3216
- if (rawLayer === "orbits") {
3217
- next["orbits-back"] = enabled;
3218
- next["orbits-front"] = enabled;
3219
- continue;
3220
- }
3221
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
3222
- next[rawLayer] = enabled;
3223
- continue;
3224
- }
3225
- throw new WorldOrbitError(`Unknown layer token "${token.value}"`, line, token.column);
4560
+ const values = tokens.map((token) => token.value).filter(Boolean);
4561
+ if (values.length === 0) {
4562
+ throw new WorldOrbitError(`Missing value for atlas field "${fieldName}"`, line, tokens[0]?.column ?? 1);
3226
4563
  }
3227
- return next;
4564
+ return values;
3228
4565
  }
3229
4566
  function parseProjectionValue(value, line, column) {
3230
4567
  const normalized = value.toLowerCase();
3231
- if (normalized === "topdown" || normalized === "isometric") {
3232
- return normalized;
4568
+ if (normalized !== "topdown" && normalized !== "isometric") {
4569
+ throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
3233
4570
  }
3234
- throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
4571
+ return normalized;
3235
4572
  }
3236
4573
  function parsePresetValue(value, line, column) {
3237
4574
  const normalized = value.toLowerCase();
@@ -3241,16 +4578,16 @@
3241
4578
  throw new WorldOrbitError(`Unknown render preset "${value}"`, line, column);
3242
4579
  }
3243
4580
  function parsePositiveNumber2(value, line, column, field) {
3244
- const parsed = Number(value);
3245
- if (!Number.isFinite(parsed) || parsed <= 0) {
3246
- throw new WorldOrbitError(`Field "${field}" expects a positive number`, line, column);
4581
+ const parsed = parseFiniteNumber2(value, line, column, field);
4582
+ if (parsed <= 0) {
4583
+ throw new WorldOrbitError(`Field "${field}" must be greater than zero`, line, column);
3247
4584
  }
3248
4585
  return parsed;
3249
4586
  }
3250
4587
  function parseFiniteNumber2(value, line, column, field) {
3251
4588
  const parsed = Number(value);
3252
4589
  if (!Number.isFinite(parsed)) {
3253
- throw new WorldOrbitError(`Field "${field}" expects a finite number`, line, column);
4590
+ throw new WorldOrbitError(`Invalid numeric value "${value}" for "${field}"`, line, column);
3254
4591
  }
3255
4592
  return parsed;
3256
4593
  }
@@ -3262,28 +4599,43 @@
3262
4599
  groupIds: []
3263
4600
  };
3264
4601
  }
3265
- function parseInlineFields2(tokens, line) {
4602
+ function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
3266
4603
  const fields = [];
3267
4604
  let index = 0;
3268
4605
  while (index < tokens.length) {
3269
4606
  const keyToken = tokens[index];
3270
- const schema = getFieldSchema(keyToken.value);
3271
- if (!schema) {
4607
+ const spec = getDraftObjectFieldSpec(keyToken.value);
4608
+ if (!spec) {
3272
4609
  throw new WorldOrbitError(`Unknown field "${keyToken.value}"`, line, keyToken.column);
3273
4610
  }
4611
+ if (spec.version === "2.1") {
4612
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, keyToken.value, {
4613
+ line,
4614
+ column: keyToken.column
4615
+ });
4616
+ }
3274
4617
  index++;
3275
4618
  const valueTokens = [];
3276
- if (schema.arity === "multiple") {
3277
- while (index < tokens.length && !isKnownFieldKey(tokens[index].value)) {
3278
- valueTokens.push(tokens[index]);
3279
- index++;
3280
- }
3281
- } else {
4619
+ if (spec.inlineMode === "single") {
3282
4620
  const nextToken = tokens[index];
3283
4621
  if (nextToken) {
3284
4622
  valueTokens.push(nextToken);
3285
4623
  index++;
3286
4624
  }
4625
+ } else if (spec.inlineMode === "pair") {
4626
+ for (let count = 0; count < 2; count++) {
4627
+ const nextToken = tokens[index];
4628
+ if (!nextToken) {
4629
+ break;
4630
+ }
4631
+ valueTokens.push(nextToken);
4632
+ index++;
4633
+ }
4634
+ } else {
4635
+ while (index < tokens.length && !DRAFT_OBJECT_FIELD_KEYS.has(tokens[index].value)) {
4636
+ valueTokens.push(tokens[index]);
4637
+ index++;
4638
+ }
3287
4639
  }
3288
4640
  if (valueTokens.length === 0) {
3289
4641
  throw new WorldOrbitError(`Missing value for field "${keyToken.value}"`, line, keyToken.column);
@@ -3295,25 +4647,35 @@
3295
4647
  location: { line, column: keyToken.column }
3296
4648
  });
3297
4649
  }
4650
+ validateDraftObjectFieldCompatibility(fields, objectType);
3298
4651
  return fields;
3299
4652
  }
3300
- function parseField2(tokens, line) {
4653
+ function parseObjectField(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
3301
4654
  if (tokens.length < 2) {
3302
4655
  throw new WorldOrbitError("Invalid field line", line, tokens[0]?.column ?? 1);
3303
4656
  }
3304
- if (!getFieldSchema(tokens[0].value)) {
4657
+ const spec = getDraftObjectFieldSpec(tokens[0].value);
4658
+ if (!spec) {
3305
4659
  throw new WorldOrbitError(`Unknown field "${tokens[0].value}"`, line, tokens[0].column);
3306
4660
  }
3307
- return {
4661
+ if (spec.version === "2.1") {
4662
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, tokens[0].value, {
4663
+ line,
4664
+ column: tokens[0].column
4665
+ });
4666
+ }
4667
+ const field = {
3308
4668
  type: "field",
3309
4669
  key: tokens[0].value,
3310
4670
  values: tokens.slice(1).map((token) => token.value),
3311
4671
  location: { line, column: tokens[0].column }
3312
4672
  };
4673
+ validateDraftObjectFieldCompatibility([field], objectType);
4674
+ return field;
3313
4675
  }
3314
- function parseInfoEntry2(tokens, line) {
4676
+ function parseInfoLikeEntry(tokens, line, errorMessage) {
3315
4677
  if (tokens.length < 2) {
3316
- throw new WorldOrbitError("Invalid info entry", line, tokens[0]?.column ?? 1);
4678
+ throw new WorldOrbitError(errorMessage, line, tokens[0]?.column ?? 1);
3317
4679
  }
3318
4680
  return {
3319
4681
  type: "info-entry",
@@ -3322,18 +4684,366 @@
3322
4684
  location: { line, column: tokens[0].column }
3323
4685
  };
3324
4686
  }
3325
- function normalizeIdentifier(value) {
3326
- return value.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
4687
+ function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
4688
+ const fieldMap = collectDraftFields(node.fields);
4689
+ const placement = extractPlacementFromFieldMap(fieldMap);
4690
+ const properties = normalizeDraftProperties(node.objectType, fieldMap);
4691
+ const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
4692
+ const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
4693
+ const referencePlane = parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0]);
4694
+ const tidalLock = fieldMap.has("tidalLock") ? parseAtlasBoolean(singleFieldValue2(fieldMap.get("tidalLock")[0]), "tidalLock", fieldMap.get("tidalLock")[0].location) : void 0;
4695
+ const resonance = fieldMap.has("resonance") ? parseResonanceField(fieldMap.get("resonance")[0]) : void 0;
4696
+ const renderHints = extractRenderHints(fieldMap);
4697
+ const deriveRules = fieldMap.get("derive")?.map((field) => parseDeriveField(field));
4698
+ const validationRules = fieldMap.get("validate")?.map((field) => ({
4699
+ rule: singleFieldValue2(field)
4700
+ }));
4701
+ const lockedFields = fieldMap.has("locked") ? [...new Set(fieldMap.get("locked").flatMap((field) => field.values))] : void 0;
4702
+ const tolerances = fieldMap.get("tolerance")?.map((field) => parseToleranceField(field));
4703
+ const typedBlocks = normalizeTypedBlocks(node.typedBlockEntries);
4704
+ const info2 = normalizeInfoEntries(node.infoEntries, "info");
4705
+ const object = {
4706
+ type: node.objectType,
4707
+ id: node.id,
4708
+ properties,
4709
+ placement,
4710
+ info: info2
4711
+ };
4712
+ if (groups.length > 0)
4713
+ object.groups = groups;
4714
+ if (epoch)
4715
+ object.epoch = epoch;
4716
+ if (referencePlane)
4717
+ object.referencePlane = referencePlane;
4718
+ if (tidalLock !== void 0)
4719
+ object.tidalLock = tidalLock;
4720
+ if (resonance)
4721
+ object.resonance = resonance;
4722
+ if (renderHints)
4723
+ object.renderHints = renderHints;
4724
+ if (deriveRules?.length)
4725
+ object.deriveRules = deriveRules;
4726
+ if (validationRules?.length)
4727
+ object.validationRules = validationRules;
4728
+ if (lockedFields?.length)
4729
+ object.lockedFields = lockedFields;
4730
+ if (tolerances?.length)
4731
+ object.tolerances = tolerances;
4732
+ if (typedBlocks && Object.keys(typedBlocks).length > 0)
4733
+ object.typedBlocks = typedBlocks;
4734
+ if (sourceSchemaVersion !== "2.1") {
4735
+ 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) {
4736
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
4737
+ }
4738
+ }
4739
+ return object;
4740
+ }
4741
+ function normalizeDraftEvent(event, rawPoses) {
4742
+ return {
4743
+ ...event,
4744
+ participantObjectIds: [...new Set(event.participantObjectIds)],
4745
+ tags: [...new Set(event.tags)],
4746
+ positions: rawPoses.map((pose) => normalizeDraftEventPose(pose))
4747
+ };
3327
4748
  }
3328
- function humanizeIdentifier2(value) {
3329
- return value.split(/[-_]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join(" ");
4749
+ function normalizeDraftEventPose(rawPose) {
4750
+ const fieldMap = collectDraftFields(rawPose.fields);
4751
+ const placement = extractPlacementFromFieldMap(fieldMap);
4752
+ return {
4753
+ objectId: rawPose.objectId,
4754
+ placement,
4755
+ inner: parseOptionalUnitField(fieldMap.get("inner")?.[0], "inner"),
4756
+ outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer")
4757
+ };
4758
+ }
4759
+ function collectDraftFields(fields) {
4760
+ const grouped = /* @__PURE__ */ new Map();
4761
+ for (const field of fields) {
4762
+ const spec = getDraftObjectFieldSpec(field.key);
4763
+ if (!spec) {
4764
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
4765
+ }
4766
+ if (!spec.allowRepeat && grouped.has(field.key)) {
4767
+ throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
4768
+ }
4769
+ const existing = grouped.get(field.key) ?? [];
4770
+ existing.push(field);
4771
+ grouped.set(field.key, existing);
4772
+ }
4773
+ return grouped;
4774
+ }
4775
+ function extractPlacementFromFieldMap(fieldMap) {
4776
+ const orbitField = fieldMap.get("orbit")?.[0];
4777
+ const atField = fieldMap.get("at")?.[0];
4778
+ const surfaceField = fieldMap.get("surface")?.[0];
4779
+ const freeField = fieldMap.get("free")?.[0];
4780
+ const count = [orbitField, atField, surfaceField, freeField].filter(Boolean).length;
4781
+ if (count > 1) {
4782
+ const conflictingField = orbitField ?? atField ?? surfaceField ?? freeField;
4783
+ throw WorldOrbitError.fromLocation("Object has multiple placement modes", conflictingField?.location);
4784
+ }
4785
+ if (orbitField) {
4786
+ return {
4787
+ mode: "orbit",
4788
+ target: singleFieldValue2(orbitField),
4789
+ distance: parseOptionalUnitField(fieldMap.get("distance")?.[0], "distance"),
4790
+ semiMajor: parseOptionalUnitField(fieldMap.get("semiMajor")?.[0], "semiMajor"),
4791
+ eccentricity: parseOptionalNumberField(fieldMap.get("eccentricity")?.[0], "eccentricity"),
4792
+ period: parseOptionalUnitField(fieldMap.get("period")?.[0], "period"),
4793
+ angle: parseOptionalUnitField(fieldMap.get("angle")?.[0], "angle"),
4794
+ inclination: parseOptionalUnitField(fieldMap.get("inclination")?.[0], "inclination"),
4795
+ phase: parseOptionalUnitField(fieldMap.get("phase")?.[0], "phase")
4796
+ };
4797
+ }
4798
+ if (atField) {
4799
+ const target = singleFieldValue2(atField);
4800
+ return {
4801
+ mode: "at",
4802
+ target,
4803
+ reference: parseAtlasAtReference(target, atField.location)
4804
+ };
4805
+ }
4806
+ if (surfaceField) {
4807
+ return {
4808
+ mode: "surface",
4809
+ target: singleFieldValue2(surfaceField)
4810
+ };
4811
+ }
4812
+ if (freeField) {
4813
+ const raw = singleFieldValue2(freeField);
4814
+ const distance = tryParseAtlasUnitValue(raw);
4815
+ return {
4816
+ mode: "free",
4817
+ distance: distance ?? void 0,
4818
+ descriptor: distance ? void 0 : raw
4819
+ };
4820
+ }
4821
+ return null;
4822
+ }
4823
+ function normalizeDraftProperties(objectType, fieldMap) {
4824
+ const properties = {};
4825
+ for (const [key, fields] of fieldMap.entries()) {
4826
+ const field = fields[0];
4827
+ const spec = getDraftObjectFieldSpec(key);
4828
+ if (!field || !spec?.legacySchema || spec.legacySchema.placement) {
4829
+ continue;
4830
+ }
4831
+ ensureAtlasFieldSupported(key, objectType, field.location);
4832
+ properties[key] = normalizeLegacyScalarValue(key, field.values, field.location);
4833
+ }
4834
+ return properties;
4835
+ }
4836
+ function normalizeInfoEntries(entries, label) {
4837
+ const normalized = {};
4838
+ for (const entry of entries) {
4839
+ if (entry.key in normalized) {
4840
+ throw WorldOrbitError.fromLocation(`Duplicate ${label} key "${entry.key}"`, entry.location);
4841
+ }
4842
+ normalized[entry.key] = entry.value;
4843
+ }
4844
+ return normalized;
4845
+ }
4846
+ function normalizeTypedBlocks(typedBlockEntries) {
4847
+ const typedBlocks = {};
4848
+ for (const blockName of Object.keys(typedBlockEntries)) {
4849
+ const entries = typedBlockEntries[blockName];
4850
+ if (entries?.length) {
4851
+ typedBlocks[blockName] = normalizeInfoEntries(entries, blockName);
4852
+ }
4853
+ }
4854
+ return typedBlocks;
4855
+ }
4856
+ function extractRenderHints(fieldMap) {
4857
+ const renderHints = {};
4858
+ const renderLabelField = fieldMap.get("renderLabel")?.[0];
4859
+ const renderOrbitField = fieldMap.get("renderOrbit")?.[0];
4860
+ const renderPriorityField = fieldMap.get("renderPriority")?.[0];
4861
+ if (renderLabelField) {
4862
+ renderHints.renderLabel = parseAtlasBoolean(singleFieldValue2(renderLabelField), "renderLabel", renderLabelField.location);
4863
+ }
4864
+ if (renderOrbitField) {
4865
+ renderHints.renderOrbit = parseAtlasBoolean(singleFieldValue2(renderOrbitField), "renderOrbit", renderOrbitField.location);
4866
+ }
4867
+ if (renderPriorityField) {
4868
+ renderHints.renderPriority = parseAtlasNumber(singleFieldValue2(renderPriorityField), "renderPriority", renderPriorityField.location);
4869
+ }
4870
+ return Object.keys(renderHints).length > 0 ? renderHints : void 0;
4871
+ }
4872
+ function parseResonanceField(field) {
4873
+ if (field.values.length !== 2) {
4874
+ throw WorldOrbitError.fromLocation('Field "resonance" expects "<targetObjectId> <ratio>"', field.location);
4875
+ }
4876
+ const ratio = field.values[1];
4877
+ if (!/^\d+:\d+$/.test(ratio)) {
4878
+ throw WorldOrbitError.fromLocation(`Invalid resonance ratio "${ratio}"`, field.location);
4879
+ }
4880
+ return {
4881
+ targetObjectId: field.values[0],
4882
+ ratio
4883
+ };
4884
+ }
4885
+ function parseDeriveField(field) {
4886
+ if (field.values.length !== 2) {
4887
+ throw WorldOrbitError.fromLocation('Field "derive" expects "<field> <strategy>"', field.location);
4888
+ }
4889
+ return {
4890
+ field: field.values[0],
4891
+ strategy: field.values[1]
4892
+ };
4893
+ }
4894
+ function parseToleranceField(field) {
4895
+ if (field.values.length !== 2) {
4896
+ throw WorldOrbitError.fromLocation('Field "tolerance" expects "<field> <value>"', field.location);
4897
+ }
4898
+ const rawValue = field.values[1];
4899
+ const unitValue = tryParseAtlasUnitValue(rawValue);
4900
+ const numericValue2 = Number(rawValue);
4901
+ return {
4902
+ field: field.values[0],
4903
+ value: unitValue ?? (Number.isFinite(numericValue2) ? numericValue2 : rawValue)
4904
+ };
4905
+ }
4906
+ function parseOptionalTokenList(field) {
4907
+ return field ? [...new Set(field.values)] : [];
4908
+ }
4909
+ function parseOptionalJoinedValue(field) {
4910
+ if (!field) {
4911
+ return null;
4912
+ }
4913
+ return field.values.join(" ").trim() || null;
4914
+ }
4915
+ function parseOptionalUnitField(field, key) {
4916
+ return field ? parseAtlasUnitValue(singleFieldValue2(field), field.location, key) : void 0;
4917
+ }
4918
+ function parseOptionalNumberField(field, key) {
4919
+ return field ? parseAtlasNumber(singleFieldValue2(field), key, field.location) : void 0;
4920
+ }
4921
+ function singleFieldValue2(field) {
4922
+ return singleAtlasValue(field.values, field.key, field.location);
4923
+ }
4924
+ function getDraftObjectFieldSpec(key) {
4925
+ return DRAFT_OBJECT_FIELD_SPECS.get(key);
4926
+ }
4927
+ function validateDraftObjectFieldCompatibility(fields, objectType) {
4928
+ for (const field of fields) {
4929
+ const spec = getDraftObjectFieldSpec(field.key);
4930
+ if (!spec) {
4931
+ throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
4932
+ }
4933
+ if (spec.legacySchema) {
4934
+ ensureAtlasFieldSupported(field.key, objectType, field.location);
4935
+ continue;
4936
+ }
4937
+ if ((field.key === "renderLabel" || field.key === "renderOrbit" || field.key === "tidalLock") && field.values.length !== 1) {
4938
+ throw WorldOrbitError.fromLocation(`Field "${field.key}" expects exactly one value`, field.location);
4939
+ }
4940
+ }
4941
+ }
4942
+ function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
4943
+ if (sourceSchemaVersion === "2.1") {
4944
+ return;
4945
+ }
4946
+ diagnostics.push({
4947
+ code: "parse.schema21.featureCompatibility",
4948
+ severity: "warning",
4949
+ source: "parse",
4950
+ message: `Feature "${featureName}" requires schema 2.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
4951
+ line: location.line,
4952
+ column: location.column
4953
+ });
4954
+ }
4955
+ function preprocessAtlasSource(source) {
4956
+ const chars = [...source];
4957
+ const comments = [];
4958
+ let inString = false;
4959
+ let inBlockComment = false;
4960
+ let blockCommentStart = null;
4961
+ let line = 1;
4962
+ let column = 1;
4963
+ for (let index = 0; index < chars.length; index++) {
4964
+ const ch = chars[index];
4965
+ const next = chars[index + 1];
4966
+ if (inBlockComment) {
4967
+ if (ch === "*" && next === "/") {
4968
+ chars[index] = " ";
4969
+ chars[index + 1] = " ";
4970
+ inBlockComment = false;
4971
+ blockCommentStart = null;
4972
+ index++;
4973
+ column += 2;
4974
+ continue;
4975
+ }
4976
+ if (ch !== "\n" && ch !== "\r") {
4977
+ chars[index] = " ";
4978
+ }
4979
+ if (ch === "\n") {
4980
+ line++;
4981
+ column = 1;
4982
+ } else {
4983
+ column++;
4984
+ }
4985
+ continue;
4986
+ }
4987
+ if (!inString && ch === "/" && next === "*") {
4988
+ comments.push({ kind: "block", line, column });
4989
+ chars[index] = " ";
4990
+ chars[index + 1] = " ";
4991
+ inBlockComment = true;
4992
+ blockCommentStart = { line, column };
4993
+ index++;
4994
+ column += 2;
4995
+ continue;
4996
+ }
4997
+ if (!inString && ch === "#" && !isHexColorLiteral(chars, index)) {
4998
+ comments.push({ kind: "line", line, column });
4999
+ chars[index] = " ";
5000
+ let inner = index + 1;
5001
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
5002
+ chars[inner] = " ";
5003
+ inner++;
5004
+ }
5005
+ column += inner - index;
5006
+ index = inner - 1;
5007
+ continue;
5008
+ }
5009
+ if (ch === '"' && chars[index - 1] !== "\\") {
5010
+ inString = !inString;
5011
+ }
5012
+ if (ch === "\n") {
5013
+ line++;
5014
+ column = 1;
5015
+ } else {
5016
+ column++;
5017
+ }
5018
+ }
5019
+ if (inBlockComment) {
5020
+ throw WorldOrbitError.fromLocation("Unclosed block comment", blockCommentStart ?? void 0);
5021
+ }
5022
+ return {
5023
+ source: chars.join(""),
5024
+ comments
5025
+ };
5026
+ }
5027
+ function isHexColorLiteral(chars, start) {
5028
+ let index = start + 1;
5029
+ let length = 0;
5030
+ while (index < chars.length && /[0-9a-f]/i.test(chars[index] ?? "")) {
5031
+ index++;
5032
+ length++;
5033
+ }
5034
+ if (![3, 4, 6, 8].includes(length)) {
5035
+ return false;
5036
+ }
5037
+ const next = chars[index];
5038
+ return next === void 0 || next === " " || next === " " || next === "\r" || next === "\n";
3330
5039
  }
3331
5040
 
3332
5041
  // packages/core/dist/load.js
3333
- var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0)?$/i;
5042
+ var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1)?$/i;
5043
+ var ATLAS_SCHEMA_21_PATTERN = /^schema\s+2\.1$/i;
3334
5044
  var LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
3335
5045
  function detectWorldOrbitSchemaVersion(source) {
3336
- for (const line of source.split(/\r?\n/)) {
5046
+ for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
3337
5047
  const trimmed = line.trim();
3338
5048
  if (!trimmed) {
3339
5049
  continue;
@@ -3341,6 +5051,9 @@
3341
5051
  if (LEGACY_DRAFT_SCHEMA_PATTERN.test(trimmed)) {
3342
5052
  return "2.0-draft";
3343
5053
  }
5054
+ if (ATLAS_SCHEMA_21_PATTERN.test(trimmed)) {
5055
+ return "2.1";
5056
+ }
3344
5057
  if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
3345
5058
  return "2.0";
3346
5059
  }
@@ -3348,6 +5061,49 @@
3348
5061
  }
3349
5062
  return "1.0";
3350
5063
  }
5064
+ function stripCommentsForSchemaDetection(source) {
5065
+ const chars = [...source];
5066
+ let inString = false;
5067
+ let inBlockComment = false;
5068
+ for (let index = 0; index < chars.length; index++) {
5069
+ const ch = chars[index];
5070
+ const next = chars[index + 1];
5071
+ if (inBlockComment) {
5072
+ if (ch === "*" && next === "/") {
5073
+ chars[index] = " ";
5074
+ chars[index + 1] = " ";
5075
+ inBlockComment = false;
5076
+ index++;
5077
+ continue;
5078
+ }
5079
+ if (ch !== "\n" && ch !== "\r") {
5080
+ chars[index] = " ";
5081
+ }
5082
+ continue;
5083
+ }
5084
+ if (!inString && ch === "/" && next === "*") {
5085
+ chars[index] = " ";
5086
+ chars[index + 1] = " ";
5087
+ inBlockComment = true;
5088
+ index++;
5089
+ continue;
5090
+ }
5091
+ if (!inString && ch === "#") {
5092
+ chars[index] = " ";
5093
+ let inner = index + 1;
5094
+ while (inner < chars.length && chars[inner] !== "\n" && chars[inner] !== "\r") {
5095
+ chars[inner] = " ";
5096
+ inner++;
5097
+ }
5098
+ index = inner - 1;
5099
+ continue;
5100
+ }
5101
+ if (ch === '"' && chars[index - 1] !== "\\") {
5102
+ inString = !inString;
5103
+ }
5104
+ }
5105
+ return chars.join("");
5106
+ }
3351
5107
  function loadWorldOrbitSource(source) {
3352
5108
  const result = loadWorldOrbitSourceWithDiagnostics(source);
3353
5109
  if (!result.ok || !result.value) {
@@ -3358,36 +5114,36 @@
3358
5114
  }
3359
5115
  function loadWorldOrbitSourceWithDiagnostics(source) {
3360
5116
  const schemaVersion = detectWorldOrbitSchemaVersion(source);
3361
- if (schemaVersion === "2.0" || schemaVersion === "2.0-draft") {
5117
+ if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1") {
3362
5118
  return loadAtlasSourceWithDiagnostics(source, schemaVersion);
3363
5119
  }
3364
5120
  let ast;
3365
5121
  try {
3366
5122
  ast = parseWorldOrbit(source);
3367
- } catch (error) {
5123
+ } catch (error2) {
3368
5124
  return {
3369
5125
  ok: false,
3370
5126
  value: null,
3371
- diagnostics: [diagnosticFromError(error, "parse")]
5127
+ diagnostics: [diagnosticFromError(error2, "parse")]
3372
5128
  };
3373
5129
  }
3374
5130
  let document2;
3375
5131
  try {
3376
5132
  document2 = normalizeDocument(ast);
3377
- } catch (error) {
5133
+ } catch (error2) {
3378
5134
  return {
3379
5135
  ok: false,
3380
5136
  value: null,
3381
- diagnostics: [diagnosticFromError(error, "normalize")]
5137
+ diagnostics: [diagnosticFromError(error2, "normalize")]
3382
5138
  };
3383
5139
  }
3384
5140
  try {
3385
5141
  validateDocument(document2);
3386
- } catch (error) {
5142
+ } catch (error2) {
3387
5143
  return {
3388
5144
  ok: false,
3389
5145
  value: null,
3390
- diagnostics: [diagnosticFromError(error, "validate")]
5146
+ diagnostics: [diagnosticFromError(error2, "validate")]
3391
5147
  };
3392
5148
  }
3393
5149
  return {
@@ -3407,30 +5163,29 @@
3407
5163
  let atlasDocument;
3408
5164
  try {
3409
5165
  atlasDocument = parseWorldOrbitAtlas(source);
3410
- } catch (error) {
5166
+ } catch (error2) {
3411
5167
  return {
3412
5168
  ok: false,
3413
5169
  value: null,
3414
- diagnostics: [diagnosticFromError(error, "parse", "load.atlas.failed")]
5170
+ diagnostics: [diagnosticFromError(error2, "parse", "load.atlas.failed")]
3415
5171
  };
3416
5172
  }
3417
- let document2;
3418
- try {
3419
- document2 = materializeAtlasDocument(atlasDocument);
3420
- } catch (error) {
5173
+ const atlasDiagnostics = [...atlasDocument.diagnostics];
5174
+ if (atlasDiagnostics.some((diagnostic) => diagnostic.severity === "error")) {
3421
5175
  return {
3422
5176
  ok: false,
3423
5177
  value: null,
3424
- diagnostics: [diagnosticFromError(error, "normalize", "load.atlas.materialize.failed")]
5178
+ diagnostics: atlasDiagnostics
3425
5179
  };
3426
5180
  }
5181
+ let document2;
3427
5182
  try {
3428
- validateDocument(document2);
3429
- } catch (error) {
5183
+ document2 = materializeAtlasDocument(atlasDocument);
5184
+ } catch (error2) {
3430
5185
  return {
3431
5186
  ok: false,
3432
5187
  value: null,
3433
- diagnostics: [diagnosticFromError(error, "validate", "load.atlas.validate.failed")]
5188
+ diagnostics: [diagnosticFromError(error2, "normalize", "load.atlas.materialize.failed")]
3434
5189
  };
3435
5190
  }
3436
5191
  const loaded = {
@@ -3439,12 +5194,12 @@
3439
5194
  document: document2,
3440
5195
  atlasDocument,
3441
5196
  draftDocument: atlasDocument,
3442
- diagnostics: [...atlasDocument.diagnostics]
5197
+ diagnostics: atlasDiagnostics
3443
5198
  };
3444
5199
  return {
3445
5200
  ok: true,
3446
5201
  value: loaded,
3447
- diagnostics: [...atlasDocument.diagnostics]
5202
+ diagnostics: atlasDiagnostics
3448
5203
  };
3449
5204
  }
3450
5205
 
@@ -3627,6 +5382,8 @@
3627
5382
  const imageDefinitions = buildImageDefinitions(visibleObjects);
3628
5383
  const orbitMarkup = layers.orbits ? renderOrbitLayer(scene, visibleObjectIds, layers.structures) : { back: "", front: "" };
3629
5384
  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("") : "";
5385
+ 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("") : "";
5386
+ const eventMarkup = layers.events ? scene.events.filter((event) => !event.hidden).map((event) => renderSceneEventOverlay(scene, event, visibleObjectIds, theme)).join("") : "";
3630
5387
  const objectMarkup = layers.objects ? visibleObjects.map((object) => renderSceneObject(object, options.selectedObjectId ?? null, theme)).join("") : "";
3631
5388
  const labelMarkup = layers.labels ? visibleLabels.map((label) => renderSceneLabel(scene, label, options.selectedObjectId ?? null)).join("") : "";
3632
5389
  const metadataMarkup = layers.metadata ? `<text class="wo-title" x="56" y="64">${escapeXml(scene.title)}</text>
@@ -3661,6 +5418,10 @@
3661
5418
  .wo-orbit-back { opacity: 0.38; stroke-dasharray: 8 6; }
3662
5419
  .wo-orbit-front { opacity: 0.9; }
3663
5420
  .wo-orbit-band { stroke: ${theme.orbitBand}; stroke-linecap: round; }
5421
+ .wo-relation { stroke: ${theme.relation}; stroke-width: 2; stroke-dasharray: 10 6; }
5422
+ .wo-event-line { stroke: ${theme.accent}; stroke-width: 1.6; stroke-dasharray: 5 5; opacity: 0.72; }
5423
+ .wo-event-node { fill: ${theme.accent}; stroke: ${theme.selected}; stroke-width: 1.4; opacity: 0.92; }
5424
+ .wo-event-label { fill: ${theme.accent}; font-family: ${theme.fontFamily}; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
3664
5425
  .wo-leader { stroke: ${theme.leader}; stroke-width: 1.5; stroke-dasharray: 6 5; }
3665
5426
  .wo-label { fill: ${theme.ink}; font-family: ${theme.fontFamily}; font-weight: 600; letter-spacing: 0.02em; }
3666
5427
  .wo-label-secondary { fill: ${theme.muted}; font-family: ${theme.fontFamily}; font-weight: 500; }
@@ -3694,6 +5455,8 @@
3694
5455
  <g data-worldorbit-world-content="true">
3695
5456
  ${layers.orbits ? `<g data-layer-id="orbits-back">${orbitMarkup.back}</g>` : ""}
3696
5457
  ${layers.guides ? `<g data-layer-id="guides">${leaderMarkup}</g>` : ""}
5458
+ ${layers.relations ? `<g data-layer-id="relations">${relationMarkup}</g>` : ""}
5459
+ ${layers.events ? `<g data-layer-id="events">${eventMarkup}</g>` : ""}
3697
5460
  ${layers.objects ? `<g data-layer-id="objects">${objectMarkup}</g>` : ""}
3698
5461
  ${layers.orbits ? `<g data-layer-id="orbits-front">${orbitMarkup.front}</g>` : ""}
3699
5462
  ${layers.labels ? `<g data-layer-id="labels">${labelMarkup}</g>` : ""}
@@ -3701,6 +5464,20 @@
3701
5464
  </g>
3702
5465
  </g>
3703
5466
  </svg>`;
5467
+ }
5468
+ function renderSceneEventOverlay(scene, event, visibleObjectIds, theme) {
5469
+ const participants = event.objectIds.filter((objectId) => visibleObjectIds.has(objectId)).map((objectId) => scene.objects.find((object) => object.objectId === objectId && !object.hidden)).filter(Boolean);
5470
+ if (participants.length === 0) {
5471
+ return "";
5472
+ }
5473
+ const stroke = event.event.color || theme.accent;
5474
+ const label = event.event.label || event.event.id;
5475
+ 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("");
5476
+ return `<g class="wo-event" data-render-id="${escapeXml(event.renderId)}" data-event-id="${escapeAttribute(event.eventId)}">
5477
+ ${lineMarkup}
5478
+ <circle class="wo-event-node" cx="${event.x}" cy="${event.y}" r="5" fill="${escapeAttribute(stroke)}" />
5479
+ <text class="wo-event-label" x="${event.x}" y="${event.y - 10}" text-anchor="middle" font-size="10">${escapeXml(label)}</text>
5480
+ </g>`;
3704
5481
  }
3705
5482
  function renderDocumentToSvg(document2, options = {}) {
3706
5483
  return renderSceneToSvg(renderDocumentToScene(document2, options), options);
@@ -3754,10 +5531,11 @@
3754
5531
  function renderSceneObject(sceneObject, selectedObjectId, theme) {
3755
5532
  const { object, x, y, radius, visualRadius } = sceneObject;
3756
5533
  const selectionClass = selectedObjectId === sceneObject.objectId ? " wo-object-selected" : "";
5534
+ const kindClass = object.properties.kind ? ` wo-kind-${String(object.properties.kind).toLowerCase().replace(/[^a-z0-9-]/g, "-")}` : "";
3757
5535
  const palette = resolveObjectPalette(sceneObject, theme);
3758
5536
  const imageMarkup = renderObjectImage(sceneObject);
3759
5537
  const outlineMarkup = imageMarkup ? renderObjectBody(object, x, y, radius, palette, { outlineOnly: true }) : "";
3760
- 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}`)}">
5538
+ 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}`)}">
3761
5539
  <circle class="wo-selection-ring" cx="${x}" cy="${y}" r="${visualRadius + 8}" />
3762
5540
  ${renderAtmosphere(sceneObject, palette)}
3763
5541
  ${renderObjectBody(object, x, y, radius, palette)}
@@ -3791,8 +5569,33 @@
3791
5569
  <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
3792
5570
  case "structure":
3793
5571
  return `<polygon points="${diamondPoints(x, y, radius)}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
3794
- case "phenomenon":
5572
+ case "phenomenon": {
5573
+ const kind = String(object.properties.kind ?? "").toLowerCase().replace(/_/g, "-");
5574
+ if (options.outlineOnly) {
5575
+ if (kind === "black-hole" || kind === "nebula" || kind === "galaxy" || kind === "dwarf-galaxy") {
5576
+ return `<circle cx="${x}" cy="${y}" r="${radius}" fill="transparent" stroke="${palette.stroke}" stroke-width="1.4" />`;
5577
+ }
5578
+ return `<polygon points="${phenomenonPoints(x, y, radius)}" fill="transparent" stroke="${palette.stroke}" stroke-width="1.4" />`;
5579
+ }
5580
+ if (kind === "black-hole") {
5581
+ 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" />
5582
+ <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="2" />`;
5583
+ }
5584
+ if (kind === "galaxy") {
5585
+ return `<ellipse cx="${x}" cy="${y}" rx="${radius * 2.6}" ry="${radius}" fill="${palette.halo ?? "none"}" stroke="none" />
5586
+ <ellipse cx="${x}" cy="${y}" rx="${radius * 1.5}" ry="${radius * 0.42}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.2" />
5587
+ <circle cx="${x}" cy="${y}" r="${radius * 0.28}" fill="${palette.core ?? "#fff"}" stroke="none" />`;
5588
+ }
5589
+ if (kind === "dwarf-galaxy") {
5590
+ return `<ellipse cx="${x}" cy="${y}" rx="${radius * 1.6}" ry="${radius * 0.55}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1" />
5591
+ <circle cx="${x}" cy="${y}" r="${radius * 0.25}" fill="${palette.core ?? "#fff"}" stroke="none" />`;
5592
+ }
5593
+ if (kind === "nebula") {
5594
+ return `<circle cx="${x}" cy="${y}" r="${radius * 2.2}" fill="${palette.halo ?? "none"}" stroke="none" />
5595
+ <circle cx="${x}" cy="${y}" r="${radius}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1" />`;
5596
+ }
3795
5597
  return `<polygon points="${phenomenonPoints(x, y, radius)}" fill="${fill}" stroke="${palette.stroke}" stroke-width="1.4" />`;
5598
+ }
3796
5599
  }
3797
5600
  }
3798
5601
  function renderAtmosphere(sceneObject, palette) {
@@ -3861,7 +5664,8 @@
3861
5664
  }
3862
5665
  }
3863
5666
  function resolveObjectPalette(sceneObject, theme) {
3864
- const base = basePaletteForType(sceneObject.object.type, theme);
5667
+ const kind = String(sceneObject.object.properties.kind ?? "").toLowerCase().replace(/_/g, "-");
5668
+ const base = basePaletteForType(sceneObject.object.type, kind, theme);
3865
5669
  const customFill = sceneObject.fillColor && isColorLike(sceneObject.fillColor) ? sceneObject.fillColor : base.fill;
3866
5670
  const albedo = numericValue(sceneObject.object.properties.albedo);
3867
5671
  const temperature = numericValue(sceneObject.object.properties.temperature);
@@ -3877,7 +5681,7 @@
3877
5681
  tail: sceneObject.object.type === "comet" ? rgbaString(mixColors(fill, "#ffffff", 0.5) ?? fill, 0.72) : void 0
3878
5682
  };
3879
5683
  }
3880
- function basePaletteForType(type, theme) {
5684
+ function basePaletteForType(type, kind, theme) {
3881
5685
  switch (type) {
3882
5686
  case "star":
3883
5687
  return {
@@ -3899,8 +5703,26 @@
3899
5703
  case "structure":
3900
5704
  return { fill: theme.accentStrong, stroke: "#fff2ea" };
3901
5705
  case "phenomenon":
3902
- return { fill: "#78ffd7", stroke: "#e9fff7" };
5706
+ return kindPhenomenonPalette(kind);
5707
+ }
5708
+ }
5709
+ function kindPhenomenonPalette(kind) {
5710
+ if (kind === "galaxy") {
5711
+ return { fill: "rgba(165,125,255,0.55)", stroke: "rgba(210,185,255,0.75)", halo: "rgba(160,120,255,0.10)", core: "#ede0ff" };
5712
+ }
5713
+ if (kind === "dwarf-galaxy") {
5714
+ return { fill: "rgba(190,165,255,0.45)", stroke: "rgba(220,205,255,0.75)", core: "#ddd0ff" };
3903
5715
  }
5716
+ if (kind === "black-hole") {
5717
+ return { fill: "#040408", stroke: "#ff6a00", accentRing: "rgba(255,140,20,0.72)" };
5718
+ }
5719
+ if (kind === "nebula") {
5720
+ return { fill: "rgba(105,205,255,0.45)", stroke: "rgba(180,235,255,0.72)", halo: "rgba(100,200,255,0.08)" };
5721
+ }
5722
+ if (kind === "void") {
5723
+ return { fill: "#05080f", stroke: "rgba(130,160,255,0.4)" };
5724
+ }
5725
+ return { fill: "#78ffd7", stroke: "#e9fff7" };
3904
5726
  }
3905
5727
  function applyTemperatureAndAlbedo(baseColor, temperature, albedo, type) {
3906
5728
  let nextColor = baseColor;
@@ -4220,6 +6042,48 @@
4220
6042
  });
4221
6043
  }
4222
6044
  const placement = details.object.placement;
6045
+ if (details.object.groups?.length) {
6046
+ fields.set("groups", {
6047
+ key: "groups",
6048
+ label: "Groups",
6049
+ value: details.object.groups.join(", ")
6050
+ });
6051
+ }
6052
+ if (details.object.epoch) {
6053
+ fields.set("epoch", {
6054
+ key: "epoch",
6055
+ label: "Epoch",
6056
+ value: details.object.epoch
6057
+ });
6058
+ }
6059
+ if (details.object.referencePlane) {
6060
+ fields.set("referencePlane", {
6061
+ key: "referencePlane",
6062
+ label: "Reference Plane",
6063
+ value: details.object.referencePlane
6064
+ });
6065
+ }
6066
+ if (details.object.tidalLock !== void 0) {
6067
+ fields.set("tidalLock", {
6068
+ key: "tidalLock",
6069
+ label: "Tidal Lock",
6070
+ value: details.object.tidalLock ? "true" : "false"
6071
+ });
6072
+ }
6073
+ if (details.object.resonance) {
6074
+ fields.set("resonance", {
6075
+ key: "resonance",
6076
+ label: "Resonance",
6077
+ value: `${details.object.resonance.targetObjectId} ${details.object.resonance.ratio}`
6078
+ });
6079
+ }
6080
+ if (details.relatedEvents.length > 0) {
6081
+ fields.set("events", {
6082
+ key: "events",
6083
+ label: "Events",
6084
+ value: details.relatedEvents.map((event) => event.event.label || event.event.id).join(", ")
6085
+ });
6086
+ }
4223
6087
  if (placement?.mode === "at") {
4224
6088
  fields.set("placement", {
4225
6089
  key: "placement",
@@ -4389,6 +6253,9 @@
4389
6253
  touchPoints.set(event.pointerId, point);
4390
6254
  if (touchPoints.size === 2) {
4391
6255
  touchGesture = createTouchGestureState(scene, state, touchPoints);
6256
+ } else if (touchPoints.size === 1) {
6257
+ dragDistance = 0;
6258
+ suppressClick = false;
4392
6259
  }
4393
6260
  return;
4394
6261
  }
@@ -4406,7 +6273,9 @@
4406
6273
  if (!behavior.touch || !touchPoints.has(event.pointerId)) {
4407
6274
  return;
4408
6275
  }
4409
- touchPoints.set(event.pointerId, getViewportPointFromClient(event.clientX, event.clientY));
6276
+ const prevPoint = touchPoints.get(event.pointerId);
6277
+ const nextPoint2 = getViewportPointFromClient(event.clientX, event.clientY);
6278
+ touchPoints.set(event.pointerId, nextPoint2);
4410
6279
  if (touchPoints.size === 2) {
4411
6280
  if (!touchGesture) {
4412
6281
  touchGesture = createTouchGestureState(scene, state, touchPoints);
@@ -4417,6 +6286,14 @@
4417
6286
  const deltaX2 = current.center.x - touchGesture.startViewportCenter.x;
4418
6287
  const deltaY2 = current.center.y - touchGesture.startViewportCenter.y;
4419
6288
  updateState(panViewerState(zoomedState, deltaX2, deltaY2));
6289
+ } else if (touchPoints.size === 1) {
6290
+ const deltaX2 = nextPoint2.x - prevPoint.x;
6291
+ const deltaY2 = nextPoint2.y - prevPoint.y;
6292
+ dragDistance += Math.abs(deltaX2) + Math.abs(deltaY2);
6293
+ if (dragDistance > 2) {
6294
+ suppressClick = true;
6295
+ }
6296
+ updateState(panViewerState(state, deltaX2, deltaY2));
4420
6297
  }
4421
6298
  return;
4422
6299
  }
@@ -4621,6 +6498,12 @@
4621
6498
  emitAtlasStateChange();
4622
6499
  return true;
4623
6500
  },
6501
+ getActiveEventId() {
6502
+ return renderOptions.activeEventId ?? null;
6503
+ },
6504
+ setActiveEvent(id) {
6505
+ api.setRenderOptions({ activeEventId: id });
6506
+ },
4624
6507
  search(query, limit = 12) {
4625
6508
  return searchSceneObjects(scene, query, limit);
4626
6509
  },
@@ -4881,8 +6764,11 @@
4881
6764
  renderObject,
4882
6765
  label: scene.labels.find((label) => label.objectId === renderObject.objectId && !label.hidden) ?? null,
4883
6766
  group: scene.groups.find((group) => group.renderId === renderObject.groupId) ?? null,
6767
+ semanticGroups: scene.semanticGroups.filter((group) => renderObject.semanticGroupIds.includes(group.id)),
4884
6768
  orbit: scene.orbitVisuals.find((orbit) => orbit.objectId === renderObject.objectId && !orbit.hidden) ?? null,
4885
6769
  relatedOrbits: scene.orbitVisuals.filter((orbit) => !orbit.hidden && (orbit.objectId === renderObject.objectId || renderObject.ancestorIds.includes(orbit.objectId) || renderObject.childIds.includes(orbit.objectId))),
6770
+ relations: scene.relations.filter((relation) => !relation.hidden && (relation.fromObjectId === renderObject.objectId || relation.toObjectId === renderObject.objectId)),
6771
+ relatedEvents: scene.events.filter((event) => !event.hidden && (event.targetObjectId === renderObject.objectId || event.objectIds.includes(renderObject.objectId))),
4886
6772
  parent: getObjectById(renderObject.parentId),
4887
6773
  children: renderObject.childIds.map((childId) => getObjectById(childId)).filter(Boolean),
4888
6774
  ancestors: renderObject.ancestorIds.map((ancestorId) => getObjectById(ancestorId)).filter(Boolean),
@@ -5199,7 +7085,8 @@
5199
7085
  filter: renderOptions.filter ? { ...renderOptions.filter } : void 0,
5200
7086
  scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0,
5201
7087
  layers: renderOptions.layers ? { ...renderOptions.layers } : void 0,
5202
- theme: renderOptions.theme && typeof renderOptions.theme === "object" ? { ...renderOptions.theme } : renderOptions.theme
7088
+ theme: renderOptions.theme && typeof renderOptions.theme === "object" ? { ...renderOptions.theme } : renderOptions.theme,
7089
+ activeEventId: renderOptions.activeEventId ?? null
5203
7090
  };
5204
7091
  }
5205
7092
  function mergeRenderOptions(current, next) {
@@ -5219,7 +7106,7 @@
5219
7106
  };
5220
7107
  }
5221
7108
  function hasSceneAffectingRenderOptions(options) {
5222
- 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;
7109
+ 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;
5223
7110
  }
5224
7111
  function resolveSourceRenderOptions2(loaded, renderOptions) {
5225
7112
  const atlasDocument = loaded.atlasDocument ?? loaded.draftDocument;
@@ -5507,6 +7394,7 @@
5507
7394
  const controls = {
5508
7395
  search: options.controls?.search ?? true,
5509
7396
  typeFilter: options.controls?.typeFilter ?? true,
7397
+ groupFilter: options.controls?.groupFilter ?? true,
5510
7398
  viewpointSelect: options.controls?.viewpointSelect ?? true,
5511
7399
  inspector: options.controls?.inspector ?? true,
5512
7400
  bookmarks: options.controls?.bookmarks ?? true
@@ -5516,6 +7404,7 @@
5516
7404
  const toolbar = container.querySelector("[data-atlas-toolbar]");
5517
7405
  const searchInput = container.querySelector("[data-atlas-search]");
5518
7406
  const typeFilterSelect = container.querySelector("[data-atlas-type-filter]");
7407
+ const groupFilterSelect = container.querySelector("[data-atlas-group-filter]");
5519
7408
  const viewpointSelect = container.querySelector("[data-atlas-viewpoint]");
5520
7409
  const bookmarkButton = container.querySelector("[data-atlas-bookmark]");
5521
7410
  const bookmarkList = container.querySelector("[data-atlas-bookmarks]");
@@ -5528,6 +7417,7 @@
5528
7417
  const baseFilter = normalizeViewerFilter(options.initialFilter ?? null);
5529
7418
  let searchQuery = options.initialQuery?.trim() ?? baseFilter?.query ?? "";
5530
7419
  let objectTypeFilter = options.initialObjectType ?? (baseFilter?.objectTypes?.length === 1 ? baseFilter.objectTypes[0] : null);
7420
+ let groupFilter = baseFilter?.groupIds?.[0] ?? null;
5531
7421
  let bookmarks = [];
5532
7422
  let viewer;
5533
7423
  viewer = createInteractiveViewer(stage, {
@@ -5575,6 +7465,7 @@
5575
7465
  });
5576
7466
  applyCurrentFilter();
5577
7467
  populateViewpoints();
7468
+ populateGroups();
5578
7469
  syncControlsFromFilter(viewer.getFilter());
5579
7470
  renderBookmarks();
5580
7471
  updateSearchResults();
@@ -5587,6 +7478,10 @@
5587
7478
  objectTypeFilter = typeFilterSelect.value || null;
5588
7479
  applyCurrentFilter();
5589
7480
  });
7481
+ groupFilterSelect?.addEventListener("change", () => {
7482
+ groupFilter = groupFilterSelect.value || null;
7483
+ applyCurrentFilter();
7484
+ });
5590
7485
  viewpointSelect?.addEventListener("change", () => {
5591
7486
  const activeViewer = requireViewer();
5592
7487
  if (!viewpointSelect.value) {
@@ -5728,6 +7623,7 @@
5728
7623
  return api;
5729
7624
  function refreshAfterInputChange() {
5730
7625
  populateViewpoints();
7626
+ populateGroups();
5731
7627
  applyCurrentFilter();
5732
7628
  renderBookmarks();
5733
7629
  updateSearchResults();
@@ -5744,19 +7640,23 @@
5744
7640
  query: searchQuery || void 0,
5745
7641
  objectTypes: objectTypeFilter ? [objectTypeFilter] : void 0,
5746
7642
  tags: baseFilter?.tags,
5747
- groupIds: baseFilter?.groupIds,
7643
+ groupIds: groupFilter ? [groupFilter] : baseFilter?.groupIds,
5748
7644
  includeAncestors: baseFilter?.includeAncestors ?? true
5749
7645
  });
5750
7646
  }
5751
7647
  function syncControlsFromFilter(filter) {
5752
7648
  searchQuery = filter?.query?.trim() ?? "";
5753
7649
  objectTypeFilter = filter?.objectTypes?.length === 1 ? filter.objectTypes[0] : null;
7650
+ groupFilter = filter?.groupIds?.length === 1 ? filter.groupIds[0] : null;
5754
7651
  if (searchInput && document.activeElement !== searchInput) {
5755
7652
  searchInput.value = searchQuery;
5756
7653
  }
5757
7654
  if (typeFilterSelect) {
5758
7655
  typeFilterSelect.value = objectTypeFilter ?? "";
5759
7656
  }
7657
+ if (groupFilterSelect) {
7658
+ groupFilterSelect.value = groupFilter ?? "";
7659
+ }
5760
7660
  }
5761
7661
  function populateViewpoints() {
5762
7662
  if (!viewpointSelect) {
@@ -5770,6 +7670,17 @@
5770
7670
  ].join("");
5771
7671
  viewpointSelect.value = active;
5772
7672
  }
7673
+ function populateGroups() {
7674
+ if (!groupFilterSelect) {
7675
+ return;
7676
+ }
7677
+ const activeViewer = requireViewer();
7678
+ groupFilterSelect.innerHTML = [
7679
+ `<option value="">All groups</option>`,
7680
+ ...activeViewer.getScene().semanticGroups.map((group) => `<option value="${escapeHtml2(group.id)}">${escapeHtml2(group.label)}</option>`)
7681
+ ].join("");
7682
+ groupFilterSelect.value = groupFilter ?? "";
7683
+ }
5773
7684
  function syncViewpointControl() {
5774
7685
  if (!viewpointSelect) {
5775
7686
  return;
@@ -5803,6 +7714,9 @@
5803
7714
  projection: activeViewer.getScene().projection,
5804
7715
  renderPreset: activeViewer.getScene().renderPreset,
5805
7716
  groupCount: activeViewer.getScene().groups.length,
7717
+ semanticGroupCount: activeViewer.getScene().semanticGroups.length,
7718
+ relationCount: activeViewer.getScene().relations.length,
7719
+ eventCount: activeViewer.getScene().events.length,
5806
7720
  viewpointCount: activeViewer.getScene().viewpoints.length
5807
7721
  }
5808
7722
  };
@@ -5835,6 +7749,12 @@
5835
7749
  <option value="phenomenon">Phenomenon</option>
5836
7750
  </select>
5837
7751
  </label>` : "",
7752
+ controls.groupFilter ? `<label class="wo-atlas-field">
7753
+ <span>Group</span>
7754
+ <select data-atlas-group-filter>
7755
+ <option value="">All groups</option>
7756
+ </select>
7757
+ </label>` : "",
5838
7758
  controls.viewpointSelect ? `<label class="wo-atlas-field">
5839
7759
  <span>Viewpoint</span>
5840
7760
  <select data-atlas-viewpoint>