worldorbit 2.5.17 → 2.6.0

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.
@@ -998,7 +998,9 @@
998
998
  const height = frame.height;
999
999
  const padding = frame.padding;
1000
1000
  const layoutPreset = resolveLayoutPreset(document);
1001
- const projection = resolveProjection(document, options.projection);
1001
+ const schemaProjection = resolveProjection(document, options.projection);
1002
+ const camera = normalizeViewCamera(options.camera ?? null);
1003
+ const renderProjection = resolveRenderProjection(schemaProjection, camera);
1002
1004
  const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
1003
1005
  const spacingFactor = layoutPresetSpacing(layoutPreset);
1004
1006
  const systemId = document.system?.id ?? null;
@@ -1041,7 +1043,7 @@
1041
1043
  surfaceChildren,
1042
1044
  objectMap,
1043
1045
  spacingFactor,
1044
- projection,
1046
+ projection: renderProjection,
1045
1047
  scaleModel
1046
1048
  };
1047
1049
  const primaryRoot = rootObjects.find((object) => object.type === "star") ?? rootObjects[0] ?? null;
@@ -1053,7 +1055,7 @@
1053
1055
  const rootRingRadius = Math.min(width, height) * 0.28 * spacingFactor * scaleModel.orbitDistanceMultiplier;
1054
1056
  secondaryRoots.forEach((object, index) => {
1055
1057
  const angle = angleForIndex(index, secondaryRoots.length, -Math.PI / 2);
1056
- const offset = projectPolarOffset(angle, rootRingRadius, projection, 1);
1058
+ const offset = projectPolarOffset(angle, rootRingRadius, renderProjection, 1);
1057
1059
  placeObject(object, centerX + offset.x, centerY + offset.y, 0, positions, orbitDrafts, leaderDrafts, context);
1058
1060
  });
1059
1061
  }
@@ -1115,27 +1117,34 @@
1115
1117
  const layers = createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels);
1116
1118
  const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, scaleModel.labelMultiplier);
1117
1119
  const semanticGroups = createSceneSemanticGroups(document, objects);
1118
- const viewpoints = createSceneViewpoints(document, projection, frame.preset, relationships, objectMap);
1120
+ const viewpoints = createSceneViewpoints(document, schemaProjection, frame.preset, relationships, objectMap);
1119
1121
  const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, scaleModel.labelMultiplier);
1120
1122
  return {
1121
1123
  width,
1122
1124
  height,
1123
1125
  padding,
1124
1126
  renderPreset: frame.preset,
1125
- projection,
1127
+ projection: schemaProjection,
1128
+ renderProjection,
1129
+ camera,
1126
1130
  scaleModel,
1127
1131
  title: String(document.system?.title ?? document.system?.properties.title ?? document.system?.id ?? "WorldOrbit") || "WorldOrbit",
1128
- subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
1132
+ subtitle: buildSceneSubtitle(schemaProjection, renderProjection, layoutPreset, camera),
1129
1133
  systemId,
1130
- viewMode: projection,
1134
+ viewMode: schemaProjection,
1131
1135
  layoutPreset,
1132
1136
  metadata: {
1133
1137
  format: document.format,
1134
1138
  version: document.version,
1135
- view: projection,
1139
+ view: schemaProjection,
1140
+ renderProjection,
1136
1141
  scale: String(document.system?.properties.scale ?? layoutPreset),
1137
1142
  units: String(document.system?.properties.units ?? "mixed"),
1138
- preset: frame.preset ?? "custom"
1143
+ preset: frame.preset ?? "custom",
1144
+ ...camera?.azimuth !== null ? { "camera.azimuth": String(camera?.azimuth) } : {},
1145
+ ...camera?.elevation !== null ? { "camera.elevation": String(camera?.elevation) } : {},
1146
+ ...camera?.roll !== null ? { "camera.roll": String(camera?.roll) } : {},
1147
+ ...camera?.distance !== null ? { "camera.distance": String(camera?.distance) } : {}
1139
1148
  },
1140
1149
  contentBounds,
1141
1150
  layers,
@@ -1172,21 +1181,42 @@
1172
1181
  return cloned;
1173
1182
  }
1174
1183
  const objectMap = new Map(cloned.map((object) => [object.id, object]));
1184
+ const referencedIds = /* @__PURE__ */ new Set([
1185
+ ...activeEvent.targetObjectId ? [activeEvent.targetObjectId] : [],
1186
+ ...activeEvent.participantObjectIds,
1187
+ ...activeEvent.positions.map((pose) => pose.objectId)
1188
+ ]);
1189
+ for (const objectId of referencedIds) {
1190
+ const object = objectMap.get(objectId);
1191
+ if (!object) {
1192
+ continue;
1193
+ }
1194
+ if (activeEvent.epoch) {
1195
+ object.epoch = activeEvent.epoch;
1196
+ }
1197
+ if (activeEvent.referencePlane) {
1198
+ object.referencePlane = activeEvent.referencePlane;
1199
+ }
1200
+ }
1175
1201
  for (const pose of activeEvent.positions) {
1176
1202
  const object = objectMap.get(pose.objectId);
1177
1203
  if (!object) {
1178
1204
  continue;
1179
1205
  }
1180
- object.placement = pose.placement ? structuredClone(pose.placement) : null;
1206
+ if (pose.placement) {
1207
+ object.placement = structuredClone(pose.placement);
1208
+ }
1181
1209
  if (pose.inner) {
1182
1210
  object.properties.inner = { ...pose.inner };
1183
- } else {
1184
- delete object.properties.inner;
1185
1211
  }
1186
1212
  if (pose.outer) {
1187
1213
  object.properties.outer = { ...pose.outer };
1188
- } else {
1189
- delete object.properties.outer;
1214
+ }
1215
+ if (pose.epoch) {
1216
+ object.epoch = pose.epoch;
1217
+ }
1218
+ if (pose.referencePlane) {
1219
+ object.referencePlane = pose.referencePlane;
1190
1220
  }
1191
1221
  }
1192
1222
  return cloned;
@@ -1227,10 +1257,59 @@
1227
1257
  }
1228
1258
  }
1229
1259
  function resolveProjection(document, projection) {
1230
- if (projection === "topdown" || projection === "isometric") {
1260
+ if (projection === "topdown" || projection === "isometric" || projection === "orthographic" || projection === "perspective") {
1231
1261
  return projection;
1232
1262
  }
1233
- return String(document.system?.properties.view ?? "topdown").toLowerCase() === "isometric" ? "isometric" : "topdown";
1263
+ const documentView = String(document.system?.properties.view ?? "topdown").toLowerCase();
1264
+ return parseViewProjection(documentView) ?? "topdown";
1265
+ }
1266
+ function resolveRenderProjection(projection, camera) {
1267
+ switch (projection) {
1268
+ case "topdown":
1269
+ return "topdown";
1270
+ case "isometric":
1271
+ return "isometric";
1272
+ case "orthographic":
1273
+ return camera && (camera.azimuth !== null || camera.elevation !== null || camera.roll !== null) ? "isometric" : "topdown";
1274
+ case "perspective":
1275
+ return "isometric";
1276
+ }
1277
+ }
1278
+ function normalizeViewCamera(camera) {
1279
+ if (!camera) {
1280
+ return null;
1281
+ }
1282
+ const normalized = {
1283
+ azimuth: normalizeFiniteCameraValue(camera.azimuth),
1284
+ elevation: normalizeFiniteCameraValue(camera.elevation),
1285
+ roll: normalizeFiniteCameraValue(camera.roll),
1286
+ distance: normalizePositiveCameraDistance(camera.distance)
1287
+ };
1288
+ return normalized.azimuth !== null || normalized.elevation !== null || normalized.roll !== null || normalized.distance !== null ? normalized : null;
1289
+ }
1290
+ function normalizeFiniteCameraValue(value) {
1291
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1292
+ }
1293
+ function normalizePositiveCameraDistance(value) {
1294
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
1295
+ }
1296
+ function buildSceneSubtitle(projection, renderProjection, layoutPreset, camera) {
1297
+ const parts = [`${capitalizeLabel(projection)} view`, `${capitalizeLabel(layoutPreset)} layout`];
1298
+ if (projection !== renderProjection) {
1299
+ parts.push(`2D ${renderProjection} fallback`);
1300
+ }
1301
+ if (camera) {
1302
+ const cameraParts = [
1303
+ camera.azimuth !== null ? `az ${camera.azimuth}` : null,
1304
+ camera.elevation !== null ? `el ${camera.elevation}` : null,
1305
+ camera.roll !== null ? `roll ${camera.roll}` : null,
1306
+ camera.distance !== null ? `dist ${camera.distance}` : null
1307
+ ].filter(Boolean);
1308
+ if (cameraParts.length > 0) {
1309
+ parts.push(`camera ${cameraParts.join(" / ")}`);
1310
+ }
1311
+ }
1312
+ return parts.join(" - ");
1234
1313
  }
1235
1314
  function resolveScaleModel(layoutPreset, overrides) {
1236
1315
  const defaults = defaultScaleModel(layoutPreset);
@@ -1666,6 +1745,8 @@
1666
1745
  function createGeneratedOverviewViewpoint(document, projection, preset) {
1667
1746
  const title = document.system?.title ?? document.system?.properties.title;
1668
1747
  const label = title ? `${String(title)} Overview` : "Overview";
1748
+ const camera = normalizeViewCamera(null);
1749
+ const renderProjection = resolveRenderProjection(projection, camera);
1669
1750
  return {
1670
1751
  id: "overview",
1671
1752
  label,
@@ -1674,6 +1755,8 @@
1674
1755
  selectedObjectId: null,
1675
1756
  eventIds: [],
1676
1757
  projection,
1758
+ renderProjection,
1759
+ camera,
1677
1760
  preset,
1678
1761
  rotationDeg: 0,
1679
1762
  scale: null,
@@ -1723,6 +1806,30 @@
1723
1806
  case "angle":
1724
1807
  draft.rotationDeg = parseFiniteNumber(normalizedValue) ?? draft.rotationDeg ?? 0;
1725
1808
  return;
1809
+ case "camera.azimuth":
1810
+ draft.camera = {
1811
+ ...draft.camera ?? createEmptyViewCamera(),
1812
+ azimuth: parseFiniteNumber(normalizedValue)
1813
+ };
1814
+ return;
1815
+ case "camera.elevation":
1816
+ draft.camera = {
1817
+ ...draft.camera ?? createEmptyViewCamera(),
1818
+ elevation: parseFiniteNumber(normalizedValue)
1819
+ };
1820
+ return;
1821
+ case "camera.roll":
1822
+ draft.camera = {
1823
+ ...draft.camera ?? createEmptyViewCamera(),
1824
+ roll: parseFiniteNumber(normalizedValue)
1825
+ };
1826
+ return;
1827
+ case "camera.distance":
1828
+ draft.camera = {
1829
+ ...draft.camera ?? createEmptyViewCamera(),
1830
+ distance: parsePositiveNumber(normalizedValue)
1831
+ };
1832
+ return;
1726
1833
  case "zoom":
1727
1834
  case "scale":
1728
1835
  draft.scale = parsePositiveNumber(normalizedValue);
@@ -1762,6 +1869,9 @@
1762
1869
  const selectedObjectId = draft.select && objectMap.has(draft.select) ? draft.select : objectId;
1763
1870
  const filter = normalizeViewpointFilter(draft.filter);
1764
1871
  const label = draft.label?.trim() || humanizeIdentifier(draft.id);
1872
+ const resolvedProjection = draft.projection ?? projection;
1873
+ const camera = normalizeViewCamera(draft.camera ?? null);
1874
+ const renderProjection = resolveRenderProjection(resolvedProjection, camera);
1765
1875
  return {
1766
1876
  id: draft.id,
1767
1877
  label,
@@ -1769,7 +1879,9 @@
1769
1879
  objectId,
1770
1880
  selectedObjectId,
1771
1881
  eventIds: [...new Set(draft.eventIds ?? [])],
1772
- projection: draft.projection ?? projection,
1882
+ projection: resolvedProjection,
1883
+ renderProjection,
1884
+ camera,
1773
1885
  preset: draft.preset ?? preset,
1774
1886
  rotationDeg: draft.rotationDeg ?? 0,
1775
1887
  scale: draft.scale ?? null,
@@ -1786,6 +1898,14 @@
1786
1898
  groupIds: []
1787
1899
  };
1788
1900
  }
1901
+ function createEmptyViewCamera() {
1902
+ return {
1903
+ azimuth: null,
1904
+ elevation: null,
1905
+ roll: null,
1906
+ distance: null
1907
+ };
1908
+ }
1789
1909
  function normalizeViewpointFilter(filter) {
1790
1910
  if (!filter) {
1791
1911
  return null;
@@ -1799,7 +1919,18 @@
1799
1919
  return normalized.query || normalized.objectTypes.length > 0 || normalized.tags.length > 0 || normalized.groupIds.length > 0 ? normalized : null;
1800
1920
  }
1801
1921
  function parseViewProjection(value) {
1802
- return value.toLowerCase() === "isometric" ? "isometric" : value.toLowerCase() === "topdown" ? "topdown" : null;
1922
+ switch (value.toLowerCase()) {
1923
+ case "topdown":
1924
+ return "topdown";
1925
+ case "isometric":
1926
+ return "isometric";
1927
+ case "orthographic":
1928
+ return "orthographic";
1929
+ case "perspective":
1930
+ return "perspective";
1931
+ default:
1932
+ return null;
1933
+ }
1803
1934
  }
1804
1935
  function parseRenderPreset(value) {
1805
1936
  const normalized = value.toLowerCase();
@@ -1837,7 +1968,7 @@
1837
1968
  }
1838
1969
  function parseViewpointGroups(value, document, relationships, objectMap) {
1839
1970
  return splitListValue(value).map((entry) => {
1840
- if (document.schemaVersion === "2.1" || document.groups.some((group) => group.id === entry)) {
1971
+ if (document.schemaVersion === "2.1" || document.schemaVersion === "2.5" || document.groups.some((group) => group.id === entry)) {
1841
1972
  return entry;
1842
1973
  }
1843
1974
  if (entry.startsWith("wo-") && entry.endsWith("-group")) {
@@ -2630,8 +2761,8 @@
2630
2761
  }
2631
2762
  return {
2632
2763
  format: "worldorbit",
2633
- version: "2.0",
2634
- schemaVersion: "2.0",
2764
+ version: "2.5",
2765
+ schemaVersion: "2.5",
2635
2766
  sourceVersion: document.version,
2636
2767
  system,
2637
2768
  groups: structuredClone(document.groups ?? []),
@@ -2690,8 +2821,9 @@
2690
2821
  };
2691
2822
  }
2692
2823
  function createDraftDefaults(document, preset, projection) {
2824
+ const rawView = typeof document.system?.properties.view === "string" ? document.system.properties.view.toLowerCase() : null;
2693
2825
  return {
2694
- view: typeof document.system?.properties.view === "string" && document.system.properties.view.toLowerCase() === "topdown" ? "topdown" : projection,
2826
+ view: rawView === "topdown" || rawView === "isometric" || rawView === "orthographic" || rawView === "perspective" ? rawView : projection,
2695
2827
  scale: typeof document.system?.properties.scale === "string" ? document.system.properties.scale : null,
2696
2828
  units: typeof document.system?.properties.units === "string" ? document.system.properties.units : null,
2697
2829
  preset,
@@ -2798,6 +2930,7 @@
2798
2930
  preset: viewpoint.preset,
2799
2931
  zoom: viewpoint.scale,
2800
2932
  rotationDeg: viewpoint.rotationDeg,
2933
+ camera: viewpoint.camera ? { ...viewpoint.camera } : null,
2801
2934
  layers: { ...viewpoint.layers },
2802
2935
  filter: viewpoint.filter ? {
2803
2936
  query: viewpoint.filter.query,
@@ -2839,7 +2972,9 @@
2839
2972
  objectId: pose.objectId,
2840
2973
  placement: clonePlacement(pose.placement),
2841
2974
  inner: pose.inner ? { ...pose.inner } : void 0,
2842
- outer: pose.outer ? { ...pose.outer } : void 0
2975
+ outer: pose.outer ? { ...pose.outer } : void 0,
2976
+ epoch: pose.epoch ?? null,
2977
+ referencePlane: pose.referencePlane ?? null
2843
2978
  };
2844
2979
  }
2845
2980
  function clonePlacement(placement) {
@@ -2854,21 +2989,42 @@
2854
2989
  return;
2855
2990
  }
2856
2991
  const objectMap = new Map(objects.map((object) => [object.id, object]));
2992
+ const referencedIds = /* @__PURE__ */ new Set([
2993
+ ...event.targetObjectId ? [event.targetObjectId] : [],
2994
+ ...event.participantObjectIds,
2995
+ ...event.positions.map((pose) => pose.objectId)
2996
+ ]);
2997
+ for (const objectId of referencedIds) {
2998
+ const object = objectMap.get(objectId);
2999
+ if (!object) {
3000
+ continue;
3001
+ }
3002
+ if (event.epoch) {
3003
+ object.epoch = event.epoch;
3004
+ }
3005
+ if (event.referencePlane) {
3006
+ object.referencePlane = event.referencePlane;
3007
+ }
3008
+ }
2857
3009
  for (const pose of event.positions) {
2858
3010
  const object = objectMap.get(pose.objectId);
2859
3011
  if (!object) {
2860
3012
  continue;
2861
3013
  }
2862
- object.placement = clonePlacement(pose.placement);
3014
+ if (pose.placement) {
3015
+ object.placement = clonePlacement(pose.placement);
3016
+ }
2863
3017
  if (pose.inner) {
2864
3018
  object.properties.inner = { ...pose.inner };
2865
- } else {
2866
- delete object.properties.inner;
2867
3019
  }
2868
3020
  if (pose.outer) {
2869
3021
  object.properties.outer = { ...pose.outer };
2870
- } else {
2871
- delete object.properties.outer;
3022
+ }
3023
+ if (pose.epoch) {
3024
+ object.epoch = pose.epoch;
3025
+ }
3026
+ if (pose.referencePlane) {
3027
+ object.referencePlane = pose.referencePlane;
2872
3028
  }
2873
3029
  }
2874
3030
  }
@@ -2953,6 +3109,18 @@
2953
3109
  if (viewpoint.rotationDeg !== 0) {
2954
3110
  info2[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2955
3111
  }
3112
+ if (viewpoint.camera?.azimuth !== null) {
3113
+ info2[`${prefix}.camera.azimuth`] = String(viewpoint.camera?.azimuth);
3114
+ }
3115
+ if (viewpoint.camera?.elevation !== null) {
3116
+ info2[`${prefix}.camera.elevation`] = String(viewpoint.camera?.elevation);
3117
+ }
3118
+ if (viewpoint.camera?.roll !== null) {
3119
+ info2[`${prefix}.camera.roll`] = String(viewpoint.camera?.roll);
3120
+ }
3121
+ if (viewpoint.camera?.distance !== null) {
3122
+ info2[`${prefix}.camera.distance`] = String(viewpoint.camera?.distance);
3123
+ }
2956
3124
  const serializedLayers = serializeViewpointLayers(viewpoint.layers);
2957
3125
  if (serializedLayers) {
2958
3126
  info2[`${prefix}.layers`] = serializedLayers;
@@ -3049,26 +3217,26 @@
3049
3217
  ];
3050
3218
  function formatDocument(document, options = {}) {
3051
3219
  const schema = options.schema ?? "auto";
3052
- const useDraft = schema === "2.0" || schema === "2.1" || schema === "2.0-draft" || document.version === "2.0" || document.version === "2.1" || document.version === "2.0-draft";
3220
+ const useDraft = schema === "2.0" || schema === "2.1" || schema === "2.5" || schema === "2.0-draft" || document.version === "2.0" || document.version === "2.1" || document.version === "2.5" || document.version === "2.0-draft";
3053
3221
  if (useDraft) {
3054
3222
  if (schema === "2.0-draft") {
3055
- const legacyDraftDocument = document.version === "2.0-draft" ? document : document.version === "2.0" || document.version === "2.1" ? {
3223
+ const legacyDraftDocument = document.version === "2.0-draft" ? document : document.version === "2.0" || document.version === "2.1" || document.version === "2.5" ? {
3056
3224
  ...document,
3057
3225
  version: "2.0-draft",
3058
3226
  schemaVersion: "2.0-draft"
3059
3227
  } : upgradeDocumentToDraftV2(document);
3060
3228
  return formatDraftDocument(legacyDraftDocument);
3061
3229
  }
3062
- const atlasDocument = document.version === "2.0" || document.version === "2.1" ? document : document.version === "2.0-draft" ? {
3230
+ const atlasDocument = document.version === "2.0" || document.version === "2.1" || document.version === "2.5" ? document : document.version === "2.0-draft" ? {
3063
3231
  ...document,
3064
3232
  version: "2.0",
3065
3233
  schemaVersion: "2.0"
3066
3234
  } : upgradeDocumentToV2(document);
3067
- if (schema === "2.1" && atlasDocument.version !== "2.1") {
3235
+ if ((schema === "2.0" || schema === "2.1" || schema === "2.5") && atlasDocument.version !== schema) {
3068
3236
  return formatAtlasDocument({
3069
3237
  ...atlasDocument,
3070
- version: "2.1",
3071
- schemaVersion: "2.1"
3238
+ version: schema,
3239
+ schemaVersion: schema
3072
3240
  });
3073
3241
  }
3074
3242
  return formatAtlasDocument(atlasDocument);
@@ -3345,6 +3513,21 @@
3345
3513
  if (viewpoint.rotationDeg !== 0) {
3346
3514
  lines.push(` rotation ${viewpoint.rotationDeg}`);
3347
3515
  }
3516
+ if (viewpoint.camera && hasCameraValues(viewpoint.camera)) {
3517
+ lines.push(" camera");
3518
+ if (viewpoint.camera.azimuth !== null) {
3519
+ lines.push(` azimuth ${viewpoint.camera.azimuth}`);
3520
+ }
3521
+ if (viewpoint.camera.elevation !== null) {
3522
+ lines.push(` elevation ${viewpoint.camera.elevation}`);
3523
+ }
3524
+ if (viewpoint.camera.roll !== null) {
3525
+ lines.push(` roll ${viewpoint.camera.roll}`);
3526
+ }
3527
+ if (viewpoint.camera.distance !== null) {
3528
+ lines.push(` distance ${viewpoint.camera.distance}`);
3529
+ }
3530
+ }
3348
3531
  const layerTokens = formatDraftLayers(viewpoint.layers);
3349
3532
  if (layerTokens.length > 0) {
3350
3533
  lines.push(` layers ${layerTokens.join(" ")}`);
@@ -3444,6 +3627,12 @@
3444
3627
  if (event.visibility) {
3445
3628
  lines.push(` visibility ${quoteIfNeeded(event.visibility)}`);
3446
3629
  }
3630
+ if (event.epoch) {
3631
+ lines.push(` epoch ${quoteIfNeeded(event.epoch)}`);
3632
+ }
3633
+ if (event.referencePlane) {
3634
+ lines.push(` referencePlane ${quoteIfNeeded(event.referencePlane)}`);
3635
+ }
3447
3636
  if (event.tags.length > 0) {
3448
3637
  lines.push(` tags ${event.tags.map(quoteIfNeeded).join(" ")}`);
3449
3638
  }
@@ -3468,10 +3657,15 @@
3468
3657
  function formatEventPoseFields(pose) {
3469
3658
  return [
3470
3659
  ...formatPlacement(pose.placement),
3660
+ ...pose.epoch ? [`epoch ${quoteIfNeeded(pose.epoch)}`] : [],
3661
+ ...pose.referencePlane ? [`referencePlane ${quoteIfNeeded(pose.referencePlane)}`] : [],
3471
3662
  ...formatOptionalUnit("inner", pose.inner),
3472
3663
  ...formatOptionalUnit("outer", pose.outer)
3473
3664
  ];
3474
3665
  }
3666
+ function hasCameraValues(camera) {
3667
+ return camera.azimuth !== null || camera.elevation !== null || camera.roll !== null || camera.distance !== null;
3668
+ }
3475
3669
  function formatValue(value) {
3476
3670
  if (Array.isArray(value)) {
3477
3671
  return value.map((item) => quoteIfNeeded(item)).join(" ");
@@ -3765,13 +3959,13 @@
3765
3959
  validateRelation(relation, objectMap, diagnostics);
3766
3960
  }
3767
3961
  for (const viewpoint of document.system?.viewpoints ?? []) {
3768
- validateViewpoint(viewpoint.filter, viewpoint.events ?? [], groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpoint.id);
3962
+ validateViewpoint(viewpoint, groupIds, eventIds, sourceSchemaVersion, diagnostics, objectMap);
3769
3963
  }
3770
3964
  for (const object of document.objects) {
3771
3965
  validateObject(object, document.system, objectMap, groupIds, diagnostics);
3772
3966
  }
3773
3967
  for (const event of document.events) {
3774
- validateEvent(event, objectMap, diagnostics);
3968
+ validateEvent(event, document.system, objectMap, diagnostics);
3775
3969
  }
3776
3970
  return diagnostics;
3777
3971
  }
@@ -3790,21 +3984,24 @@
3790
3984
  diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
3791
3985
  }
3792
3986
  }
3793
- function validateViewpoint(filter, eventRefs, groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpointId) {
3794
- if (sourceSchemaVersion === "2.1") {
3987
+ function validateViewpoint(viewpoint, groupIds, eventIds, sourceSchemaVersion, diagnostics, objectMap) {
3988
+ const filter = viewpoint.filter;
3989
+ if (sourceSchemaVersion === "2.1" || sourceSchemaVersion === "2.5") {
3795
3990
  if (filter) {
3796
3991
  for (const groupId of filter.groupIds) {
3797
3992
  if (!groupIds.has(groupId)) {
3798
- diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.groups`));
3993
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpoint.id}".`, void 0, `viewpoint.${viewpoint.id}.groups`));
3799
3994
  }
3800
3995
  }
3801
3996
  }
3802
- for (const eventId of eventRefs) {
3997
+ for (const eventId of viewpoint.events ?? []) {
3803
3998
  if (!eventIds.has(eventId)) {
3804
- diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.events`));
3999
+ diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpoint.id}".`, void 0, `viewpoint.${viewpoint.id}.events`));
3805
4000
  }
3806
4001
  }
3807
4002
  }
4003
+ validateProjection(viewpoint.projection, diagnostics, `viewpoint.${viewpoint.id}.projection`, viewpoint.id);
4004
+ validateCamera(viewpoint.camera, viewpoint.projection, viewpoint.rotationDeg, diagnostics, viewpoint.id, viewpoint.focusObjectId, viewpoint.selectedObjectId, filter, objectMap);
3808
4005
  }
3809
4006
  function validateObject(object, system, objectMap, groupIds, diagnostics) {
3810
4007
  const placement = object.placement;
@@ -3817,6 +4014,12 @@
3817
4014
  }
3818
4015
  }
3819
4016
  }
4017
+ if (typeof object.epoch === "string" && !object.epoch.trim()) {
4018
+ diagnostics.push(warn("validate.epoch.empty", `Object "${object.id}" defines an empty epoch string.`, object.id, "epoch"));
4019
+ }
4020
+ if (typeof object.referencePlane === "string" && !object.referencePlane.trim()) {
4021
+ diagnostics.push(warn("validate.referencePlane.empty", `Object "${object.id}" defines an empty reference plane string.`, object.id, "referencePlane"));
4022
+ }
3820
4023
  if (orbitPlacement) {
3821
4024
  if (!objectMap.has(orbitPlacement.target)) {
3822
4025
  diagnostics.push(error("validate.orbit.target.unknown", `Unknown placement target "${orbitPlacement.target}" on "${object.id}".`, object.id, "orbit"));
@@ -3888,12 +4091,18 @@
3888
4091
  }
3889
4092
  }
3890
4093
  }
3891
- function validateEvent(event, objectMap, diagnostics) {
4094
+ function validateEvent(event, system, objectMap, diagnostics) {
3892
4095
  const fieldPrefix = `event.${event.id}`;
3893
4096
  const referencedIds = /* @__PURE__ */ new Set();
3894
4097
  if (!event.kind.trim()) {
3895
4098
  diagnostics.push(error("validate.event.kind.required", `Event "${event.id}" is missing a "kind" value.`, void 0, `${fieldPrefix}.kind`));
3896
4099
  }
4100
+ if (typeof event.epoch === "string" && !event.epoch.trim()) {
4101
+ diagnostics.push(warn("validate.event.epoch.empty", `Event "${event.id}" defines an empty epoch string.`, void 0, `${fieldPrefix}.epoch`));
4102
+ }
4103
+ if (typeof event.referencePlane === "string" && !event.referencePlane.trim()) {
4104
+ diagnostics.push(warn("validate.event.referencePlane.empty", `Event "${event.id}" defines an empty reference plane string.`, void 0, `${fieldPrefix}.referencePlane`));
4105
+ }
3897
4106
  if (!event.targetObjectId && event.participantObjectIds.length === 0) {
3898
4107
  diagnostics.push(error("validate.event.references.required", `Event "${event.id}" must define a "target" or at least one participant.`, void 0, `${fieldPrefix}.participants`));
3899
4108
  }
@@ -3940,10 +4149,14 @@
3940
4149
  if (!referencedIds.has(pose.objectId)) {
3941
4150
  diagnostics.push(warn("validate.event.pose.unreferenced", `Event pose "${pose.objectId}" on "${event.id}" is not listed in target/participants.`, void 0, poseFieldPrefix));
3942
4151
  }
3943
- validateEventPose(pose, object, objectMap, diagnostics, poseFieldPrefix, event.id);
4152
+ validateEventPose(pose, object, event, system, objectMap, diagnostics, poseFieldPrefix, event.id);
4153
+ }
4154
+ const missingPoseIds = [...referencedIds].filter((objectId) => !poseIds.has(objectId));
4155
+ if (event.positions.length > 0 && missingPoseIds.length > 0) {
4156
+ diagnostics.push(warn("validate.event.positions.partial", `Event "${event.id}" leaves ${missingPoseIds.length} referenced object(s) on their base placement.`, void 0, `${fieldPrefix}.positions`));
3944
4157
  }
3945
4158
  }
3946
- function validateEventPose(pose, object, objectMap, diagnostics, fieldPrefix, eventId) {
4159
+ function validateEventPose(pose, object, event, system, objectMap, diagnostics, fieldPrefix, eventId) {
3947
4160
  const placement = pose.placement;
3948
4161
  if (!placement) {
3949
4162
  diagnostics.push(error("validate.event.pose.placement.required", `Event "${eventId}" pose "${pose.objectId}" is missing a placement mode.`, void 0, fieldPrefix));
@@ -3956,6 +4169,15 @@
3956
4169
  if (placement.distance && placement.semiMajor) {
3957
4170
  diagnostics.push(error("validate.event.pose.orbit.distanceConflict", `Event "${eventId}" pose "${pose.objectId}" cannot declare both "distance" and "semiMajor".`, void 0, `${fieldPrefix}.distance`));
3958
4171
  }
4172
+ if (placement.phase && !resolveEffectiveEpoch(system, object, event, pose)) {
4173
+ diagnostics.push(warn("validate.event.pose.phase.epochMissing", `Event "${eventId}" pose "${pose.objectId}" sets "phase" without an effective epoch.`, void 0, `${fieldPrefix}.phase`));
4174
+ }
4175
+ if (placement.inclination && !resolveEffectiveReferencePlane(system, object, event, pose)) {
4176
+ diagnostics.push(warn("validate.event.pose.inclination.referencePlaneMissing", `Event "${eventId}" pose "${pose.objectId}" sets "inclination" without an effective reference plane.`, void 0, `${fieldPrefix}.inclination`));
4177
+ }
4178
+ if (placement.period && !massInSolar(objectMap.get(placement.target)?.properties.mass)) {
4179
+ diagnostics.push(warn("validate.event.pose.period.massMissing", `Event "${eventId}" pose "${pose.objectId}" sets "period" but its central mass cannot be derived.`, void 0, `${fieldPrefix}.period`));
4180
+ }
3959
4181
  return;
3960
4182
  }
3961
4183
  if (placement.mode === "surface") {
@@ -4090,6 +4312,52 @@
4090
4312
  return null;
4091
4313
  }
4092
4314
  }
4315
+ function validateProjection(projection, diagnostics, field, viewpointId) {
4316
+ if (projection !== "topdown" && projection !== "isometric" && projection !== "orthographic" && projection !== "perspective") {
4317
+ diagnostics.push(error("validate.viewpoint.projection.invalid", `Unknown projection "${String(projection)}" in viewpoint "${viewpointId}".`, void 0, field));
4318
+ }
4319
+ }
4320
+ function validateCamera(camera, projection, rotationDeg, diagnostics, viewpointId, focusObjectId, selectedObjectId, filter, objectMap) {
4321
+ if (!camera) {
4322
+ return;
4323
+ }
4324
+ const prefix = `viewpoint.${viewpointId}.camera`;
4325
+ for (const [key, value] of [
4326
+ ["azimuth", camera.azimuth],
4327
+ ["elevation", camera.elevation],
4328
+ ["roll", camera.roll],
4329
+ ["distance", camera.distance]
4330
+ ]) {
4331
+ if (value !== null && (!Number.isFinite(value) || key === "distance" && value <= 0)) {
4332
+ diagnostics.push(error("validate.viewpoint.camera.invalid", `Invalid camera ${key} "${String(value)}" in viewpoint "${viewpointId}".`, void 0, `${prefix}.${key}`));
4333
+ }
4334
+ }
4335
+ if (camera.distance !== null && projection !== "perspective") {
4336
+ diagnostics.push(warn("validate.viewpoint.camera.distance.partialEffect", `Camera "distance" only has a semantic effect in perspective viewpoints; "${viewpointId}" uses "${projection}".`, void 0, `${prefix}.distance`));
4337
+ }
4338
+ if (projection === "topdown" && (camera.elevation !== null || camera.roll !== null)) {
4339
+ diagnostics.push(warn("validate.viewpoint.camera.topdownPartial", `Camera elevation/roll on topdown viewpoint "${viewpointId}" are currently stored for future 3D use and only partially affect 2D rendering.`, void 0, prefix));
4340
+ }
4341
+ if (projection === "isometric" && camera.elevation !== null) {
4342
+ diagnostics.push(info("validate.viewpoint.camera.isometricStored", `Camera elevation on isometric viewpoint "${viewpointId}" is preserved semantically for future 3D rendering.`, void 0, `${prefix}.elevation`));
4343
+ }
4344
+ if (camera.azimuth !== null && camera.azimuth !== 0 && rotationDeg !== 0) {
4345
+ diagnostics.push(warn("validate.viewpoint.rotation.cameraOverlap", `Viewpoint "${viewpointId}" uses camera.azimuth; keep "rotation" only for 2D screen rotation to avoid ambiguity.`, void 0, `${prefix}.azimuth`));
4346
+ }
4347
+ const hasAnchor = focusObjectId !== null && objectMap.has(focusObjectId) || selectedObjectId !== null && objectMap.has(selectedObjectId) || !!filter;
4348
+ if (!hasAnchor) {
4349
+ diagnostics.push(info("validate.viewpoint.camera.anchorMissing", `Viewpoint "${viewpointId}" stores camera settings without a focus object, selection, or filter anchor.`, void 0, prefix));
4350
+ }
4351
+ }
4352
+ function resolveEffectiveEpoch(system, object, event, pose) {
4353
+ return normalizeOptionalContextString(pose?.epoch) ?? normalizeOptionalContextString(event?.epoch) ?? normalizeOptionalContextString(object.epoch) ?? normalizeOptionalContextString(system?.epoch) ?? null;
4354
+ }
4355
+ function resolveEffectiveReferencePlane(system, object, event, pose) {
4356
+ return normalizeOptionalContextString(pose?.referencePlane) ?? normalizeOptionalContextString(event?.referencePlane) ?? normalizeOptionalContextString(object.referencePlane) ?? normalizeOptionalContextString(system?.referencePlane) ?? null;
4357
+ }
4358
+ function normalizeOptionalContextString(value) {
4359
+ return typeof value === "string" && value.trim() ? value.trim() : null;
4360
+ }
4093
4361
  function toleranceForField(object, field) {
4094
4362
  const tolerance = object.tolerances?.find((entry) => entry.field === field)?.value;
4095
4363
  if (typeof tolerance === "number") {
@@ -4198,7 +4466,9 @@
4198
4466
  "surface",
4199
4467
  "free",
4200
4468
  "inner",
4201
- "outer"
4469
+ "outer",
4470
+ "epoch",
4471
+ "referencePlane"
4202
4472
  ]);
4203
4473
  function parseWorldOrbitAtlas(source) {
4204
4474
  return parseAtlasSource(source);
@@ -4243,7 +4513,7 @@
4243
4513
  if (!sawSchemaHeader) {
4244
4514
  sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
4245
4515
  sawSchemaHeader = true;
4246
- if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
4516
+ if (prepared.comments.length > 0 && isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
4247
4517
  diagnostics.push({
4248
4518
  code: "parse.schema21.commentCompatibility",
4249
4519
  severity: "warning",
@@ -4313,11 +4583,11 @@
4313
4583
  return document;
4314
4584
  }
4315
4585
  function assertDraftSchemaHeader(tokens, line) {
4316
- if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
4317
- throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
4586
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1", "2.5"].includes(tokens[1].value.toLowerCase())) {
4587
+ throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", "schema 2.5", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
4318
4588
  }
4319
4589
  const version = tokens[1].value.toLowerCase();
4320
- return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
4590
+ return version === "2.5" ? "2.5" : version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
4321
4591
  }
4322
4592
  function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, flags) {
4323
4593
  const keyword = tokens[0]?.value.toLowerCase();
@@ -4337,6 +4607,8 @@
4337
4607
  return {
4338
4608
  kind: "defaults",
4339
4609
  system,
4610
+ sourceSchemaVersion,
4611
+ diagnostics,
4340
4612
  seenFields: /* @__PURE__ */ new Set()
4341
4613
  };
4342
4614
  case "atlas":
@@ -4429,6 +4701,7 @@
4429
4701
  preset: system.defaults.preset,
4430
4702
  zoom: null,
4431
4703
  rotationDeg: 0,
4704
+ camera: null,
4432
4705
  layers: {},
4433
4706
  filter: null
4434
4707
  };
@@ -4442,7 +4715,10 @@
4442
4715
  seenFields: /* @__PURE__ */ new Set(),
4443
4716
  inFilter: false,
4444
4717
  filterIndent: null,
4445
- seenFilterFields: /* @__PURE__ */ new Set()
4718
+ seenFilterFields: /* @__PURE__ */ new Set(),
4719
+ inCamera: false,
4720
+ cameraIndent: null,
4721
+ seenCameraFields: /* @__PURE__ */ new Set()
4446
4722
  };
4447
4723
  }
4448
4724
  function startAnnotationSection(tokens, line, system, annotationIds) {
@@ -4549,6 +4825,8 @@
4549
4825
  participantObjectIds: [],
4550
4826
  timing: null,
4551
4827
  visibility: null,
4828
+ epoch: null,
4829
+ referencePlane: null,
4552
4830
  tags: [],
4553
4831
  color: null,
4554
4832
  hidden: false,
@@ -4673,6 +4951,12 @@
4673
4951
  const value = joinFieldValue(tokens, line);
4674
4952
  switch (key) {
4675
4953
  case "view":
4954
+ if (isSchema25Projection(value)) {
4955
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "defaults.view", {
4956
+ line,
4957
+ column: tokens[0].column
4958
+ });
4959
+ }
4676
4960
  section.system.defaults.view = parseProjectionValue(value, line, tokens[0].column);
4677
4961
  return;
4678
4962
  case "scale":
@@ -4712,14 +4996,36 @@
4712
4996
  throw new WorldOrbitError(`Unknown atlas field "${tokens[0].value}"`, line, tokens[0].column);
4713
4997
  }
4714
4998
  function applyViewpointField2(section, indent, tokens, line) {
4999
+ if (section.inCamera && indent <= (section.cameraIndent ?? 0)) {
5000
+ section.inCamera = false;
5001
+ section.cameraIndent = null;
5002
+ }
4715
5003
  if (section.inFilter && indent <= (section.filterIndent ?? 0)) {
4716
5004
  section.inFilter = false;
4717
5005
  section.filterIndent = null;
4718
5006
  }
5007
+ if (section.inCamera) {
5008
+ applyViewpointCameraField(section, tokens, line);
5009
+ return;
5010
+ }
4719
5011
  if (section.inFilter) {
4720
5012
  applyViewpointFilterField(section, tokens, line);
4721
5013
  return;
4722
5014
  }
5015
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "camera") {
5016
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.camera", {
5017
+ line,
5018
+ column: tokens[0].column
5019
+ });
5020
+ if (section.seenFields.has("camera")) {
5021
+ throw new WorldOrbitError('Duplicate viewpoint field "camera"', line, tokens[0].column);
5022
+ }
5023
+ section.seenFields.add("camera");
5024
+ section.inCamera = true;
5025
+ section.cameraIndent = indent;
5026
+ section.viewpoint.camera = section.viewpoint.camera ?? createEmptyViewCamera2();
5027
+ return;
5028
+ }
4723
5029
  if (tokens.length === 1 && tokens[0].value.toLowerCase() === "filter") {
4724
5030
  if (section.seenFields.has("filter")) {
4725
5031
  throw new WorldOrbitError('Duplicate viewpoint field "filter"', line, tokens[0].column);
@@ -4745,6 +5051,12 @@
4745
5051
  section.viewpoint.selectedObjectId = value;
4746
5052
  return;
4747
5053
  case "projection":
5054
+ if (isSchema25Projection(value)) {
5055
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "projection", {
5056
+ line,
5057
+ column: tokens[0].column
5058
+ });
5059
+ }
4748
5060
  section.viewpoint.projection = parseProjectionValue(value, line, tokens[0].column);
4749
5061
  return;
4750
5062
  case "preset":
@@ -4756,6 +5068,13 @@
4756
5068
  case "rotation":
4757
5069
  section.viewpoint.rotationDeg = parseFiniteNumber2(value, line, tokens[0].column, "rotation");
4758
5070
  return;
5071
+ case "camera":
5072
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.camera", {
5073
+ line,
5074
+ column: tokens[0].column
5075
+ });
5076
+ section.viewpoint.camera = parseInlineViewCamera(tokens.slice(1), line, section.viewpoint.camera);
5077
+ return;
4759
5078
  case "layers":
4760
5079
  section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line, section.sourceSchemaVersion, section.diagnostics);
4761
5080
  return;
@@ -4770,6 +5089,28 @@
4770
5089
  throw new WorldOrbitError(`Unknown viewpoint field "${tokens[0].value}"`, line, tokens[0].column);
4771
5090
  }
4772
5091
  }
5092
+ function applyViewpointCameraField(section, tokens, line) {
5093
+ const key = requireUniqueField(tokens, section.seenCameraFields, line);
5094
+ const value = joinFieldValue(tokens, line);
5095
+ const camera = section.viewpoint.camera ?? createEmptyViewCamera2();
5096
+ switch (key) {
5097
+ case "azimuth":
5098
+ camera.azimuth = parseFiniteNumber2(value, line, tokens[0].column, "camera.azimuth");
5099
+ break;
5100
+ case "elevation":
5101
+ camera.elevation = parseFiniteNumber2(value, line, tokens[0].column, "camera.elevation");
5102
+ break;
5103
+ case "roll":
5104
+ camera.roll = parseFiniteNumber2(value, line, tokens[0].column, "camera.roll");
5105
+ break;
5106
+ case "distance":
5107
+ camera.distance = parsePositiveNumber2(value, line, tokens[0].column, "camera.distance");
5108
+ break;
5109
+ default:
5110
+ throw new WorldOrbitError(`Unknown viewpoint camera field "${tokens[0].value}"`, line, tokens[0].column);
5111
+ }
5112
+ section.viewpoint.camera = camera;
5113
+ }
4773
5114
  function applyViewpointFilterField(section, tokens, line) {
4774
5115
  const key = requireUniqueField(tokens, section.seenFilterFields, line);
4775
5116
  const filter = section.viewpoint.filter ?? createEmptyViewpointFilter2();
@@ -4880,6 +5221,12 @@
4880
5221
  section.positionsIndent = null;
4881
5222
  }
4882
5223
  if (section.activePose) {
5224
+ if (tokens[0]?.value === "epoch" || tokens[0]?.value === "referencePlane") {
5225
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, `pose.${tokens[0].value}`, {
5226
+ line,
5227
+ column: tokens[0]?.column ?? 1
5228
+ });
5229
+ }
4883
5230
  section.activePose.fields.push(parseEventPoseField(tokens, line, section.activePoseSeenFields));
4884
5231
  return;
4885
5232
  }
@@ -4934,6 +5281,20 @@
4934
5281
  case "visibility":
4935
5282
  section.event.visibility = joinFieldValue(tokens, line);
4936
5283
  return;
5284
+ case "epoch":
5285
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "event.epoch", {
5286
+ line,
5287
+ column: tokens[0].column
5288
+ });
5289
+ section.event.epoch = joinFieldValue(tokens, line);
5290
+ return;
5291
+ case "referenceplane":
5292
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "event.referencePlane", {
5293
+ line,
5294
+ column: tokens[0].column
5295
+ });
5296
+ section.event.referencePlane = joinFieldValue(tokens, line);
5297
+ return;
4937
5298
  case "tags":
4938
5299
  section.event.tags = parseTokenList(tokens.slice(1), line, "tags");
4939
5300
  return;
@@ -5061,11 +5422,15 @@
5061
5422
  }
5062
5423
  function parseProjectionValue(value, line, column) {
5063
5424
  const normalized = value.toLowerCase();
5064
- if (normalized !== "topdown" && normalized !== "isometric") {
5425
+ if (normalized !== "topdown" && normalized !== "isometric" && normalized !== "orthographic" && normalized !== "perspective") {
5065
5426
  throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
5066
5427
  }
5067
5428
  return normalized;
5068
5429
  }
5430
+ function isSchema25Projection(value) {
5431
+ const normalized = value.toLowerCase();
5432
+ return normalized === "orthographic" || normalized === "perspective";
5433
+ }
5069
5434
  function parsePresetValue(value, line, column) {
5070
5435
  const normalized = value.toLowerCase();
5071
5436
  if (normalized === "diagram" || normalized === "presentation" || normalized === "atlas-card" || normalized === "markdown") {
@@ -5095,6 +5460,48 @@
5095
5460
  groupIds: []
5096
5461
  };
5097
5462
  }
5463
+ function createEmptyViewCamera2() {
5464
+ return {
5465
+ azimuth: null,
5466
+ elevation: null,
5467
+ roll: null,
5468
+ distance: null
5469
+ };
5470
+ }
5471
+ function parseInlineViewCamera(tokens, line, current) {
5472
+ if (tokens.length === 0 || tokens.length % 2 !== 0) {
5473
+ throw new WorldOrbitError('Field "camera" expects "<field> <value>" pairs', line, tokens[0]?.column ?? 1);
5474
+ }
5475
+ const camera = current ? { ...current } : createEmptyViewCamera2();
5476
+ const seen = /* @__PURE__ */ new Set();
5477
+ for (let index = 0; index < tokens.length; index += 2) {
5478
+ const fieldToken = tokens[index];
5479
+ const valueToken = tokens[index + 1];
5480
+ const key = fieldToken.value.toLowerCase();
5481
+ if (seen.has(key)) {
5482
+ throw new WorldOrbitError(`Duplicate viewpoint camera field "${fieldToken.value}"`, line, fieldToken.column);
5483
+ }
5484
+ seen.add(key);
5485
+ const value = valueToken.value;
5486
+ switch (key) {
5487
+ case "azimuth":
5488
+ camera.azimuth = parseFiniteNumber2(value, line, fieldToken.column, "camera.azimuth");
5489
+ break;
5490
+ case "elevation":
5491
+ camera.elevation = parseFiniteNumber2(value, line, fieldToken.column, "camera.elevation");
5492
+ break;
5493
+ case "roll":
5494
+ camera.roll = parseFiniteNumber2(value, line, fieldToken.column, "camera.roll");
5495
+ break;
5496
+ case "distance":
5497
+ camera.distance = parsePositiveNumber2(value, line, fieldToken.column, "camera.distance");
5498
+ break;
5499
+ default:
5500
+ throw new WorldOrbitError(`Unknown viewpoint camera field "${fieldToken.value}"`, line, fieldToken.column);
5501
+ }
5502
+ }
5503
+ return camera;
5504
+ }
5098
5505
  function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
5099
5506
  const fields = [];
5100
5507
  let index = 0;
@@ -5227,7 +5634,7 @@
5227
5634
  object.tolerances = tolerances;
5228
5635
  if (typedBlocks && Object.keys(typedBlocks).length > 0)
5229
5636
  object.typedBlocks = typedBlocks;
5230
- if (sourceSchemaVersion !== "2.1") {
5637
+ if (isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
5231
5638
  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) {
5232
5639
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
5233
5640
  }
@@ -5243,23 +5650,25 @@
5243
5650
  };
5244
5651
  }
5245
5652
  function normalizeDraftEventPose(rawPose) {
5246
- const fieldMap = collectDraftFields(rawPose.fields);
5653
+ const fieldMap = collectDraftFields(rawPose.fields, "event-pose");
5247
5654
  const placement = extractPlacementFromFieldMap(fieldMap);
5248
5655
  return {
5249
5656
  objectId: rawPose.objectId,
5250
5657
  placement,
5251
5658
  inner: parseOptionalUnitField(fieldMap.get("inner")?.[0], "inner"),
5252
- outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer")
5659
+ outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer"),
5660
+ epoch: parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]),
5661
+ referencePlane: parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0])
5253
5662
  };
5254
5663
  }
5255
- function collectDraftFields(fields) {
5664
+ function collectDraftFields(fields, _mode = "object") {
5256
5665
  const grouped = /* @__PURE__ */ new Map();
5257
5666
  for (const field of fields) {
5258
5667
  const spec = getDraftObjectFieldSpec(field.key);
5259
- if (!spec) {
5668
+ if (!spec && !EVENT_POSE_FIELD_KEYS.has(field.key)) {
5260
5669
  throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
5261
5670
  }
5262
- if (!spec.allowRepeat && grouped.has(field.key)) {
5671
+ if (!spec?.allowRepeat && grouped.has(field.key)) {
5263
5672
  throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
5264
5673
  }
5265
5674
  const existing = grouped.get(field.key) ?? [];
@@ -5436,7 +5845,7 @@
5436
5845
  }
5437
5846
  }
5438
5847
  function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
5439
- if (sourceSchemaVersion === "2.1") {
5848
+ if (!isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
5440
5849
  return;
5441
5850
  }
5442
5851
  diagnostics.push({
@@ -5448,6 +5857,34 @@
5448
5857
  column: location.column
5449
5858
  });
5450
5859
  }
5860
+ function warnIfSchema25Feature(sourceSchemaVersion, diagnostics, featureName, location) {
5861
+ if (!isSchemaOlderThan(sourceSchemaVersion, "2.5")) {
5862
+ return;
5863
+ }
5864
+ diagnostics.push({
5865
+ code: "parse.schema25.featureCompatibility",
5866
+ severity: "warning",
5867
+ source: "parse",
5868
+ message: `Feature "${featureName}" requires schema 2.5; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
5869
+ line: location.line,
5870
+ column: location.column
5871
+ });
5872
+ }
5873
+ function isSchemaOlderThan(sourceSchemaVersion, requiredVersion) {
5874
+ return schemaVersionRank(sourceSchemaVersion) < schemaVersionRank(requiredVersion);
5875
+ }
5876
+ function schemaVersionRank(version) {
5877
+ switch (version) {
5878
+ case "2.0-draft":
5879
+ return 0;
5880
+ case "2.0":
5881
+ return 1;
5882
+ case "2.1":
5883
+ return 2;
5884
+ case "2.5":
5885
+ return 3;
5886
+ }
5887
+ }
5451
5888
  function preprocessAtlasSource(source) {
5452
5889
  const chars = [...source];
5453
5890
  const comments = [];
@@ -5535,7 +5972,7 @@
5535
5972
  }
5536
5973
 
5537
5974
  // packages/core/dist/atlas-edit.js
5538
- function createEmptyAtlasDocument(systemId = "WorldOrbit", version = "2.0") {
5975
+ function createEmptyAtlasDocument(systemId = "WorldOrbit", version = "2.5") {
5539
5976
  return {
5540
5977
  format: "worldorbit",
5541
5978
  version,
@@ -5882,8 +6319,9 @@
5882
6319
  }
5883
6320
 
5884
6321
  // packages/core/dist/load.js
5885
- var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1)?$/i;
6322
+ var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1|\.5)?$/i;
5886
6323
  var ATLAS_SCHEMA_21_PATTERN = /^schema\s+2\.1$/i;
6324
+ var ATLAS_SCHEMA_25_PATTERN = /^schema\s+2\.5$/i;
5887
6325
  var LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
5888
6326
  function detectWorldOrbitSchemaVersion(source) {
5889
6327
  for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
@@ -5897,6 +6335,9 @@
5897
6335
  if (ATLAS_SCHEMA_21_PATTERN.test(trimmed)) {
5898
6336
  return "2.1";
5899
6337
  }
6338
+ if (ATLAS_SCHEMA_25_PATTERN.test(trimmed)) {
6339
+ return "2.5";
6340
+ }
5900
6341
  if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
5901
6342
  return "2.0";
5902
6343
  }
@@ -5957,7 +6398,7 @@
5957
6398
  }
5958
6399
  function loadWorldOrbitSourceWithDiagnostics(source) {
5959
6400
  const schemaVersion = detectWorldOrbitSchemaVersion(source);
5960
- if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1") {
6401
+ if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1" || schemaVersion === "2.5") {
5961
6402
  return loadAtlasSourceWithDiagnostics(source, schemaVersion);
5962
6403
  }
5963
6404
  let ast;