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.
@@ -921,7 +921,9 @@
921
921
  const height = frame.height;
922
922
  const padding = frame.padding;
923
923
  const layoutPreset = resolveLayoutPreset(document2);
924
- const projection = resolveProjection(document2, options.projection);
924
+ const schemaProjection = resolveProjection(document2, options.projection);
925
+ const camera = normalizeViewCamera(options.camera ?? null);
926
+ const renderProjection = resolveRenderProjection(schemaProjection, camera);
925
927
  const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
926
928
  const spacingFactor = layoutPresetSpacing(layoutPreset);
927
929
  const systemId = document2.system?.id ?? null;
@@ -964,7 +966,7 @@
964
966
  surfaceChildren,
965
967
  objectMap,
966
968
  spacingFactor,
967
- projection,
969
+ projection: renderProjection,
968
970
  scaleModel
969
971
  };
970
972
  const primaryRoot = rootObjects.find((object) => object.type === "star") ?? rootObjects[0] ?? null;
@@ -976,7 +978,7 @@
976
978
  const rootRingRadius = Math.min(width, height) * 0.28 * spacingFactor * scaleModel.orbitDistanceMultiplier;
977
979
  secondaryRoots.forEach((object, index) => {
978
980
  const angle = angleForIndex(index, secondaryRoots.length, -Math.PI / 2);
979
- const offset = projectPolarOffset(angle, rootRingRadius, projection, 1);
981
+ const offset = projectPolarOffset(angle, rootRingRadius, renderProjection, 1);
980
982
  placeObject(object, centerX + offset.x, centerY + offset.y, 0, positions, orbitDrafts, leaderDrafts, context);
981
983
  });
982
984
  }
@@ -1038,27 +1040,34 @@
1038
1040
  const layers = createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels);
1039
1041
  const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, scaleModel.labelMultiplier);
1040
1042
  const semanticGroups = createSceneSemanticGroups(document2, objects);
1041
- const viewpoints = createSceneViewpoints(document2, projection, frame.preset, relationships, objectMap);
1043
+ const viewpoints = createSceneViewpoints(document2, schemaProjection, frame.preset, relationships, objectMap);
1042
1044
  const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, scaleModel.labelMultiplier);
1043
1045
  return {
1044
1046
  width,
1045
1047
  height,
1046
1048
  padding,
1047
1049
  renderPreset: frame.preset,
1048
- projection,
1050
+ projection: schemaProjection,
1051
+ renderProjection,
1052
+ camera,
1049
1053
  scaleModel,
1050
1054
  title: String(document2.system?.title ?? document2.system?.properties.title ?? document2.system?.id ?? "WorldOrbit") || "WorldOrbit",
1051
- subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
1055
+ subtitle: buildSceneSubtitle(schemaProjection, renderProjection, layoutPreset, camera),
1052
1056
  systemId,
1053
- viewMode: projection,
1057
+ viewMode: schemaProjection,
1054
1058
  layoutPreset,
1055
1059
  metadata: {
1056
1060
  format: document2.format,
1057
1061
  version: document2.version,
1058
- view: projection,
1062
+ view: schemaProjection,
1063
+ renderProjection,
1059
1064
  scale: String(document2.system?.properties.scale ?? layoutPreset),
1060
1065
  units: String(document2.system?.properties.units ?? "mixed"),
1061
- preset: frame.preset ?? "custom"
1066
+ preset: frame.preset ?? "custom",
1067
+ ...camera?.azimuth !== null ? { "camera.azimuth": String(camera?.azimuth) } : {},
1068
+ ...camera?.elevation !== null ? { "camera.elevation": String(camera?.elevation) } : {},
1069
+ ...camera?.roll !== null ? { "camera.roll": String(camera?.roll) } : {},
1070
+ ...camera?.distance !== null ? { "camera.distance": String(camera?.distance) } : {}
1062
1071
  },
1063
1072
  contentBounds,
1064
1073
  layers,
@@ -1095,21 +1104,42 @@
1095
1104
  return cloned;
1096
1105
  }
1097
1106
  const objectMap = new Map(cloned.map((object) => [object.id, object]));
1107
+ const referencedIds = /* @__PURE__ */ new Set([
1108
+ ...activeEvent.targetObjectId ? [activeEvent.targetObjectId] : [],
1109
+ ...activeEvent.participantObjectIds,
1110
+ ...activeEvent.positions.map((pose) => pose.objectId)
1111
+ ]);
1112
+ for (const objectId of referencedIds) {
1113
+ const object = objectMap.get(objectId);
1114
+ if (!object) {
1115
+ continue;
1116
+ }
1117
+ if (activeEvent.epoch) {
1118
+ object.epoch = activeEvent.epoch;
1119
+ }
1120
+ if (activeEvent.referencePlane) {
1121
+ object.referencePlane = activeEvent.referencePlane;
1122
+ }
1123
+ }
1098
1124
  for (const pose of activeEvent.positions) {
1099
1125
  const object = objectMap.get(pose.objectId);
1100
1126
  if (!object) {
1101
1127
  continue;
1102
1128
  }
1103
- object.placement = pose.placement ? structuredClone(pose.placement) : null;
1129
+ if (pose.placement) {
1130
+ object.placement = structuredClone(pose.placement);
1131
+ }
1104
1132
  if (pose.inner) {
1105
1133
  object.properties.inner = { ...pose.inner };
1106
- } else {
1107
- delete object.properties.inner;
1108
1134
  }
1109
1135
  if (pose.outer) {
1110
1136
  object.properties.outer = { ...pose.outer };
1111
- } else {
1112
- delete object.properties.outer;
1137
+ }
1138
+ if (pose.epoch) {
1139
+ object.epoch = pose.epoch;
1140
+ }
1141
+ if (pose.referencePlane) {
1142
+ object.referencePlane = pose.referencePlane;
1113
1143
  }
1114
1144
  }
1115
1145
  return cloned;
@@ -1150,10 +1180,59 @@
1150
1180
  }
1151
1181
  }
1152
1182
  function resolveProjection(document2, projection) {
1153
- if (projection === "topdown" || projection === "isometric") {
1183
+ if (projection === "topdown" || projection === "isometric" || projection === "orthographic" || projection === "perspective") {
1154
1184
  return projection;
1155
1185
  }
1156
- return String(document2.system?.properties.view ?? "topdown").toLowerCase() === "isometric" ? "isometric" : "topdown";
1186
+ const documentView = String(document2.system?.properties.view ?? "topdown").toLowerCase();
1187
+ return parseViewProjection(documentView) ?? "topdown";
1188
+ }
1189
+ function resolveRenderProjection(projection, camera) {
1190
+ switch (projection) {
1191
+ case "topdown":
1192
+ return "topdown";
1193
+ case "isometric":
1194
+ return "isometric";
1195
+ case "orthographic":
1196
+ return camera && (camera.azimuth !== null || camera.elevation !== null || camera.roll !== null) ? "isometric" : "topdown";
1197
+ case "perspective":
1198
+ return "isometric";
1199
+ }
1200
+ }
1201
+ function normalizeViewCamera(camera) {
1202
+ if (!camera) {
1203
+ return null;
1204
+ }
1205
+ const normalized = {
1206
+ azimuth: normalizeFiniteCameraValue(camera.azimuth),
1207
+ elevation: normalizeFiniteCameraValue(camera.elevation),
1208
+ roll: normalizeFiniteCameraValue(camera.roll),
1209
+ distance: normalizePositiveCameraDistance(camera.distance)
1210
+ };
1211
+ return normalized.azimuth !== null || normalized.elevation !== null || normalized.roll !== null || normalized.distance !== null ? normalized : null;
1212
+ }
1213
+ function normalizeFiniteCameraValue(value) {
1214
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1215
+ }
1216
+ function normalizePositiveCameraDistance(value) {
1217
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
1218
+ }
1219
+ function buildSceneSubtitle(projection, renderProjection, layoutPreset, camera) {
1220
+ const parts = [`${capitalizeLabel(projection)} view`, `${capitalizeLabel(layoutPreset)} layout`];
1221
+ if (projection !== renderProjection) {
1222
+ parts.push(`2D ${renderProjection} fallback`);
1223
+ }
1224
+ if (camera) {
1225
+ const cameraParts = [
1226
+ camera.azimuth !== null ? `az ${camera.azimuth}` : null,
1227
+ camera.elevation !== null ? `el ${camera.elevation}` : null,
1228
+ camera.roll !== null ? `roll ${camera.roll}` : null,
1229
+ camera.distance !== null ? `dist ${camera.distance}` : null
1230
+ ].filter(Boolean);
1231
+ if (cameraParts.length > 0) {
1232
+ parts.push(`camera ${cameraParts.join(" / ")}`);
1233
+ }
1234
+ }
1235
+ return parts.join(" - ");
1157
1236
  }
1158
1237
  function resolveScaleModel(layoutPreset, overrides) {
1159
1238
  const defaults = defaultScaleModel(layoutPreset);
@@ -1589,6 +1668,8 @@
1589
1668
  function createGeneratedOverviewViewpoint(document2, projection, preset) {
1590
1669
  const title = document2.system?.title ?? document2.system?.properties.title;
1591
1670
  const label = title ? `${String(title)} Overview` : "Overview";
1671
+ const camera = normalizeViewCamera(null);
1672
+ const renderProjection = resolveRenderProjection(projection, camera);
1592
1673
  return {
1593
1674
  id: "overview",
1594
1675
  label,
@@ -1597,6 +1678,8 @@
1597
1678
  selectedObjectId: null,
1598
1679
  eventIds: [],
1599
1680
  projection,
1681
+ renderProjection,
1682
+ camera,
1600
1683
  preset,
1601
1684
  rotationDeg: 0,
1602
1685
  scale: null,
@@ -1646,6 +1729,30 @@
1646
1729
  case "angle":
1647
1730
  draft.rotationDeg = parseFiniteNumber(normalizedValue) ?? draft.rotationDeg ?? 0;
1648
1731
  return;
1732
+ case "camera.azimuth":
1733
+ draft.camera = {
1734
+ ...draft.camera ?? createEmptyViewCamera(),
1735
+ azimuth: parseFiniteNumber(normalizedValue)
1736
+ };
1737
+ return;
1738
+ case "camera.elevation":
1739
+ draft.camera = {
1740
+ ...draft.camera ?? createEmptyViewCamera(),
1741
+ elevation: parseFiniteNumber(normalizedValue)
1742
+ };
1743
+ return;
1744
+ case "camera.roll":
1745
+ draft.camera = {
1746
+ ...draft.camera ?? createEmptyViewCamera(),
1747
+ roll: parseFiniteNumber(normalizedValue)
1748
+ };
1749
+ return;
1750
+ case "camera.distance":
1751
+ draft.camera = {
1752
+ ...draft.camera ?? createEmptyViewCamera(),
1753
+ distance: parsePositiveNumber(normalizedValue)
1754
+ };
1755
+ return;
1649
1756
  case "zoom":
1650
1757
  case "scale":
1651
1758
  draft.scale = parsePositiveNumber(normalizedValue);
@@ -1685,6 +1792,9 @@
1685
1792
  const selectedObjectId = draft.select && objectMap.has(draft.select) ? draft.select : objectId;
1686
1793
  const filter = normalizeViewpointFilter(draft.filter);
1687
1794
  const label = draft.label?.trim() || humanizeIdentifier(draft.id);
1795
+ const resolvedProjection = draft.projection ?? projection;
1796
+ const camera = normalizeViewCamera(draft.camera ?? null);
1797
+ const renderProjection = resolveRenderProjection(resolvedProjection, camera);
1688
1798
  return {
1689
1799
  id: draft.id,
1690
1800
  label,
@@ -1692,7 +1802,9 @@
1692
1802
  objectId,
1693
1803
  selectedObjectId,
1694
1804
  eventIds: [...new Set(draft.eventIds ?? [])],
1695
- projection: draft.projection ?? projection,
1805
+ projection: resolvedProjection,
1806
+ renderProjection,
1807
+ camera,
1696
1808
  preset: draft.preset ?? preset,
1697
1809
  rotationDeg: draft.rotationDeg ?? 0,
1698
1810
  scale: draft.scale ?? null,
@@ -1709,6 +1821,14 @@
1709
1821
  groupIds: []
1710
1822
  };
1711
1823
  }
1824
+ function createEmptyViewCamera() {
1825
+ return {
1826
+ azimuth: null,
1827
+ elevation: null,
1828
+ roll: null,
1829
+ distance: null
1830
+ };
1831
+ }
1712
1832
  function normalizeViewpointFilter(filter) {
1713
1833
  if (!filter) {
1714
1834
  return null;
@@ -1722,7 +1842,18 @@
1722
1842
  return normalized.query || normalized.objectTypes.length > 0 || normalized.tags.length > 0 || normalized.groupIds.length > 0 ? normalized : null;
1723
1843
  }
1724
1844
  function parseViewProjection(value) {
1725
- return value.toLowerCase() === "isometric" ? "isometric" : value.toLowerCase() === "topdown" ? "topdown" : null;
1845
+ switch (value.toLowerCase()) {
1846
+ case "topdown":
1847
+ return "topdown";
1848
+ case "isometric":
1849
+ return "isometric";
1850
+ case "orthographic":
1851
+ return "orthographic";
1852
+ case "perspective":
1853
+ return "perspective";
1854
+ default:
1855
+ return null;
1856
+ }
1726
1857
  }
1727
1858
  function parseRenderPreset(value) {
1728
1859
  const normalized = value.toLowerCase();
@@ -1760,7 +1891,7 @@
1760
1891
  }
1761
1892
  function parseViewpointGroups(value, document2, relationships, objectMap) {
1762
1893
  return splitListValue(value).map((entry) => {
1763
- if (document2.schemaVersion === "2.1" || document2.groups.some((group) => group.id === entry)) {
1894
+ if (document2.schemaVersion === "2.1" || document2.schemaVersion === "2.5" || document2.groups.some((group) => group.id === entry)) {
1764
1895
  return entry;
1765
1896
  }
1766
1897
  if (entry.startsWith("wo-") && entry.endsWith("-group")) {
@@ -2553,8 +2684,8 @@
2553
2684
  }
2554
2685
  return {
2555
2686
  format: "worldorbit",
2556
- version: "2.0",
2557
- schemaVersion: "2.0",
2687
+ version: "2.5",
2688
+ schemaVersion: "2.5",
2558
2689
  sourceVersion: document2.version,
2559
2690
  system,
2560
2691
  groups: structuredClone(document2.groups ?? []),
@@ -2610,8 +2741,9 @@
2610
2741
  };
2611
2742
  }
2612
2743
  function createDraftDefaults(document2, preset, projection) {
2744
+ const rawView = typeof document2.system?.properties.view === "string" ? document2.system.properties.view.toLowerCase() : null;
2613
2745
  return {
2614
- view: typeof document2.system?.properties.view === "string" && document2.system.properties.view.toLowerCase() === "topdown" ? "topdown" : projection,
2746
+ view: rawView === "topdown" || rawView === "isometric" || rawView === "orthographic" || rawView === "perspective" ? rawView : projection,
2615
2747
  scale: typeof document2.system?.properties.scale === "string" ? document2.system.properties.scale : null,
2616
2748
  units: typeof document2.system?.properties.units === "string" ? document2.system.properties.units : null,
2617
2749
  preset,
@@ -2718,6 +2850,7 @@
2718
2850
  preset: viewpoint.preset,
2719
2851
  zoom: viewpoint.scale,
2720
2852
  rotationDeg: viewpoint.rotationDeg,
2853
+ camera: viewpoint.camera ? { ...viewpoint.camera } : null,
2721
2854
  layers: { ...viewpoint.layers },
2722
2855
  filter: viewpoint.filter ? {
2723
2856
  query: viewpoint.filter.query,
@@ -2759,7 +2892,9 @@
2759
2892
  objectId: pose.objectId,
2760
2893
  placement: clonePlacement(pose.placement),
2761
2894
  inner: pose.inner ? { ...pose.inner } : void 0,
2762
- outer: pose.outer ? { ...pose.outer } : void 0
2895
+ outer: pose.outer ? { ...pose.outer } : void 0,
2896
+ epoch: pose.epoch ?? null,
2897
+ referencePlane: pose.referencePlane ?? null
2763
2898
  };
2764
2899
  }
2765
2900
  function clonePlacement(placement) {
@@ -2774,21 +2909,42 @@
2774
2909
  return;
2775
2910
  }
2776
2911
  const objectMap = new Map(objects.map((object) => [object.id, object]));
2912
+ const referencedIds = /* @__PURE__ */ new Set([
2913
+ ...event.targetObjectId ? [event.targetObjectId] : [],
2914
+ ...event.participantObjectIds,
2915
+ ...event.positions.map((pose) => pose.objectId)
2916
+ ]);
2917
+ for (const objectId of referencedIds) {
2918
+ const object = objectMap.get(objectId);
2919
+ if (!object) {
2920
+ continue;
2921
+ }
2922
+ if (event.epoch) {
2923
+ object.epoch = event.epoch;
2924
+ }
2925
+ if (event.referencePlane) {
2926
+ object.referencePlane = event.referencePlane;
2927
+ }
2928
+ }
2777
2929
  for (const pose of event.positions) {
2778
2930
  const object = objectMap.get(pose.objectId);
2779
2931
  if (!object) {
2780
2932
  continue;
2781
2933
  }
2782
- object.placement = clonePlacement(pose.placement);
2934
+ if (pose.placement) {
2935
+ object.placement = clonePlacement(pose.placement);
2936
+ }
2783
2937
  if (pose.inner) {
2784
2938
  object.properties.inner = { ...pose.inner };
2785
- } else {
2786
- delete object.properties.inner;
2787
2939
  }
2788
2940
  if (pose.outer) {
2789
2941
  object.properties.outer = { ...pose.outer };
2790
- } else {
2791
- delete object.properties.outer;
2942
+ }
2943
+ if (pose.epoch) {
2944
+ object.epoch = pose.epoch;
2945
+ }
2946
+ if (pose.referencePlane) {
2947
+ object.referencePlane = pose.referencePlane;
2792
2948
  }
2793
2949
  }
2794
2950
  }
@@ -2873,6 +3029,18 @@
2873
3029
  if (viewpoint.rotationDeg !== 0) {
2874
3030
  info2[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2875
3031
  }
3032
+ if (viewpoint.camera?.azimuth !== null) {
3033
+ info2[`${prefix}.camera.azimuth`] = String(viewpoint.camera?.azimuth);
3034
+ }
3035
+ if (viewpoint.camera?.elevation !== null) {
3036
+ info2[`${prefix}.camera.elevation`] = String(viewpoint.camera?.elevation);
3037
+ }
3038
+ if (viewpoint.camera?.roll !== null) {
3039
+ info2[`${prefix}.camera.roll`] = String(viewpoint.camera?.roll);
3040
+ }
3041
+ if (viewpoint.camera?.distance !== null) {
3042
+ info2[`${prefix}.camera.distance`] = String(viewpoint.camera?.distance);
3043
+ }
2876
3044
  const serializedLayers = serializeViewpointLayers(viewpoint.layers);
2877
3045
  if (serializedLayers) {
2878
3046
  info2[`${prefix}.layers`] = serializedLayers;
@@ -2969,26 +3137,26 @@
2969
3137
  ];
2970
3138
  function formatDocument(document2, options = {}) {
2971
3139
  const schema = options.schema ?? "auto";
2972
- const useDraft = schema === "2.0" || schema === "2.1" || schema === "2.0-draft" || document2.version === "2.0" || document2.version === "2.1" || document2.version === "2.0-draft";
3140
+ const useDraft = schema === "2.0" || schema === "2.1" || schema === "2.5" || schema === "2.0-draft" || document2.version === "2.0" || document2.version === "2.1" || document2.version === "2.5" || document2.version === "2.0-draft";
2973
3141
  if (useDraft) {
2974
3142
  if (schema === "2.0-draft") {
2975
- const legacyDraftDocument = document2.version === "2.0-draft" ? document2 : document2.version === "2.0" || document2.version === "2.1" ? {
3143
+ const legacyDraftDocument = document2.version === "2.0-draft" ? document2 : document2.version === "2.0" || document2.version === "2.1" || document2.version === "2.5" ? {
2976
3144
  ...document2,
2977
3145
  version: "2.0-draft",
2978
3146
  schemaVersion: "2.0-draft"
2979
3147
  } : upgradeDocumentToDraftV2(document2);
2980
3148
  return formatDraftDocument(legacyDraftDocument);
2981
3149
  }
2982
- const atlasDocument = document2.version === "2.0" || document2.version === "2.1" ? document2 : document2.version === "2.0-draft" ? {
3150
+ const atlasDocument = document2.version === "2.0" || document2.version === "2.1" || document2.version === "2.5" ? document2 : document2.version === "2.0-draft" ? {
2983
3151
  ...document2,
2984
3152
  version: "2.0",
2985
3153
  schemaVersion: "2.0"
2986
3154
  } : upgradeDocumentToV2(document2);
2987
- if (schema === "2.1" && atlasDocument.version !== "2.1") {
3155
+ if ((schema === "2.0" || schema === "2.1" || schema === "2.5") && atlasDocument.version !== schema) {
2988
3156
  return formatAtlasDocument({
2989
3157
  ...atlasDocument,
2990
- version: "2.1",
2991
- schemaVersion: "2.1"
3158
+ version: schema,
3159
+ schemaVersion: schema
2992
3160
  });
2993
3161
  }
2994
3162
  return formatAtlasDocument(atlasDocument);
@@ -3265,6 +3433,21 @@
3265
3433
  if (viewpoint.rotationDeg !== 0) {
3266
3434
  lines.push(` rotation ${viewpoint.rotationDeg}`);
3267
3435
  }
3436
+ if (viewpoint.camera && hasCameraValues(viewpoint.camera)) {
3437
+ lines.push(" camera");
3438
+ if (viewpoint.camera.azimuth !== null) {
3439
+ lines.push(` azimuth ${viewpoint.camera.azimuth}`);
3440
+ }
3441
+ if (viewpoint.camera.elevation !== null) {
3442
+ lines.push(` elevation ${viewpoint.camera.elevation}`);
3443
+ }
3444
+ if (viewpoint.camera.roll !== null) {
3445
+ lines.push(` roll ${viewpoint.camera.roll}`);
3446
+ }
3447
+ if (viewpoint.camera.distance !== null) {
3448
+ lines.push(` distance ${viewpoint.camera.distance}`);
3449
+ }
3450
+ }
3268
3451
  const layerTokens = formatDraftLayers(viewpoint.layers);
3269
3452
  if (layerTokens.length > 0) {
3270
3453
  lines.push(` layers ${layerTokens.join(" ")}`);
@@ -3364,6 +3547,12 @@
3364
3547
  if (event.visibility) {
3365
3548
  lines.push(` visibility ${quoteIfNeeded(event.visibility)}`);
3366
3549
  }
3550
+ if (event.epoch) {
3551
+ lines.push(` epoch ${quoteIfNeeded(event.epoch)}`);
3552
+ }
3553
+ if (event.referencePlane) {
3554
+ lines.push(` referencePlane ${quoteIfNeeded(event.referencePlane)}`);
3555
+ }
3367
3556
  if (event.tags.length > 0) {
3368
3557
  lines.push(` tags ${event.tags.map(quoteIfNeeded).join(" ")}`);
3369
3558
  }
@@ -3388,10 +3577,15 @@
3388
3577
  function formatEventPoseFields(pose) {
3389
3578
  return [
3390
3579
  ...formatPlacement(pose.placement),
3580
+ ...pose.epoch ? [`epoch ${quoteIfNeeded(pose.epoch)}`] : [],
3581
+ ...pose.referencePlane ? [`referencePlane ${quoteIfNeeded(pose.referencePlane)}`] : [],
3391
3582
  ...formatOptionalUnit("inner", pose.inner),
3392
3583
  ...formatOptionalUnit("outer", pose.outer)
3393
3584
  ];
3394
3585
  }
3586
+ function hasCameraValues(camera) {
3587
+ return camera.azimuth !== null || camera.elevation !== null || camera.roll !== null || camera.distance !== null;
3588
+ }
3395
3589
  function formatValue(value) {
3396
3590
  if (Array.isArray(value)) {
3397
3591
  return value.map((item) => quoteIfNeeded(item)).join(" ");
@@ -3685,13 +3879,13 @@
3685
3879
  validateRelation(relation, objectMap, diagnostics);
3686
3880
  }
3687
3881
  for (const viewpoint of document2.system?.viewpoints ?? []) {
3688
- validateViewpoint(viewpoint.filter, viewpoint.events ?? [], groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpoint.id);
3882
+ validateViewpoint(viewpoint, groupIds, eventIds, sourceSchemaVersion, diagnostics, objectMap);
3689
3883
  }
3690
3884
  for (const object of document2.objects) {
3691
3885
  validateObject(object, document2.system, objectMap, groupIds, diagnostics);
3692
3886
  }
3693
3887
  for (const event of document2.events) {
3694
- validateEvent(event, objectMap, diagnostics);
3888
+ validateEvent(event, document2.system, objectMap, diagnostics);
3695
3889
  }
3696
3890
  return diagnostics;
3697
3891
  }
@@ -3710,21 +3904,24 @@
3710
3904
  diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
3711
3905
  }
3712
3906
  }
3713
- function validateViewpoint(filter, eventRefs, groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpointId) {
3714
- if (sourceSchemaVersion === "2.1") {
3907
+ function validateViewpoint(viewpoint, groupIds, eventIds, sourceSchemaVersion, diagnostics, objectMap) {
3908
+ const filter = viewpoint.filter;
3909
+ if (sourceSchemaVersion === "2.1" || sourceSchemaVersion === "2.5") {
3715
3910
  if (filter) {
3716
3911
  for (const groupId of filter.groupIds) {
3717
3912
  if (!groupIds.has(groupId)) {
3718
- diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.groups`));
3913
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpoint.id}".`, void 0, `viewpoint.${viewpoint.id}.groups`));
3719
3914
  }
3720
3915
  }
3721
3916
  }
3722
- for (const eventId of eventRefs) {
3917
+ for (const eventId of viewpoint.events ?? []) {
3723
3918
  if (!eventIds.has(eventId)) {
3724
- diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.events`));
3919
+ diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpoint.id}".`, void 0, `viewpoint.${viewpoint.id}.events`));
3725
3920
  }
3726
3921
  }
3727
3922
  }
3923
+ validateProjection(viewpoint.projection, diagnostics, `viewpoint.${viewpoint.id}.projection`, viewpoint.id);
3924
+ validateCamera(viewpoint.camera, viewpoint.projection, viewpoint.rotationDeg, diagnostics, viewpoint.id, viewpoint.focusObjectId, viewpoint.selectedObjectId, filter, objectMap);
3728
3925
  }
3729
3926
  function validateObject(object, system, objectMap, groupIds, diagnostics) {
3730
3927
  const placement = object.placement;
@@ -3737,6 +3934,12 @@
3737
3934
  }
3738
3935
  }
3739
3936
  }
3937
+ if (typeof object.epoch === "string" && !object.epoch.trim()) {
3938
+ diagnostics.push(warn("validate.epoch.empty", `Object "${object.id}" defines an empty epoch string.`, object.id, "epoch"));
3939
+ }
3940
+ if (typeof object.referencePlane === "string" && !object.referencePlane.trim()) {
3941
+ diagnostics.push(warn("validate.referencePlane.empty", `Object "${object.id}" defines an empty reference plane string.`, object.id, "referencePlane"));
3942
+ }
3740
3943
  if (orbitPlacement) {
3741
3944
  if (!objectMap.has(orbitPlacement.target)) {
3742
3945
  diagnostics.push(error("validate.orbit.target.unknown", `Unknown placement target "${orbitPlacement.target}" on "${object.id}".`, object.id, "orbit"));
@@ -3808,12 +4011,18 @@
3808
4011
  }
3809
4012
  }
3810
4013
  }
3811
- function validateEvent(event, objectMap, diagnostics) {
4014
+ function validateEvent(event, system, objectMap, diagnostics) {
3812
4015
  const fieldPrefix = `event.${event.id}`;
3813
4016
  const referencedIds = /* @__PURE__ */ new Set();
3814
4017
  if (!event.kind.trim()) {
3815
4018
  diagnostics.push(error("validate.event.kind.required", `Event "${event.id}" is missing a "kind" value.`, void 0, `${fieldPrefix}.kind`));
3816
4019
  }
4020
+ if (typeof event.epoch === "string" && !event.epoch.trim()) {
4021
+ diagnostics.push(warn("validate.event.epoch.empty", `Event "${event.id}" defines an empty epoch string.`, void 0, `${fieldPrefix}.epoch`));
4022
+ }
4023
+ if (typeof event.referencePlane === "string" && !event.referencePlane.trim()) {
4024
+ diagnostics.push(warn("validate.event.referencePlane.empty", `Event "${event.id}" defines an empty reference plane string.`, void 0, `${fieldPrefix}.referencePlane`));
4025
+ }
3817
4026
  if (!event.targetObjectId && event.participantObjectIds.length === 0) {
3818
4027
  diagnostics.push(error("validate.event.references.required", `Event "${event.id}" must define a "target" or at least one participant.`, void 0, `${fieldPrefix}.participants`));
3819
4028
  }
@@ -3860,10 +4069,14 @@
3860
4069
  if (!referencedIds.has(pose.objectId)) {
3861
4070
  diagnostics.push(warn("validate.event.pose.unreferenced", `Event pose "${pose.objectId}" on "${event.id}" is not listed in target/participants.`, void 0, poseFieldPrefix));
3862
4071
  }
3863
- validateEventPose(pose, object, objectMap, diagnostics, poseFieldPrefix, event.id);
4072
+ validateEventPose(pose, object, event, system, objectMap, diagnostics, poseFieldPrefix, event.id);
4073
+ }
4074
+ const missingPoseIds = [...referencedIds].filter((objectId) => !poseIds.has(objectId));
4075
+ if (event.positions.length > 0 && missingPoseIds.length > 0) {
4076
+ diagnostics.push(warn("validate.event.positions.partial", `Event "${event.id}" leaves ${missingPoseIds.length} referenced object(s) on their base placement.`, void 0, `${fieldPrefix}.positions`));
3864
4077
  }
3865
4078
  }
3866
- function validateEventPose(pose, object, objectMap, diagnostics, fieldPrefix, eventId) {
4079
+ function validateEventPose(pose, object, event, system, objectMap, diagnostics, fieldPrefix, eventId) {
3867
4080
  const placement = pose.placement;
3868
4081
  if (!placement) {
3869
4082
  diagnostics.push(error("validate.event.pose.placement.required", `Event "${eventId}" pose "${pose.objectId}" is missing a placement mode.`, void 0, fieldPrefix));
@@ -3876,6 +4089,15 @@
3876
4089
  if (placement.distance && placement.semiMajor) {
3877
4090
  diagnostics.push(error("validate.event.pose.orbit.distanceConflict", `Event "${eventId}" pose "${pose.objectId}" cannot declare both "distance" and "semiMajor".`, void 0, `${fieldPrefix}.distance`));
3878
4091
  }
4092
+ if (placement.phase && !resolveEffectiveEpoch(system, object, event, pose)) {
4093
+ diagnostics.push(warn("validate.event.pose.phase.epochMissing", `Event "${eventId}" pose "${pose.objectId}" sets "phase" without an effective epoch.`, void 0, `${fieldPrefix}.phase`));
4094
+ }
4095
+ if (placement.inclination && !resolveEffectiveReferencePlane(system, object, event, pose)) {
4096
+ diagnostics.push(warn("validate.event.pose.inclination.referencePlaneMissing", `Event "${eventId}" pose "${pose.objectId}" sets "inclination" without an effective reference plane.`, void 0, `${fieldPrefix}.inclination`));
4097
+ }
4098
+ if (placement.period && !massInSolar(objectMap.get(placement.target)?.properties.mass)) {
4099
+ 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`));
4100
+ }
3879
4101
  return;
3880
4102
  }
3881
4103
  if (placement.mode === "surface") {
@@ -4010,6 +4232,52 @@
4010
4232
  return null;
4011
4233
  }
4012
4234
  }
4235
+ function validateProjection(projection, diagnostics, field, viewpointId) {
4236
+ if (projection !== "topdown" && projection !== "isometric" && projection !== "orthographic" && projection !== "perspective") {
4237
+ diagnostics.push(error("validate.viewpoint.projection.invalid", `Unknown projection "${String(projection)}" in viewpoint "${viewpointId}".`, void 0, field));
4238
+ }
4239
+ }
4240
+ function validateCamera(camera, projection, rotationDeg, diagnostics, viewpointId, focusObjectId, selectedObjectId, filter, objectMap) {
4241
+ if (!camera) {
4242
+ return;
4243
+ }
4244
+ const prefix = `viewpoint.${viewpointId}.camera`;
4245
+ for (const [key, value] of [
4246
+ ["azimuth", camera.azimuth],
4247
+ ["elevation", camera.elevation],
4248
+ ["roll", camera.roll],
4249
+ ["distance", camera.distance]
4250
+ ]) {
4251
+ if (value !== null && (!Number.isFinite(value) || key === "distance" && value <= 0)) {
4252
+ diagnostics.push(error("validate.viewpoint.camera.invalid", `Invalid camera ${key} "${String(value)}" in viewpoint "${viewpointId}".`, void 0, `${prefix}.${key}`));
4253
+ }
4254
+ }
4255
+ if (camera.distance !== null && projection !== "perspective") {
4256
+ 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`));
4257
+ }
4258
+ if (projection === "topdown" && (camera.elevation !== null || camera.roll !== null)) {
4259
+ 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));
4260
+ }
4261
+ if (projection === "isometric" && camera.elevation !== null) {
4262
+ diagnostics.push(info("validate.viewpoint.camera.isometricStored", `Camera elevation on isometric viewpoint "${viewpointId}" is preserved semantically for future 3D rendering.`, void 0, `${prefix}.elevation`));
4263
+ }
4264
+ if (camera.azimuth !== null && camera.azimuth !== 0 && rotationDeg !== 0) {
4265
+ 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`));
4266
+ }
4267
+ const hasAnchor = focusObjectId !== null && objectMap.has(focusObjectId) || selectedObjectId !== null && objectMap.has(selectedObjectId) || !!filter;
4268
+ if (!hasAnchor) {
4269
+ diagnostics.push(info("validate.viewpoint.camera.anchorMissing", `Viewpoint "${viewpointId}" stores camera settings without a focus object, selection, or filter anchor.`, void 0, prefix));
4270
+ }
4271
+ }
4272
+ function resolveEffectiveEpoch(system, object, event, pose) {
4273
+ return normalizeOptionalContextString(pose?.epoch) ?? normalizeOptionalContextString(event?.epoch) ?? normalizeOptionalContextString(object.epoch) ?? normalizeOptionalContextString(system?.epoch) ?? null;
4274
+ }
4275
+ function resolveEffectiveReferencePlane(system, object, event, pose) {
4276
+ return normalizeOptionalContextString(pose?.referencePlane) ?? normalizeOptionalContextString(event?.referencePlane) ?? normalizeOptionalContextString(object.referencePlane) ?? normalizeOptionalContextString(system?.referencePlane) ?? null;
4277
+ }
4278
+ function normalizeOptionalContextString(value) {
4279
+ return typeof value === "string" && value.trim() ? value.trim() : null;
4280
+ }
4013
4281
  function toleranceForField(object, field) {
4014
4282
  const tolerance = object.tolerances?.find((entry) => entry.field === field)?.value;
4015
4283
  if (typeof tolerance === "number") {
@@ -4118,7 +4386,9 @@
4118
4386
  "surface",
4119
4387
  "free",
4120
4388
  "inner",
4121
- "outer"
4389
+ "outer",
4390
+ "epoch",
4391
+ "referencePlane"
4122
4392
  ]);
4123
4393
  function parseWorldOrbitAtlas(source) {
4124
4394
  return parseAtlasSource(source);
@@ -4160,7 +4430,7 @@
4160
4430
  if (!sawSchemaHeader) {
4161
4431
  sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
4162
4432
  sawSchemaHeader = true;
4163
- if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
4433
+ if (prepared.comments.length > 0 && isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
4164
4434
  diagnostics.push({
4165
4435
  code: "parse.schema21.commentCompatibility",
4166
4436
  severity: "warning",
@@ -4230,11 +4500,11 @@
4230
4500
  return document2;
4231
4501
  }
4232
4502
  function assertDraftSchemaHeader(tokens, line) {
4233
- if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
4234
- throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
4503
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1", "2.5"].includes(tokens[1].value.toLowerCase())) {
4504
+ 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);
4235
4505
  }
4236
4506
  const version = tokens[1].value.toLowerCase();
4237
- return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
4507
+ return version === "2.5" ? "2.5" : version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
4238
4508
  }
4239
4509
  function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, flags) {
4240
4510
  const keyword = tokens[0]?.value.toLowerCase();
@@ -4254,6 +4524,8 @@
4254
4524
  return {
4255
4525
  kind: "defaults",
4256
4526
  system,
4527
+ sourceSchemaVersion,
4528
+ diagnostics,
4257
4529
  seenFields: /* @__PURE__ */ new Set()
4258
4530
  };
4259
4531
  case "atlas":
@@ -4346,6 +4618,7 @@
4346
4618
  preset: system.defaults.preset,
4347
4619
  zoom: null,
4348
4620
  rotationDeg: 0,
4621
+ camera: null,
4349
4622
  layers: {},
4350
4623
  filter: null
4351
4624
  };
@@ -4359,7 +4632,10 @@
4359
4632
  seenFields: /* @__PURE__ */ new Set(),
4360
4633
  inFilter: false,
4361
4634
  filterIndent: null,
4362
- seenFilterFields: /* @__PURE__ */ new Set()
4635
+ seenFilterFields: /* @__PURE__ */ new Set(),
4636
+ inCamera: false,
4637
+ cameraIndent: null,
4638
+ seenCameraFields: /* @__PURE__ */ new Set()
4363
4639
  };
4364
4640
  }
4365
4641
  function startAnnotationSection(tokens, line, system, annotationIds) {
@@ -4466,6 +4742,8 @@
4466
4742
  participantObjectIds: [],
4467
4743
  timing: null,
4468
4744
  visibility: null,
4745
+ epoch: null,
4746
+ referencePlane: null,
4469
4747
  tags: [],
4470
4748
  color: null,
4471
4749
  hidden: false,
@@ -4590,6 +4868,12 @@
4590
4868
  const value = joinFieldValue(tokens, line);
4591
4869
  switch (key) {
4592
4870
  case "view":
4871
+ if (isSchema25Projection(value)) {
4872
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "defaults.view", {
4873
+ line,
4874
+ column: tokens[0].column
4875
+ });
4876
+ }
4593
4877
  section.system.defaults.view = parseProjectionValue(value, line, tokens[0].column);
4594
4878
  return;
4595
4879
  case "scale":
@@ -4629,14 +4913,36 @@
4629
4913
  throw new WorldOrbitError(`Unknown atlas field "${tokens[0].value}"`, line, tokens[0].column);
4630
4914
  }
4631
4915
  function applyViewpointField2(section, indent, tokens, line) {
4916
+ if (section.inCamera && indent <= (section.cameraIndent ?? 0)) {
4917
+ section.inCamera = false;
4918
+ section.cameraIndent = null;
4919
+ }
4632
4920
  if (section.inFilter && indent <= (section.filterIndent ?? 0)) {
4633
4921
  section.inFilter = false;
4634
4922
  section.filterIndent = null;
4635
4923
  }
4924
+ if (section.inCamera) {
4925
+ applyViewpointCameraField(section, tokens, line);
4926
+ return;
4927
+ }
4636
4928
  if (section.inFilter) {
4637
4929
  applyViewpointFilterField(section, tokens, line);
4638
4930
  return;
4639
4931
  }
4932
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "camera") {
4933
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.camera", {
4934
+ line,
4935
+ column: tokens[0].column
4936
+ });
4937
+ if (section.seenFields.has("camera")) {
4938
+ throw new WorldOrbitError('Duplicate viewpoint field "camera"', line, tokens[0].column);
4939
+ }
4940
+ section.seenFields.add("camera");
4941
+ section.inCamera = true;
4942
+ section.cameraIndent = indent;
4943
+ section.viewpoint.camera = section.viewpoint.camera ?? createEmptyViewCamera2();
4944
+ return;
4945
+ }
4640
4946
  if (tokens.length === 1 && tokens[0].value.toLowerCase() === "filter") {
4641
4947
  if (section.seenFields.has("filter")) {
4642
4948
  throw new WorldOrbitError('Duplicate viewpoint field "filter"', line, tokens[0].column);
@@ -4662,6 +4968,12 @@
4662
4968
  section.viewpoint.selectedObjectId = value;
4663
4969
  return;
4664
4970
  case "projection":
4971
+ if (isSchema25Projection(value)) {
4972
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "projection", {
4973
+ line,
4974
+ column: tokens[0].column
4975
+ });
4976
+ }
4665
4977
  section.viewpoint.projection = parseProjectionValue(value, line, tokens[0].column);
4666
4978
  return;
4667
4979
  case "preset":
@@ -4673,6 +4985,13 @@
4673
4985
  case "rotation":
4674
4986
  section.viewpoint.rotationDeg = parseFiniteNumber2(value, line, tokens[0].column, "rotation");
4675
4987
  return;
4988
+ case "camera":
4989
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.camera", {
4990
+ line,
4991
+ column: tokens[0].column
4992
+ });
4993
+ section.viewpoint.camera = parseInlineViewCamera(tokens.slice(1), line, section.viewpoint.camera);
4994
+ return;
4676
4995
  case "layers":
4677
4996
  section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line, section.sourceSchemaVersion, section.diagnostics);
4678
4997
  return;
@@ -4687,6 +5006,28 @@
4687
5006
  throw new WorldOrbitError(`Unknown viewpoint field "${tokens[0].value}"`, line, tokens[0].column);
4688
5007
  }
4689
5008
  }
5009
+ function applyViewpointCameraField(section, tokens, line) {
5010
+ const key = requireUniqueField(tokens, section.seenCameraFields, line);
5011
+ const value = joinFieldValue(tokens, line);
5012
+ const camera = section.viewpoint.camera ?? createEmptyViewCamera2();
5013
+ switch (key) {
5014
+ case "azimuth":
5015
+ camera.azimuth = parseFiniteNumber2(value, line, tokens[0].column, "camera.azimuth");
5016
+ break;
5017
+ case "elevation":
5018
+ camera.elevation = parseFiniteNumber2(value, line, tokens[0].column, "camera.elevation");
5019
+ break;
5020
+ case "roll":
5021
+ camera.roll = parseFiniteNumber2(value, line, tokens[0].column, "camera.roll");
5022
+ break;
5023
+ case "distance":
5024
+ camera.distance = parsePositiveNumber2(value, line, tokens[0].column, "camera.distance");
5025
+ break;
5026
+ default:
5027
+ throw new WorldOrbitError(`Unknown viewpoint camera field "${tokens[0].value}"`, line, tokens[0].column);
5028
+ }
5029
+ section.viewpoint.camera = camera;
5030
+ }
4690
5031
  function applyViewpointFilterField(section, tokens, line) {
4691
5032
  const key = requireUniqueField(tokens, section.seenFilterFields, line);
4692
5033
  const filter = section.viewpoint.filter ?? createEmptyViewpointFilter2();
@@ -4797,6 +5138,12 @@
4797
5138
  section.positionsIndent = null;
4798
5139
  }
4799
5140
  if (section.activePose) {
5141
+ if (tokens[0]?.value === "epoch" || tokens[0]?.value === "referencePlane") {
5142
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, `pose.${tokens[0].value}`, {
5143
+ line,
5144
+ column: tokens[0]?.column ?? 1
5145
+ });
5146
+ }
4800
5147
  section.activePose.fields.push(parseEventPoseField(tokens, line, section.activePoseSeenFields));
4801
5148
  return;
4802
5149
  }
@@ -4851,6 +5198,20 @@
4851
5198
  case "visibility":
4852
5199
  section.event.visibility = joinFieldValue(tokens, line);
4853
5200
  return;
5201
+ case "epoch":
5202
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "event.epoch", {
5203
+ line,
5204
+ column: tokens[0].column
5205
+ });
5206
+ section.event.epoch = joinFieldValue(tokens, line);
5207
+ return;
5208
+ case "referenceplane":
5209
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "event.referencePlane", {
5210
+ line,
5211
+ column: tokens[0].column
5212
+ });
5213
+ section.event.referencePlane = joinFieldValue(tokens, line);
5214
+ return;
4854
5215
  case "tags":
4855
5216
  section.event.tags = parseTokenList(tokens.slice(1), line, "tags");
4856
5217
  return;
@@ -4978,11 +5339,15 @@
4978
5339
  }
4979
5340
  function parseProjectionValue(value, line, column) {
4980
5341
  const normalized = value.toLowerCase();
4981
- if (normalized !== "topdown" && normalized !== "isometric") {
5342
+ if (normalized !== "topdown" && normalized !== "isometric" && normalized !== "orthographic" && normalized !== "perspective") {
4982
5343
  throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
4983
5344
  }
4984
5345
  return normalized;
4985
5346
  }
5347
+ function isSchema25Projection(value) {
5348
+ const normalized = value.toLowerCase();
5349
+ return normalized === "orthographic" || normalized === "perspective";
5350
+ }
4986
5351
  function parsePresetValue(value, line, column) {
4987
5352
  const normalized = value.toLowerCase();
4988
5353
  if (normalized === "diagram" || normalized === "presentation" || normalized === "atlas-card" || normalized === "markdown") {
@@ -5012,6 +5377,48 @@
5012
5377
  groupIds: []
5013
5378
  };
5014
5379
  }
5380
+ function createEmptyViewCamera2() {
5381
+ return {
5382
+ azimuth: null,
5383
+ elevation: null,
5384
+ roll: null,
5385
+ distance: null
5386
+ };
5387
+ }
5388
+ function parseInlineViewCamera(tokens, line, current) {
5389
+ if (tokens.length === 0 || tokens.length % 2 !== 0) {
5390
+ throw new WorldOrbitError('Field "camera" expects "<field> <value>" pairs', line, tokens[0]?.column ?? 1);
5391
+ }
5392
+ const camera = current ? { ...current } : createEmptyViewCamera2();
5393
+ const seen = /* @__PURE__ */ new Set();
5394
+ for (let index = 0; index < tokens.length; index += 2) {
5395
+ const fieldToken = tokens[index];
5396
+ const valueToken = tokens[index + 1];
5397
+ const key = fieldToken.value.toLowerCase();
5398
+ if (seen.has(key)) {
5399
+ throw new WorldOrbitError(`Duplicate viewpoint camera field "${fieldToken.value}"`, line, fieldToken.column);
5400
+ }
5401
+ seen.add(key);
5402
+ const value = valueToken.value;
5403
+ switch (key) {
5404
+ case "azimuth":
5405
+ camera.azimuth = parseFiniteNumber2(value, line, fieldToken.column, "camera.azimuth");
5406
+ break;
5407
+ case "elevation":
5408
+ camera.elevation = parseFiniteNumber2(value, line, fieldToken.column, "camera.elevation");
5409
+ break;
5410
+ case "roll":
5411
+ camera.roll = parseFiniteNumber2(value, line, fieldToken.column, "camera.roll");
5412
+ break;
5413
+ case "distance":
5414
+ camera.distance = parsePositiveNumber2(value, line, fieldToken.column, "camera.distance");
5415
+ break;
5416
+ default:
5417
+ throw new WorldOrbitError(`Unknown viewpoint camera field "${fieldToken.value}"`, line, fieldToken.column);
5418
+ }
5419
+ }
5420
+ return camera;
5421
+ }
5015
5422
  function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
5016
5423
  const fields = [];
5017
5424
  let index = 0;
@@ -5144,7 +5551,7 @@
5144
5551
  object.tolerances = tolerances;
5145
5552
  if (typedBlocks && Object.keys(typedBlocks).length > 0)
5146
5553
  object.typedBlocks = typedBlocks;
5147
- if (sourceSchemaVersion !== "2.1") {
5554
+ if (isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
5148
5555
  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) {
5149
5556
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
5150
5557
  }
@@ -5160,23 +5567,25 @@
5160
5567
  };
5161
5568
  }
5162
5569
  function normalizeDraftEventPose(rawPose) {
5163
- const fieldMap = collectDraftFields(rawPose.fields);
5570
+ const fieldMap = collectDraftFields(rawPose.fields, "event-pose");
5164
5571
  const placement = extractPlacementFromFieldMap(fieldMap);
5165
5572
  return {
5166
5573
  objectId: rawPose.objectId,
5167
5574
  placement,
5168
5575
  inner: parseOptionalUnitField(fieldMap.get("inner")?.[0], "inner"),
5169
- outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer")
5576
+ outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer"),
5577
+ epoch: parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]),
5578
+ referencePlane: parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0])
5170
5579
  };
5171
5580
  }
5172
- function collectDraftFields(fields) {
5581
+ function collectDraftFields(fields, _mode = "object") {
5173
5582
  const grouped = /* @__PURE__ */ new Map();
5174
5583
  for (const field of fields) {
5175
5584
  const spec = getDraftObjectFieldSpec(field.key);
5176
- if (!spec) {
5585
+ if (!spec && !EVENT_POSE_FIELD_KEYS.has(field.key)) {
5177
5586
  throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
5178
5587
  }
5179
- if (!spec.allowRepeat && grouped.has(field.key)) {
5588
+ if (!spec?.allowRepeat && grouped.has(field.key)) {
5180
5589
  throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
5181
5590
  }
5182
5591
  const existing = grouped.get(field.key) ?? [];
@@ -5353,7 +5762,7 @@
5353
5762
  }
5354
5763
  }
5355
5764
  function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
5356
- if (sourceSchemaVersion === "2.1") {
5765
+ if (!isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
5357
5766
  return;
5358
5767
  }
5359
5768
  diagnostics.push({
@@ -5365,6 +5774,34 @@
5365
5774
  column: location.column
5366
5775
  });
5367
5776
  }
5777
+ function warnIfSchema25Feature(sourceSchemaVersion, diagnostics, featureName, location) {
5778
+ if (!isSchemaOlderThan(sourceSchemaVersion, "2.5")) {
5779
+ return;
5780
+ }
5781
+ diagnostics.push({
5782
+ code: "parse.schema25.featureCompatibility",
5783
+ severity: "warning",
5784
+ source: "parse",
5785
+ message: `Feature "${featureName}" requires schema 2.5; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
5786
+ line: location.line,
5787
+ column: location.column
5788
+ });
5789
+ }
5790
+ function isSchemaOlderThan(sourceSchemaVersion, requiredVersion) {
5791
+ return schemaVersionRank(sourceSchemaVersion) < schemaVersionRank(requiredVersion);
5792
+ }
5793
+ function schemaVersionRank(version) {
5794
+ switch (version) {
5795
+ case "2.0-draft":
5796
+ return 0;
5797
+ case "2.0":
5798
+ return 1;
5799
+ case "2.1":
5800
+ return 2;
5801
+ case "2.5":
5802
+ return 3;
5803
+ }
5804
+ }
5368
5805
  function preprocessAtlasSource(source) {
5369
5806
  const chars = [...source];
5370
5807
  const comments = [];
@@ -5452,7 +5889,7 @@
5452
5889
  }
5453
5890
 
5454
5891
  // packages/core/dist/atlas-edit.js
5455
- function createEmptyAtlasDocument(systemId = "WorldOrbit", version = "2.0") {
5892
+ function createEmptyAtlasDocument(systemId = "WorldOrbit", version = "2.5") {
5456
5893
  return {
5457
5894
  format: "worldorbit",
5458
5895
  version,
@@ -5671,8 +6108,9 @@
5671
6108
  }
5672
6109
 
5673
6110
  // packages/core/dist/load.js
5674
- var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1)?$/i;
6111
+ var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1|\.5)?$/i;
5675
6112
  var ATLAS_SCHEMA_21_PATTERN = /^schema\s+2\.1$/i;
6113
+ var ATLAS_SCHEMA_25_PATTERN = /^schema\s+2\.5$/i;
5676
6114
  var LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
5677
6115
  function detectWorldOrbitSchemaVersion(source) {
5678
6116
  for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
@@ -5686,6 +6124,9 @@
5686
6124
  if (ATLAS_SCHEMA_21_PATTERN.test(trimmed)) {
5687
6125
  return "2.1";
5688
6126
  }
6127
+ if (ATLAS_SCHEMA_25_PATTERN.test(trimmed)) {
6128
+ return "2.5";
6129
+ }
5689
6130
  if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
5690
6131
  return "2.0";
5691
6132
  }
@@ -5746,7 +6187,7 @@
5746
6187
  }
5747
6188
  function loadWorldOrbitSourceWithDiagnostics(source) {
5748
6189
  const schemaVersion = detectWorldOrbitSchemaVersion(source);
5749
- if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1") {
6190
+ if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1" || schemaVersion === "2.5") {
5750
6191
  return loadAtlasSourceWithDiagnostics(source, schemaVersion);
5751
6192
  }
5752
6193
  let ast;
@@ -5987,13 +6428,14 @@
5987
6428
  }
5988
6429
  function createAtlasStateSnapshot(viewerState, renderOptions, filter, viewpointId) {
5989
6430
  return {
5990
- version: "2.0",
6431
+ version: "2.5",
5991
6432
  viewpointId,
5992
6433
  activeEventId: renderOptions.activeEventId ?? null,
5993
6434
  viewerState: { ...viewerState },
5994
6435
  renderOptions: {
5995
6436
  preset: renderOptions.preset,
5996
6437
  projection: renderOptions.projection,
6438
+ camera: renderOptions.camera ? { ...renderOptions.camera } : null,
5997
6439
  layers: renderOptions.layers ? { ...renderOptions.layers } : void 0,
5998
6440
  scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0,
5999
6441
  activeEventId: renderOptions.activeEventId ?? null
@@ -6007,7 +6449,7 @@
6007
6449
  function deserializeViewerAtlasState(serialized) {
6008
6450
  const raw = JSON.parse(decodeURIComponent(serialized));
6009
6451
  return {
6010
- version: "2.0",
6452
+ version: raw.version === "2.0" ? "2.0" : "2.5",
6011
6453
  viewpointId: raw.viewpointId ?? null,
6012
6454
  activeEventId: raw.activeEventId ?? raw.renderOptions?.activeEventId ?? null,
6013
6455
  viewerState: {
@@ -6020,6 +6462,7 @@
6020
6462
  renderOptions: {
6021
6463
  preset: raw.renderOptions?.preset,
6022
6464
  projection: raw.renderOptions?.projection,
6465
+ camera: raw.renderOptions?.camera ? { ...raw.renderOptions.camera } : null,
6023
6466
  layers: raw.renderOptions?.layers ? { ...raw.renderOptions.layers } : void 0,
6024
6467
  scaleModel: raw.renderOptions?.scaleModel ? { ...raw.renderOptions.scaleModel } : void 0,
6025
6468
  activeEventId: raw.activeEventId ?? raw.renderOptions?.activeEventId ?? null
@@ -6037,6 +6480,7 @@
6037
6480
  viewerState: { ...atlasState.viewerState },
6038
6481
  renderOptions: {
6039
6482
  ...atlasState.renderOptions,
6483
+ camera: atlasState.renderOptions.camera ? { ...atlasState.renderOptions.camera } : null,
6040
6484
  layers: atlasState.renderOptions.layers ? { ...atlasState.renderOptions.layers } : void 0,
6041
6485
  scaleModel: atlasState.renderOptions.scaleModel ? { ...atlasState.renderOptions.scaleModel } : void 0,
6042
6486
  activeEventId: atlasState.renderOptions.activeEventId ?? null
@@ -7132,6 +7576,7 @@
7132
7576
  padding: options.padding,
7133
7577
  preset: options.preset,
7134
7578
  projection: options.projection,
7579
+ camera: options.camera ? { ...options.camera } : null,
7135
7580
  scaleModel: options.scaleModel ? { ...options.scaleModel } : void 0,
7136
7581
  theme: options.theme,
7137
7582
  layers: options.layers,
@@ -7420,6 +7865,11 @@
7420
7865
  if (currentInput.kind !== "scene" && viewpoint.projection !== scene.projection) {
7421
7866
  nextRenderOptions.projection = viewpoint.projection;
7422
7867
  }
7868
+ if (viewpoint.camera) {
7869
+ nextRenderOptions.camera = { ...viewpoint.camera };
7870
+ } else if (renderOptions.camera) {
7871
+ nextRenderOptions.camera = null;
7872
+ }
7423
7873
  if (viewpointLayers) {
7424
7874
  nextRenderOptions.layers = viewpointLayers;
7425
7875
  }
@@ -8026,6 +8476,7 @@
8026
8476
  function cloneRenderOptions(renderOptions) {
8027
8477
  return {
8028
8478
  ...renderOptions,
8479
+ camera: renderOptions.camera ? { ...renderOptions.camera } : null,
8029
8480
  filter: renderOptions.filter ? { ...renderOptions.filter } : void 0,
8030
8481
  scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0,
8031
8482
  layers: renderOptions.layers ? { ...renderOptions.layers } : void 0,
@@ -8037,6 +8488,7 @@
8037
8488
  return {
8038
8489
  ...current,
8039
8490
  ...next,
8491
+ camera: next.camera !== void 0 ? next.camera ? { ...next.camera } : null : current.camera ? { ...current.camera } : null,
8040
8492
  filter: next.filter !== void 0 ? normalizeViewerFilter(next.filter) : current.filter ? { ...current.filter } : void 0,
8041
8493
  scaleModel: next.scaleModel ? {
8042
8494
  ...current.scaleModel ?? {},
@@ -8050,7 +8502,7 @@
8050
8502
  };
8051
8503
  }
8052
8504
  function hasSceneAffectingRenderOptions(options) {
8053
- 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;
8505
+ return options.width !== void 0 || options.height !== void 0 || options.padding !== void 0 || options.preset !== void 0 || options.projection !== void 0 || options.camera !== void 0 || options.scaleModel !== void 0 || options.activeEventId !== void 0;
8054
8506
  }
8055
8507
  function resolveSourceRenderOptions(loaded, renderOptions) {
8056
8508
  const atlasDocument = loaded.atlasDocument ?? loaded.draftDocument;
@@ -8361,7 +8813,11 @@
8361
8813
  var FIELD_HELP = {
8362
8814
  "defaults-view": {
8363
8815
  description: "Sets the default camera projection for the atlas.",
8364
- references: ["Topdown = map-like", "Isometric = angled overview"]
8816
+ references: [
8817
+ "Topdown = map-like",
8818
+ "Isometric = angled overview",
8819
+ "Orthographic/Perspective = 3D-ready semantic views"
8820
+ ]
8365
8821
  },
8366
8822
  "defaults-scale": {
8367
8823
  description: "Chooses the overall spacing/style preset used by the renderer.",
@@ -8373,15 +8829,35 @@
8373
8829
  },
8374
8830
  "viewpoint-projection": {
8375
8831
  description: "Overrides the projection for this saved viewpoint.",
8376
- references: ["Topdown = flat orbital map", "Isometric = angled scene"]
8832
+ references: [
8833
+ "Topdown = flat orbital map",
8834
+ "Isometric = angled scene",
8835
+ "Orthographic/Perspective = stored with current 2D fallback"
8836
+ ]
8377
8837
  },
8378
8838
  "viewpoint-zoom": {
8379
8839
  description: "Controls how closely this viewpoint frames the system.",
8380
8840
  references: ["1 = scene fit", "2+ = close-up"]
8381
8841
  },
8382
8842
  "viewpoint-rotation": {
8383
- description: "Rotates the saved camera angle in degrees.",
8384
- references: ["90deg = quarter turn", "180deg = flip"]
8843
+ description: "Legacy 2D screen rotation. This is separate from the Schema 2.5 camera block.",
8844
+ references: ["90deg = quarter turn", "Use camera.azimuth for semantic view direction"]
8845
+ },
8846
+ "viewpoint-camera-azimuth": {
8847
+ description: "Horizontal camera direction in degrees for Schema 2.5 viewpoints.",
8848
+ references: ["0 = forward/default", "90 = quarter orbit around the scene"]
8849
+ },
8850
+ "viewpoint-camera-elevation": {
8851
+ description: "Vertical camera tilt in degrees for 3D-ready viewpoints.",
8852
+ references: ["0 = level", "30 = gentle look down"]
8853
+ },
8854
+ "viewpoint-camera-roll": {
8855
+ description: "Rolls the camera around its forward axis.",
8856
+ references: ["0 = upright", "15 = slight bank"]
8857
+ },
8858
+ "viewpoint-camera-distance": {
8859
+ description: "Semantic camera distance for perspective viewpoints.",
8860
+ references: ["4 = close", "12 = wide framing"]
8385
8861
  },
8386
8862
  "viewpoint-events": {
8387
8863
  description: "Lists event IDs that this viewpoint should feature in its detail panel.",
@@ -8407,6 +8883,14 @@
8407
8883
  description: "Notes where or how the event is visible.",
8408
8884
  references: ['"Visible from Naar"', '"Southern hemisphere only"']
8409
8885
  },
8886
+ "event-epoch": {
8887
+ description: "Optional event-wide epoch that event poses inherit unless they override it.",
8888
+ references: ['"JY-0001.0"', '"Naar bloom cycle year 18"']
8889
+ },
8890
+ "event-referencePlane": {
8891
+ description: "Optional event-wide reference plane for all poses in this snapshot.",
8892
+ references: ["ecliptic", "naar-equatorial"]
8893
+ },
8410
8894
  "event-viewpoints": {
8411
8895
  description: "Viewpoint IDs that should list this event prominently.",
8412
8896
  references: ["naar-system", "overview inner-system"]
@@ -8447,6 +8931,14 @@
8447
8931
  description: "Starting position of the object along its orbit.",
8448
8932
  references: ["0deg = start position", "180deg = opposite side"]
8449
8933
  },
8934
+ "pose-epoch": {
8935
+ description: "Overrides the effective epoch for this pose only.",
8936
+ references: ['"JY-0001.0"', "Falls back to event, object, then system"]
8937
+ },
8938
+ "pose-referencePlane": {
8939
+ description: "Overrides the effective reference plane for this pose only.",
8940
+ references: ["naar-equatorial", "Falls back to event, object, then system"]
8941
+ },
8450
8942
  "prop-radius": {
8451
8943
  description: "Visual body size or real-world-inspired radius value.",
8452
8944
  references: ["1re = Earth radius", "1sol = Sun radius"]
@@ -8641,6 +9133,8 @@
8641
9133
  participantObjectIds: [],
8642
9134
  timing: null,
8643
9135
  visibility: null,
9136
+ epoch: null,
9137
+ referencePlane: null,
8644
9138
  tags: [],
8645
9139
  color: null,
8646
9140
  hidden: false,
@@ -8665,6 +9159,7 @@
8665
9159
  preset: atlasDocument.system?.defaults.preset ?? null,
8666
9160
  zoom: null,
8667
9161
  rotationDeg: 0,
9162
+ camera: null,
8668
9163
  layers: {},
8669
9164
  filter: null
8670
9165
  };
@@ -9556,6 +10051,7 @@
9556
10051
  preset: readOptionalTextInput(form, "viewpoint-preset") ?? null,
9557
10052
  zoom: parseNullableNumber(readOptionalTextInput(form, "viewpoint-zoom")),
9558
10053
  rotationDeg: parseNullableNumber(readOptionalTextInput(form, "viewpoint-rotation")) ?? 0,
10054
+ camera: buildViewCameraFromForm(form),
9559
10055
  layers: {
9560
10056
  background: readCheckbox(form, "layer-background"),
9561
10057
  guides: readCheckbox(form, "layer-guides"),
@@ -9598,6 +10094,8 @@
9598
10094
  participantObjectIds: splitTokens(readOptionalTextInput(form, "event-participants")),
9599
10095
  timing: readOptionalTextInput(form, "event-timing"),
9600
10096
  visibility: readOptionalTextInput(form, "event-visibility"),
10097
+ epoch: readOptionalTextInput(form, "event-epoch"),
10098
+ referencePlane: readOptionalTextInput(form, "event-referencePlane"),
9601
10099
  tags: splitTokens(readOptionalTextInput(form, "event-tags")),
9602
10100
  color: readOptionalTextInput(form, "event-color"),
9603
10101
  hidden: readCheckbox(form, "event-hidden")
@@ -9620,7 +10118,9 @@
9620
10118
  const nextObjectId = readTextInput(form, "pose-object-id") || currentPose.objectId;
9621
10119
  const replacement = {
9622
10120
  objectId: nextObjectId,
9623
- placement: buildPlacementFromPoseForm(form, currentPose)
10121
+ placement: buildPlacementFromPoseForm(form, currentPose),
10122
+ epoch: readOptionalTextInput(form, "pose-epoch"),
10123
+ referencePlane: readOptionalTextInput(form, "pose-referencePlane")
9624
10124
  };
9625
10125
  const inner = parseOptionalUnit(readOptionalTextInput(form, "prop-inner"));
9626
10126
  const outer = parseOptionalUnit(readOptionalTextInput(form, "prop-outer"));
@@ -9990,7 +10490,9 @@
9990
10490
  <h2>Defaults</h2>
9991
10491
  ${renderInspectorSection("defaults", "basics", "Basics", `${renderSelectField("Projection", "defaults-view", [
9992
10492
  ["topdown", "Topdown"],
9993
- ["isometric", "Isometric"]
10493
+ ["isometric", "Isometric"],
10494
+ ["orthographic", "Orthographic"],
10495
+ ["perspective", "Perspective"]
9994
10496
  ], defaults?.view ?? "topdown")}
9995
10497
  ${renderTextField("Scale preset", "defaults-scale", defaults?.scale ?? "")}
9996
10498
  ${renderTextField("Units", "defaults-units", defaults?.units ?? "")}
@@ -10026,7 +10528,9 @@
10026
10528
  ${renderTextField("Selected object", "viewpoint-select", viewpoint.selectedObjectId ?? "")}
10027
10529
  ${renderSelectField("Projection", "viewpoint-projection", [
10028
10530
  ["topdown", "Topdown"],
10029
- ["isometric", "Isometric"]
10531
+ ["isometric", "Isometric"],
10532
+ ["orthographic", "Orthographic"],
10533
+ ["perspective", "Perspective"]
10030
10534
  ], viewpoint.projection)}
10031
10535
  ${renderSelectField("Preset", "viewpoint-preset", [
10032
10536
  ["", "Document default"],
@@ -10037,6 +10541,11 @@
10037
10541
  ], viewpoint.preset ?? "")}
10038
10542
  ${renderTextField("Zoom", "viewpoint-zoom", viewpoint.zoom === null ? "" : String(viewpoint.zoom))}
10039
10543
  ${renderTextField("Rotation", "viewpoint-rotation", String(viewpoint.rotationDeg))}`, true)}
10544
+ ${renderInspectorSection("viewpoint", "camera", "Camera", `${renderTextField("Azimuth", "viewpoint-camera-azimuth", viewpoint.camera?.azimuth === null || viewpoint.camera?.azimuth === void 0 ? "" : String(viewpoint.camera.azimuth))}
10545
+ ${renderTextField("Elevation", "viewpoint-camera-elevation", viewpoint.camera?.elevation === null || viewpoint.camera?.elevation === void 0 ? "" : String(viewpoint.camera.elevation))}
10546
+ ${renderTextField("Roll", "viewpoint-camera-roll", viewpoint.camera?.roll === null || viewpoint.camera?.roll === void 0 ? "" : String(viewpoint.camera.roll))}
10547
+ ${renderTextField("Distance", "viewpoint-camera-distance", viewpoint.camera?.distance === null || viewpoint.camera?.distance === void 0 ? "" : String(viewpoint.camera.distance))}
10548
+ <p class="wo-editor-inline-note">Rotation stays a 2D screen-rotation hint. The camera block stores Schema 2.5 view direction and framing.</p>`)}
10040
10549
  ${renderInspectorSection("viewpoint", "layers", "Layers", `<fieldset class="wo-editor-fieldset">
10041
10550
  <legend>Layers</legend>
10042
10551
  ${renderCheckboxField("Background", "layer-background", viewpoint.layers.background !== false)}
@@ -10071,6 +10580,8 @@
10071
10580
  ${renderTextField("Participants", "event-participants", eventEntry.participantObjectIds.join(" "))}
10072
10581
  ${renderTextField("Timing", "event-timing", eventEntry.timing ?? "")}
10073
10582
  ${renderTextField("Visibility", "event-visibility", eventEntry.visibility ?? "")}
10583
+ ${renderTextField("Epoch", "event-epoch", eventEntry.epoch ?? "")}
10584
+ ${renderTextField("Reference plane", "event-referencePlane", eventEntry.referencePlane ?? "")}
10074
10585
  ${renderTextField("Tags", "event-tags", eventEntry.tags.join(" "))}
10075
10586
  ${renderTextField("Color", "event-color", eventEntry.color ?? "")}
10076
10587
  ${renderCheckboxField("Hidden", "event-hidden", eventEntry.hidden === true)}`, true)}
@@ -10115,6 +10626,9 @@
10115
10626
  ${renderTextField("Phase", "placement-phase", pose.placement?.mode === "orbit" && pose.placement.phase ? formatUnitValue3(pose.placement.phase) : "")}
10116
10627
  ${renderTextField("Inner", "prop-inner", pose.inner ? formatUnitValue3(pose.inner) : "")}
10117
10628
  ${renderTextField("Outer", "prop-outer", pose.outer ? formatUnitValue3(pose.outer) : "")}`, true)}
10629
+ ${renderInspectorSection("event-pose", "context", "Context", `${renderTextField("Epoch", "pose-epoch", pose.epoch ?? "")}
10630
+ ${renderTextField("Reference plane", "pose-referencePlane", pose.referencePlane ?? "")}
10631
+ <p class="wo-editor-inline-note">Falls back to event, then object, then system context when left empty.</p>`)}
10118
10632
  </form>`;
10119
10633
  }
10120
10634
  function renderAnnotationInspector(formState, id) {
@@ -10299,6 +10813,15 @@
10299
10813
  const parsed = Number(value);
10300
10814
  return Number.isFinite(parsed) ? parsed : null;
10301
10815
  }
10816
+ function buildViewCameraFromForm(form) {
10817
+ const camera = {
10818
+ azimuth: parseNullableNumber(readOptionalTextInput(form, "viewpoint-camera-azimuth")),
10819
+ elevation: parseNullableNumber(readOptionalTextInput(form, "viewpoint-camera-elevation")),
10820
+ roll: parseNullableNumber(readOptionalTextInput(form, "viewpoint-camera-roll")),
10821
+ distance: parseNullableNumber(readOptionalTextInput(form, "viewpoint-camera-distance"))
10822
+ };
10823
+ return camera.azimuth !== null || camera.elevation !== null || camera.roll !== null || camera.distance !== null ? camera : null;
10824
+ }
10302
10825
  function parseObjectTypes(value) {
10303
10826
  const tokens = splitTokens(value);
10304
10827
  return tokens.filter((token) => OBJECT_TYPES.includes(token));
@@ -10881,6 +11404,21 @@
10881
11404
  return ["viewpoint-zoom"];
10882
11405
  case "rotationDeg":
10883
11406
  return ["viewpoint-rotation"];
11407
+ case "camera":
11408
+ return [
11409
+ "viewpoint-camera-azimuth",
11410
+ "viewpoint-camera-elevation",
11411
+ "viewpoint-camera-roll",
11412
+ "viewpoint-camera-distance"
11413
+ ];
11414
+ case "camera.azimuth":
11415
+ return ["viewpoint-camera-azimuth"];
11416
+ case "camera.elevation":
11417
+ return ["viewpoint-camera-elevation"];
11418
+ case "camera.roll":
11419
+ return ["viewpoint-camera-roll"];
11420
+ case "camera.distance":
11421
+ return ["viewpoint-camera-distance"];
10884
11422
  case "events":
10885
11423
  return ["viewpoint-events"];
10886
11424
  default:
@@ -10906,6 +11444,10 @@
10906
11444
  return ["event-timing"];
10907
11445
  case "visibility":
10908
11446
  return ["event-visibility"];
11447
+ case "epoch":
11448
+ return ["event-epoch"];
11449
+ case "referencePlane":
11450
+ return ["event-referencePlane"];
10909
11451
  case "tags":
10910
11452
  return ["event-tags"];
10911
11453
  case "color":
@@ -10934,6 +11476,12 @@
10934
11476
  if (field === "inner" || field === "outer") {
10935
11477
  return [`prop-${field}`];
10936
11478
  }
11479
+ if (field === "epoch") {
11480
+ return ["pose-epoch"];
11481
+ }
11482
+ if (field === "referencePlane") {
11483
+ return ["pose-referencePlane"];
11484
+ }
10937
11485
  return [];
10938
11486
  case "annotation":
10939
11487
  switch (field) {