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.
@@ -155,13 +155,14 @@
155
155
  }
156
156
  function createAtlasStateSnapshot(viewerState, renderOptions, filter, viewpointId) {
157
157
  return {
158
- version: "2.0",
158
+ version: "2.5",
159
159
  viewpointId,
160
160
  activeEventId: renderOptions.activeEventId ?? null,
161
161
  viewerState: { ...viewerState },
162
162
  renderOptions: {
163
163
  preset: renderOptions.preset,
164
164
  projection: renderOptions.projection,
165
+ camera: renderOptions.camera ? { ...renderOptions.camera } : null,
165
166
  layers: renderOptions.layers ? { ...renderOptions.layers } : void 0,
166
167
  scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0,
167
168
  activeEventId: renderOptions.activeEventId ?? null
@@ -175,7 +176,7 @@
175
176
  function deserializeViewerAtlasState(serialized) {
176
177
  const raw = JSON.parse(decodeURIComponent(serialized));
177
178
  return {
178
- version: "2.0",
179
+ version: raw.version === "2.0" ? "2.0" : "2.5",
179
180
  viewpointId: raw.viewpointId ?? null,
180
181
  activeEventId: raw.activeEventId ?? raw.renderOptions?.activeEventId ?? null,
181
182
  viewerState: {
@@ -188,6 +189,7 @@
188
189
  renderOptions: {
189
190
  preset: raw.renderOptions?.preset,
190
191
  projection: raw.renderOptions?.projection,
192
+ camera: raw.renderOptions?.camera ? { ...raw.renderOptions.camera } : null,
191
193
  layers: raw.renderOptions?.layers ? { ...raw.renderOptions.layers } : void 0,
192
194
  scaleModel: raw.renderOptions?.scaleModel ? { ...raw.renderOptions.scaleModel } : void 0,
193
195
  activeEventId: raw.activeEventId ?? raw.renderOptions?.activeEventId ?? null
@@ -205,6 +207,7 @@
205
207
  viewerState: { ...atlasState.viewerState },
206
208
  renderOptions: {
207
209
  ...atlasState.renderOptions,
210
+ camera: atlasState.renderOptions.camera ? { ...atlasState.renderOptions.camera } : null,
208
211
  layers: atlasState.renderOptions.layers ? { ...atlasState.renderOptions.layers } : void 0,
209
212
  scaleModel: atlasState.renderOptions.scaleModel ? { ...atlasState.renderOptions.scaleModel } : void 0,
210
213
  activeEventId: atlasState.renderOptions.activeEventId ?? null
@@ -1253,7 +1256,9 @@
1253
1256
  const height = frame.height;
1254
1257
  const padding = frame.padding;
1255
1258
  const layoutPreset = resolveLayoutPreset(document2);
1256
- const projection = resolveProjection(document2, options.projection);
1259
+ const schemaProjection = resolveProjection(document2, options.projection);
1260
+ const camera = normalizeViewCamera(options.camera ?? null);
1261
+ const renderProjection = resolveRenderProjection(schemaProjection, camera);
1257
1262
  const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
1258
1263
  const spacingFactor = layoutPresetSpacing(layoutPreset);
1259
1264
  const systemId = document2.system?.id ?? null;
@@ -1296,7 +1301,7 @@
1296
1301
  surfaceChildren,
1297
1302
  objectMap,
1298
1303
  spacingFactor,
1299
- projection,
1304
+ projection: renderProjection,
1300
1305
  scaleModel
1301
1306
  };
1302
1307
  const primaryRoot = rootObjects.find((object) => object.type === "star") ?? rootObjects[0] ?? null;
@@ -1308,7 +1313,7 @@
1308
1313
  const rootRingRadius = Math.min(width, height) * 0.28 * spacingFactor * scaleModel.orbitDistanceMultiplier;
1309
1314
  secondaryRoots.forEach((object, index) => {
1310
1315
  const angle = angleForIndex(index, secondaryRoots.length, -Math.PI / 2);
1311
- const offset = projectPolarOffset(angle, rootRingRadius, projection, 1);
1316
+ const offset = projectPolarOffset(angle, rootRingRadius, renderProjection, 1);
1312
1317
  placeObject(object, centerX + offset.x, centerY + offset.y, 0, positions, orbitDrafts, leaderDrafts, context);
1313
1318
  });
1314
1319
  }
@@ -1370,27 +1375,34 @@
1370
1375
  const layers = createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels);
1371
1376
  const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, scaleModel.labelMultiplier);
1372
1377
  const semanticGroups = createSceneSemanticGroups(document2, objects);
1373
- const viewpoints = createSceneViewpoints(document2, projection, frame.preset, relationships, objectMap);
1378
+ const viewpoints = createSceneViewpoints(document2, schemaProjection, frame.preset, relationships, objectMap);
1374
1379
  const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, scaleModel.labelMultiplier);
1375
1380
  return {
1376
1381
  width,
1377
1382
  height,
1378
1383
  padding,
1379
1384
  renderPreset: frame.preset,
1380
- projection,
1385
+ projection: schemaProjection,
1386
+ renderProjection,
1387
+ camera,
1381
1388
  scaleModel,
1382
1389
  title: String(document2.system?.title ?? document2.system?.properties.title ?? document2.system?.id ?? "WorldOrbit") || "WorldOrbit",
1383
- subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
1390
+ subtitle: buildSceneSubtitle(schemaProjection, renderProjection, layoutPreset, camera),
1384
1391
  systemId,
1385
- viewMode: projection,
1392
+ viewMode: schemaProjection,
1386
1393
  layoutPreset,
1387
1394
  metadata: {
1388
1395
  format: document2.format,
1389
1396
  version: document2.version,
1390
- view: projection,
1397
+ view: schemaProjection,
1398
+ renderProjection,
1391
1399
  scale: String(document2.system?.properties.scale ?? layoutPreset),
1392
1400
  units: String(document2.system?.properties.units ?? "mixed"),
1393
- preset: frame.preset ?? "custom"
1401
+ preset: frame.preset ?? "custom",
1402
+ ...camera?.azimuth !== null ? { "camera.azimuth": String(camera?.azimuth) } : {},
1403
+ ...camera?.elevation !== null ? { "camera.elevation": String(camera?.elevation) } : {},
1404
+ ...camera?.roll !== null ? { "camera.roll": String(camera?.roll) } : {},
1405
+ ...camera?.distance !== null ? { "camera.distance": String(camera?.distance) } : {}
1394
1406
  },
1395
1407
  contentBounds,
1396
1408
  layers,
@@ -1427,21 +1439,42 @@
1427
1439
  return cloned;
1428
1440
  }
1429
1441
  const objectMap = new Map(cloned.map((object) => [object.id, object]));
1442
+ const referencedIds = /* @__PURE__ */ new Set([
1443
+ ...activeEvent.targetObjectId ? [activeEvent.targetObjectId] : [],
1444
+ ...activeEvent.participantObjectIds,
1445
+ ...activeEvent.positions.map((pose) => pose.objectId)
1446
+ ]);
1447
+ for (const objectId of referencedIds) {
1448
+ const object = objectMap.get(objectId);
1449
+ if (!object) {
1450
+ continue;
1451
+ }
1452
+ if (activeEvent.epoch) {
1453
+ object.epoch = activeEvent.epoch;
1454
+ }
1455
+ if (activeEvent.referencePlane) {
1456
+ object.referencePlane = activeEvent.referencePlane;
1457
+ }
1458
+ }
1430
1459
  for (const pose of activeEvent.positions) {
1431
1460
  const object = objectMap.get(pose.objectId);
1432
1461
  if (!object) {
1433
1462
  continue;
1434
1463
  }
1435
- object.placement = pose.placement ? structuredClone(pose.placement) : null;
1464
+ if (pose.placement) {
1465
+ object.placement = structuredClone(pose.placement);
1466
+ }
1436
1467
  if (pose.inner) {
1437
1468
  object.properties.inner = { ...pose.inner };
1438
- } else {
1439
- delete object.properties.inner;
1440
1469
  }
1441
1470
  if (pose.outer) {
1442
1471
  object.properties.outer = { ...pose.outer };
1443
- } else {
1444
- delete object.properties.outer;
1472
+ }
1473
+ if (pose.epoch) {
1474
+ object.epoch = pose.epoch;
1475
+ }
1476
+ if (pose.referencePlane) {
1477
+ object.referencePlane = pose.referencePlane;
1445
1478
  }
1446
1479
  }
1447
1480
  return cloned;
@@ -1482,10 +1515,59 @@
1482
1515
  }
1483
1516
  }
1484
1517
  function resolveProjection(document2, projection) {
1485
- if (projection === "topdown" || projection === "isometric") {
1518
+ if (projection === "topdown" || projection === "isometric" || projection === "orthographic" || projection === "perspective") {
1486
1519
  return projection;
1487
1520
  }
1488
- return String(document2.system?.properties.view ?? "topdown").toLowerCase() === "isometric" ? "isometric" : "topdown";
1521
+ const documentView = String(document2.system?.properties.view ?? "topdown").toLowerCase();
1522
+ return parseViewProjection(documentView) ?? "topdown";
1523
+ }
1524
+ function resolveRenderProjection(projection, camera) {
1525
+ switch (projection) {
1526
+ case "topdown":
1527
+ return "topdown";
1528
+ case "isometric":
1529
+ return "isometric";
1530
+ case "orthographic":
1531
+ return camera && (camera.azimuth !== null || camera.elevation !== null || camera.roll !== null) ? "isometric" : "topdown";
1532
+ case "perspective":
1533
+ return "isometric";
1534
+ }
1535
+ }
1536
+ function normalizeViewCamera(camera) {
1537
+ if (!camera) {
1538
+ return null;
1539
+ }
1540
+ const normalized = {
1541
+ azimuth: normalizeFiniteCameraValue(camera.azimuth),
1542
+ elevation: normalizeFiniteCameraValue(camera.elevation),
1543
+ roll: normalizeFiniteCameraValue(camera.roll),
1544
+ distance: normalizePositiveCameraDistance(camera.distance)
1545
+ };
1546
+ return normalized.azimuth !== null || normalized.elevation !== null || normalized.roll !== null || normalized.distance !== null ? normalized : null;
1547
+ }
1548
+ function normalizeFiniteCameraValue(value) {
1549
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
1550
+ }
1551
+ function normalizePositiveCameraDistance(value) {
1552
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : null;
1553
+ }
1554
+ function buildSceneSubtitle(projection, renderProjection, layoutPreset, camera) {
1555
+ const parts = [`${capitalizeLabel(projection)} view`, `${capitalizeLabel(layoutPreset)} layout`];
1556
+ if (projection !== renderProjection) {
1557
+ parts.push(`2D ${renderProjection} fallback`);
1558
+ }
1559
+ if (camera) {
1560
+ const cameraParts = [
1561
+ camera.azimuth !== null ? `az ${camera.azimuth}` : null,
1562
+ camera.elevation !== null ? `el ${camera.elevation}` : null,
1563
+ camera.roll !== null ? `roll ${camera.roll}` : null,
1564
+ camera.distance !== null ? `dist ${camera.distance}` : null
1565
+ ].filter(Boolean);
1566
+ if (cameraParts.length > 0) {
1567
+ parts.push(`camera ${cameraParts.join(" / ")}`);
1568
+ }
1569
+ }
1570
+ return parts.join(" - ");
1489
1571
  }
1490
1572
  function resolveScaleModel(layoutPreset, overrides) {
1491
1573
  const defaults = defaultScaleModel(layoutPreset);
@@ -1921,6 +2003,8 @@
1921
2003
  function createGeneratedOverviewViewpoint(document2, projection, preset) {
1922
2004
  const title = document2.system?.title ?? document2.system?.properties.title;
1923
2005
  const label = title ? `${String(title)} Overview` : "Overview";
2006
+ const camera = normalizeViewCamera(null);
2007
+ const renderProjection = resolveRenderProjection(projection, camera);
1924
2008
  return {
1925
2009
  id: "overview",
1926
2010
  label,
@@ -1929,6 +2013,8 @@
1929
2013
  selectedObjectId: null,
1930
2014
  eventIds: [],
1931
2015
  projection,
2016
+ renderProjection,
2017
+ camera,
1932
2018
  preset,
1933
2019
  rotationDeg: 0,
1934
2020
  scale: null,
@@ -1978,6 +2064,30 @@
1978
2064
  case "angle":
1979
2065
  draft.rotationDeg = parseFiniteNumber(normalizedValue) ?? draft.rotationDeg ?? 0;
1980
2066
  return;
2067
+ case "camera.azimuth":
2068
+ draft.camera = {
2069
+ ...draft.camera ?? createEmptyViewCamera(),
2070
+ azimuth: parseFiniteNumber(normalizedValue)
2071
+ };
2072
+ return;
2073
+ case "camera.elevation":
2074
+ draft.camera = {
2075
+ ...draft.camera ?? createEmptyViewCamera(),
2076
+ elevation: parseFiniteNumber(normalizedValue)
2077
+ };
2078
+ return;
2079
+ case "camera.roll":
2080
+ draft.camera = {
2081
+ ...draft.camera ?? createEmptyViewCamera(),
2082
+ roll: parseFiniteNumber(normalizedValue)
2083
+ };
2084
+ return;
2085
+ case "camera.distance":
2086
+ draft.camera = {
2087
+ ...draft.camera ?? createEmptyViewCamera(),
2088
+ distance: parsePositiveNumber(normalizedValue)
2089
+ };
2090
+ return;
1981
2091
  case "zoom":
1982
2092
  case "scale":
1983
2093
  draft.scale = parsePositiveNumber(normalizedValue);
@@ -2017,6 +2127,9 @@
2017
2127
  const selectedObjectId = draft.select && objectMap.has(draft.select) ? draft.select : objectId;
2018
2128
  const filter = normalizeViewpointFilter(draft.filter);
2019
2129
  const label = draft.label?.trim() || humanizeIdentifier(draft.id);
2130
+ const resolvedProjection = draft.projection ?? projection;
2131
+ const camera = normalizeViewCamera(draft.camera ?? null);
2132
+ const renderProjection = resolveRenderProjection(resolvedProjection, camera);
2020
2133
  return {
2021
2134
  id: draft.id,
2022
2135
  label,
@@ -2024,7 +2137,9 @@
2024
2137
  objectId,
2025
2138
  selectedObjectId,
2026
2139
  eventIds: [...new Set(draft.eventIds ?? [])],
2027
- projection: draft.projection ?? projection,
2140
+ projection: resolvedProjection,
2141
+ renderProjection,
2142
+ camera,
2028
2143
  preset: draft.preset ?? preset,
2029
2144
  rotationDeg: draft.rotationDeg ?? 0,
2030
2145
  scale: draft.scale ?? null,
@@ -2041,6 +2156,14 @@
2041
2156
  groupIds: []
2042
2157
  };
2043
2158
  }
2159
+ function createEmptyViewCamera() {
2160
+ return {
2161
+ azimuth: null,
2162
+ elevation: null,
2163
+ roll: null,
2164
+ distance: null
2165
+ };
2166
+ }
2044
2167
  function normalizeViewpointFilter(filter) {
2045
2168
  if (!filter) {
2046
2169
  return null;
@@ -2054,7 +2177,18 @@
2054
2177
  return normalized.query || normalized.objectTypes.length > 0 || normalized.tags.length > 0 || normalized.groupIds.length > 0 ? normalized : null;
2055
2178
  }
2056
2179
  function parseViewProjection(value) {
2057
- return value.toLowerCase() === "isometric" ? "isometric" : value.toLowerCase() === "topdown" ? "topdown" : null;
2180
+ switch (value.toLowerCase()) {
2181
+ case "topdown":
2182
+ return "topdown";
2183
+ case "isometric":
2184
+ return "isometric";
2185
+ case "orthographic":
2186
+ return "orthographic";
2187
+ case "perspective":
2188
+ return "perspective";
2189
+ default:
2190
+ return null;
2191
+ }
2058
2192
  }
2059
2193
  function parseRenderPreset(value) {
2060
2194
  const normalized = value.toLowerCase();
@@ -2092,7 +2226,7 @@
2092
2226
  }
2093
2227
  function parseViewpointGroups(value, document2, relationships, objectMap) {
2094
2228
  return splitListValue(value).map((entry) => {
2095
- if (document2.schemaVersion === "2.1" || document2.groups.some((group) => group.id === entry)) {
2229
+ if (document2.schemaVersion === "2.1" || document2.schemaVersion === "2.5" || document2.groups.some((group) => group.id === entry)) {
2096
2230
  return entry;
2097
2231
  }
2098
2232
  if (entry.startsWith("wo-") && entry.endsWith("-group")) {
@@ -2924,7 +3058,9 @@
2924
3058
  objectId: pose.objectId,
2925
3059
  placement: clonePlacement(pose.placement),
2926
3060
  inner: pose.inner ? { ...pose.inner } : void 0,
2927
- outer: pose.outer ? { ...pose.outer } : void 0
3061
+ outer: pose.outer ? { ...pose.outer } : void 0,
3062
+ epoch: pose.epoch ?? null,
3063
+ referencePlane: pose.referencePlane ?? null
2928
3064
  };
2929
3065
  }
2930
3066
  function clonePlacement(placement) {
@@ -2939,21 +3075,42 @@
2939
3075
  return;
2940
3076
  }
2941
3077
  const objectMap = new Map(objects.map((object) => [object.id, object]));
3078
+ const referencedIds = /* @__PURE__ */ new Set([
3079
+ ...event.targetObjectId ? [event.targetObjectId] : [],
3080
+ ...event.participantObjectIds,
3081
+ ...event.positions.map((pose) => pose.objectId)
3082
+ ]);
3083
+ for (const objectId of referencedIds) {
3084
+ const object = objectMap.get(objectId);
3085
+ if (!object) {
3086
+ continue;
3087
+ }
3088
+ if (event.epoch) {
3089
+ object.epoch = event.epoch;
3090
+ }
3091
+ if (event.referencePlane) {
3092
+ object.referencePlane = event.referencePlane;
3093
+ }
3094
+ }
2942
3095
  for (const pose of event.positions) {
2943
3096
  const object = objectMap.get(pose.objectId);
2944
3097
  if (!object) {
2945
3098
  continue;
2946
3099
  }
2947
- object.placement = clonePlacement(pose.placement);
3100
+ if (pose.placement) {
3101
+ object.placement = clonePlacement(pose.placement);
3102
+ }
2948
3103
  if (pose.inner) {
2949
3104
  object.properties.inner = { ...pose.inner };
2950
- } else {
2951
- delete object.properties.inner;
2952
3105
  }
2953
3106
  if (pose.outer) {
2954
3107
  object.properties.outer = { ...pose.outer };
2955
- } else {
2956
- delete object.properties.outer;
3108
+ }
3109
+ if (pose.epoch) {
3110
+ object.epoch = pose.epoch;
3111
+ }
3112
+ if (pose.referencePlane) {
3113
+ object.referencePlane = pose.referencePlane;
2957
3114
  }
2958
3115
  }
2959
3116
  }
@@ -3029,6 +3186,18 @@
3029
3186
  if (viewpoint.rotationDeg !== 0) {
3030
3187
  info2[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
3031
3188
  }
3189
+ if (viewpoint.camera?.azimuth !== null) {
3190
+ info2[`${prefix}.camera.azimuth`] = String(viewpoint.camera?.azimuth);
3191
+ }
3192
+ if (viewpoint.camera?.elevation !== null) {
3193
+ info2[`${prefix}.camera.elevation`] = String(viewpoint.camera?.elevation);
3194
+ }
3195
+ if (viewpoint.camera?.roll !== null) {
3196
+ info2[`${prefix}.camera.roll`] = String(viewpoint.camera?.roll);
3197
+ }
3198
+ if (viewpoint.camera?.distance !== null) {
3199
+ info2[`${prefix}.camera.distance`] = String(viewpoint.camera?.distance);
3200
+ }
3032
3201
  const serializedLayers = serializeViewpointLayers(viewpoint.layers);
3033
3202
  if (serializedLayers) {
3034
3203
  info2[`${prefix}.layers`] = serializedLayers;
@@ -3272,13 +3441,13 @@
3272
3441
  validateRelation(relation, objectMap, diagnostics);
3273
3442
  }
3274
3443
  for (const viewpoint of document2.system?.viewpoints ?? []) {
3275
- validateViewpoint(viewpoint.filter, viewpoint.events ?? [], groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpoint.id);
3444
+ validateViewpoint(viewpoint, groupIds, eventIds, sourceSchemaVersion, diagnostics, objectMap);
3276
3445
  }
3277
3446
  for (const object of document2.objects) {
3278
3447
  validateObject(object, document2.system, objectMap, groupIds, diagnostics);
3279
3448
  }
3280
3449
  for (const event of document2.events) {
3281
- validateEvent(event, objectMap, diagnostics);
3450
+ validateEvent(event, document2.system, objectMap, diagnostics);
3282
3451
  }
3283
3452
  return diagnostics;
3284
3453
  }
@@ -3297,21 +3466,24 @@
3297
3466
  diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
3298
3467
  }
3299
3468
  }
3300
- function validateViewpoint(filter, eventRefs, groupIds, eventIds, sourceSchemaVersion, diagnostics, viewpointId) {
3301
- if (sourceSchemaVersion === "2.1") {
3469
+ function validateViewpoint(viewpoint, groupIds, eventIds, sourceSchemaVersion, diagnostics, objectMap) {
3470
+ const filter = viewpoint.filter;
3471
+ if (sourceSchemaVersion === "2.1" || sourceSchemaVersion === "2.5") {
3302
3472
  if (filter) {
3303
3473
  for (const groupId of filter.groupIds) {
3304
3474
  if (!groupIds.has(groupId)) {
3305
- diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.groups`));
3475
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpoint.id}".`, void 0, `viewpoint.${viewpoint.id}.groups`));
3306
3476
  }
3307
3477
  }
3308
3478
  }
3309
- for (const eventId of eventRefs) {
3479
+ for (const eventId of viewpoint.events ?? []) {
3310
3480
  if (!eventIds.has(eventId)) {
3311
- diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpointId}".`, void 0, `viewpoint.${viewpointId}.events`));
3481
+ diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpoint.id}".`, void 0, `viewpoint.${viewpoint.id}.events`));
3312
3482
  }
3313
3483
  }
3314
3484
  }
3485
+ validateProjection(viewpoint.projection, diagnostics, `viewpoint.${viewpoint.id}.projection`, viewpoint.id);
3486
+ validateCamera(viewpoint.camera, viewpoint.projection, viewpoint.rotationDeg, diagnostics, viewpoint.id, viewpoint.focusObjectId, viewpoint.selectedObjectId, filter, objectMap);
3315
3487
  }
3316
3488
  function validateObject(object, system, objectMap, groupIds, diagnostics) {
3317
3489
  const placement = object.placement;
@@ -3324,6 +3496,12 @@
3324
3496
  }
3325
3497
  }
3326
3498
  }
3499
+ if (typeof object.epoch === "string" && !object.epoch.trim()) {
3500
+ diagnostics.push(warn("validate.epoch.empty", `Object "${object.id}" defines an empty epoch string.`, object.id, "epoch"));
3501
+ }
3502
+ if (typeof object.referencePlane === "string" && !object.referencePlane.trim()) {
3503
+ diagnostics.push(warn("validate.referencePlane.empty", `Object "${object.id}" defines an empty reference plane string.`, object.id, "referencePlane"));
3504
+ }
3327
3505
  if (orbitPlacement) {
3328
3506
  if (!objectMap.has(orbitPlacement.target)) {
3329
3507
  diagnostics.push(error("validate.orbit.target.unknown", `Unknown placement target "${orbitPlacement.target}" on "${object.id}".`, object.id, "orbit"));
@@ -3395,12 +3573,18 @@
3395
3573
  }
3396
3574
  }
3397
3575
  }
3398
- function validateEvent(event, objectMap, diagnostics) {
3576
+ function validateEvent(event, system, objectMap, diagnostics) {
3399
3577
  const fieldPrefix = `event.${event.id}`;
3400
3578
  const referencedIds = /* @__PURE__ */ new Set();
3401
3579
  if (!event.kind.trim()) {
3402
3580
  diagnostics.push(error("validate.event.kind.required", `Event "${event.id}" is missing a "kind" value.`, void 0, `${fieldPrefix}.kind`));
3403
3581
  }
3582
+ if (typeof event.epoch === "string" && !event.epoch.trim()) {
3583
+ diagnostics.push(warn("validate.event.epoch.empty", `Event "${event.id}" defines an empty epoch string.`, void 0, `${fieldPrefix}.epoch`));
3584
+ }
3585
+ if (typeof event.referencePlane === "string" && !event.referencePlane.trim()) {
3586
+ diagnostics.push(warn("validate.event.referencePlane.empty", `Event "${event.id}" defines an empty reference plane string.`, void 0, `${fieldPrefix}.referencePlane`));
3587
+ }
3404
3588
  if (!event.targetObjectId && event.participantObjectIds.length === 0) {
3405
3589
  diagnostics.push(error("validate.event.references.required", `Event "${event.id}" must define a "target" or at least one participant.`, void 0, `${fieldPrefix}.participants`));
3406
3590
  }
@@ -3447,10 +3631,14 @@
3447
3631
  if (!referencedIds.has(pose.objectId)) {
3448
3632
  diagnostics.push(warn("validate.event.pose.unreferenced", `Event pose "${pose.objectId}" on "${event.id}" is not listed in target/participants.`, void 0, poseFieldPrefix));
3449
3633
  }
3450
- validateEventPose(pose, object, objectMap, diagnostics, poseFieldPrefix, event.id);
3634
+ validateEventPose(pose, object, event, system, objectMap, diagnostics, poseFieldPrefix, event.id);
3635
+ }
3636
+ const missingPoseIds = [...referencedIds].filter((objectId) => !poseIds.has(objectId));
3637
+ if (event.positions.length > 0 && missingPoseIds.length > 0) {
3638
+ diagnostics.push(warn("validate.event.positions.partial", `Event "${event.id}" leaves ${missingPoseIds.length} referenced object(s) on their base placement.`, void 0, `${fieldPrefix}.positions`));
3451
3639
  }
3452
3640
  }
3453
- function validateEventPose(pose, object, objectMap, diagnostics, fieldPrefix, eventId) {
3641
+ function validateEventPose(pose, object, event, system, objectMap, diagnostics, fieldPrefix, eventId) {
3454
3642
  const placement = pose.placement;
3455
3643
  if (!placement) {
3456
3644
  diagnostics.push(error("validate.event.pose.placement.required", `Event "${eventId}" pose "${pose.objectId}" is missing a placement mode.`, void 0, fieldPrefix));
@@ -3463,6 +3651,15 @@
3463
3651
  if (placement.distance && placement.semiMajor) {
3464
3652
  diagnostics.push(error("validate.event.pose.orbit.distanceConflict", `Event "${eventId}" pose "${pose.objectId}" cannot declare both "distance" and "semiMajor".`, void 0, `${fieldPrefix}.distance`));
3465
3653
  }
3654
+ if (placement.phase && !resolveEffectiveEpoch(system, object, event, pose)) {
3655
+ diagnostics.push(warn("validate.event.pose.phase.epochMissing", `Event "${eventId}" pose "${pose.objectId}" sets "phase" without an effective epoch.`, void 0, `${fieldPrefix}.phase`));
3656
+ }
3657
+ if (placement.inclination && !resolveEffectiveReferencePlane(system, object, event, pose)) {
3658
+ diagnostics.push(warn("validate.event.pose.inclination.referencePlaneMissing", `Event "${eventId}" pose "${pose.objectId}" sets "inclination" without an effective reference plane.`, void 0, `${fieldPrefix}.inclination`));
3659
+ }
3660
+ if (placement.period && !massInSolar(objectMap.get(placement.target)?.properties.mass)) {
3661
+ 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`));
3662
+ }
3466
3663
  return;
3467
3664
  }
3468
3665
  if (placement.mode === "surface") {
@@ -3597,6 +3794,52 @@
3597
3794
  return null;
3598
3795
  }
3599
3796
  }
3797
+ function validateProjection(projection, diagnostics, field, viewpointId) {
3798
+ if (projection !== "topdown" && projection !== "isometric" && projection !== "orthographic" && projection !== "perspective") {
3799
+ diagnostics.push(error("validate.viewpoint.projection.invalid", `Unknown projection "${String(projection)}" in viewpoint "${viewpointId}".`, void 0, field));
3800
+ }
3801
+ }
3802
+ function validateCamera(camera, projection, rotationDeg, diagnostics, viewpointId, focusObjectId, selectedObjectId, filter, objectMap) {
3803
+ if (!camera) {
3804
+ return;
3805
+ }
3806
+ const prefix = `viewpoint.${viewpointId}.camera`;
3807
+ for (const [key, value] of [
3808
+ ["azimuth", camera.azimuth],
3809
+ ["elevation", camera.elevation],
3810
+ ["roll", camera.roll],
3811
+ ["distance", camera.distance]
3812
+ ]) {
3813
+ if (value !== null && (!Number.isFinite(value) || key === "distance" && value <= 0)) {
3814
+ diagnostics.push(error("validate.viewpoint.camera.invalid", `Invalid camera ${key} "${String(value)}" in viewpoint "${viewpointId}".`, void 0, `${prefix}.${key}`));
3815
+ }
3816
+ }
3817
+ if (camera.distance !== null && projection !== "perspective") {
3818
+ 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`));
3819
+ }
3820
+ if (projection === "topdown" && (camera.elevation !== null || camera.roll !== null)) {
3821
+ 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));
3822
+ }
3823
+ if (projection === "isometric" && camera.elevation !== null) {
3824
+ diagnostics.push(info("validate.viewpoint.camera.isometricStored", `Camera elevation on isometric viewpoint "${viewpointId}" is preserved semantically for future 3D rendering.`, void 0, `${prefix}.elevation`));
3825
+ }
3826
+ if (camera.azimuth !== null && camera.azimuth !== 0 && rotationDeg !== 0) {
3827
+ 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`));
3828
+ }
3829
+ const hasAnchor = focusObjectId !== null && objectMap.has(focusObjectId) || selectedObjectId !== null && objectMap.has(selectedObjectId) || !!filter;
3830
+ if (!hasAnchor) {
3831
+ diagnostics.push(info("validate.viewpoint.camera.anchorMissing", `Viewpoint "${viewpointId}" stores camera settings without a focus object, selection, or filter anchor.`, void 0, prefix));
3832
+ }
3833
+ }
3834
+ function resolveEffectiveEpoch(system, object, event, pose) {
3835
+ return normalizeOptionalContextString(pose?.epoch) ?? normalizeOptionalContextString(event?.epoch) ?? normalizeOptionalContextString(object.epoch) ?? normalizeOptionalContextString(system?.epoch) ?? null;
3836
+ }
3837
+ function resolveEffectiveReferencePlane(system, object, event, pose) {
3838
+ return normalizeOptionalContextString(pose?.referencePlane) ?? normalizeOptionalContextString(event?.referencePlane) ?? normalizeOptionalContextString(object.referencePlane) ?? normalizeOptionalContextString(system?.referencePlane) ?? null;
3839
+ }
3840
+ function normalizeOptionalContextString(value) {
3841
+ return typeof value === "string" && value.trim() ? value.trim() : null;
3842
+ }
3600
3843
  function toleranceForField(object, field) {
3601
3844
  const tolerance = object.tolerances?.find((entry) => entry.field === field)?.value;
3602
3845
  if (typeof tolerance === "number") {
@@ -3705,7 +3948,9 @@
3705
3948
  "surface",
3706
3949
  "free",
3707
3950
  "inner",
3708
- "outer"
3951
+ "outer",
3952
+ "epoch",
3953
+ "referencePlane"
3709
3954
  ]);
3710
3955
  function parseWorldOrbitAtlas(source) {
3711
3956
  return parseAtlasSource(source);
@@ -3747,7 +3992,7 @@
3747
3992
  if (!sawSchemaHeader) {
3748
3993
  sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
3749
3994
  sawSchemaHeader = true;
3750
- if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
3995
+ if (prepared.comments.length > 0 && isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
3751
3996
  diagnostics.push({
3752
3997
  code: "parse.schema21.commentCompatibility",
3753
3998
  severity: "warning",
@@ -3817,11 +4062,11 @@
3817
4062
  return document2;
3818
4063
  }
3819
4064
  function assertDraftSchemaHeader(tokens, line) {
3820
- if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
3821
- throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
4065
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1", "2.5"].includes(tokens[1].value.toLowerCase())) {
4066
+ 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);
3822
4067
  }
3823
4068
  const version = tokens[1].value.toLowerCase();
3824
- return version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
4069
+ return version === "2.5" ? "2.5" : version === "2.1" ? "2.1" : version === "2.0-draft" ? "2.0-draft" : "2.0";
3825
4070
  }
3826
4071
  function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, flags) {
3827
4072
  const keyword = tokens[0]?.value.toLowerCase();
@@ -3841,6 +4086,8 @@
3841
4086
  return {
3842
4087
  kind: "defaults",
3843
4088
  system,
4089
+ sourceSchemaVersion,
4090
+ diagnostics,
3844
4091
  seenFields: /* @__PURE__ */ new Set()
3845
4092
  };
3846
4093
  case "atlas":
@@ -3933,6 +4180,7 @@
3933
4180
  preset: system.defaults.preset,
3934
4181
  zoom: null,
3935
4182
  rotationDeg: 0,
4183
+ camera: null,
3936
4184
  layers: {},
3937
4185
  filter: null
3938
4186
  };
@@ -3946,7 +4194,10 @@
3946
4194
  seenFields: /* @__PURE__ */ new Set(),
3947
4195
  inFilter: false,
3948
4196
  filterIndent: null,
3949
- seenFilterFields: /* @__PURE__ */ new Set()
4197
+ seenFilterFields: /* @__PURE__ */ new Set(),
4198
+ inCamera: false,
4199
+ cameraIndent: null,
4200
+ seenCameraFields: /* @__PURE__ */ new Set()
3950
4201
  };
3951
4202
  }
3952
4203
  function startAnnotationSection(tokens, line, system, annotationIds) {
@@ -4053,6 +4304,8 @@
4053
4304
  participantObjectIds: [],
4054
4305
  timing: null,
4055
4306
  visibility: null,
4307
+ epoch: null,
4308
+ referencePlane: null,
4056
4309
  tags: [],
4057
4310
  color: null,
4058
4311
  hidden: false,
@@ -4177,6 +4430,12 @@
4177
4430
  const value = joinFieldValue(tokens, line);
4178
4431
  switch (key) {
4179
4432
  case "view":
4433
+ if (isSchema25Projection(value)) {
4434
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "defaults.view", {
4435
+ line,
4436
+ column: tokens[0].column
4437
+ });
4438
+ }
4180
4439
  section.system.defaults.view = parseProjectionValue(value, line, tokens[0].column);
4181
4440
  return;
4182
4441
  case "scale":
@@ -4216,14 +4475,36 @@
4216
4475
  throw new WorldOrbitError(`Unknown atlas field "${tokens[0].value}"`, line, tokens[0].column);
4217
4476
  }
4218
4477
  function applyViewpointField2(section, indent, tokens, line) {
4478
+ if (section.inCamera && indent <= (section.cameraIndent ?? 0)) {
4479
+ section.inCamera = false;
4480
+ section.cameraIndent = null;
4481
+ }
4219
4482
  if (section.inFilter && indent <= (section.filterIndent ?? 0)) {
4220
4483
  section.inFilter = false;
4221
4484
  section.filterIndent = null;
4222
4485
  }
4486
+ if (section.inCamera) {
4487
+ applyViewpointCameraField(section, tokens, line);
4488
+ return;
4489
+ }
4223
4490
  if (section.inFilter) {
4224
4491
  applyViewpointFilterField(section, tokens, line);
4225
4492
  return;
4226
4493
  }
4494
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "camera") {
4495
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.camera", {
4496
+ line,
4497
+ column: tokens[0].column
4498
+ });
4499
+ if (section.seenFields.has("camera")) {
4500
+ throw new WorldOrbitError('Duplicate viewpoint field "camera"', line, tokens[0].column);
4501
+ }
4502
+ section.seenFields.add("camera");
4503
+ section.inCamera = true;
4504
+ section.cameraIndent = indent;
4505
+ section.viewpoint.camera = section.viewpoint.camera ?? createEmptyViewCamera2();
4506
+ return;
4507
+ }
4227
4508
  if (tokens.length === 1 && tokens[0].value.toLowerCase() === "filter") {
4228
4509
  if (section.seenFields.has("filter")) {
4229
4510
  throw new WorldOrbitError('Duplicate viewpoint field "filter"', line, tokens[0].column);
@@ -4249,6 +4530,12 @@
4249
4530
  section.viewpoint.selectedObjectId = value;
4250
4531
  return;
4251
4532
  case "projection":
4533
+ if (isSchema25Projection(value)) {
4534
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "projection", {
4535
+ line,
4536
+ column: tokens[0].column
4537
+ });
4538
+ }
4252
4539
  section.viewpoint.projection = parseProjectionValue(value, line, tokens[0].column);
4253
4540
  return;
4254
4541
  case "preset":
@@ -4260,6 +4547,13 @@
4260
4547
  case "rotation":
4261
4548
  section.viewpoint.rotationDeg = parseFiniteNumber2(value, line, tokens[0].column, "rotation");
4262
4549
  return;
4550
+ case "camera":
4551
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.camera", {
4552
+ line,
4553
+ column: tokens[0].column
4554
+ });
4555
+ section.viewpoint.camera = parseInlineViewCamera(tokens.slice(1), line, section.viewpoint.camera);
4556
+ return;
4263
4557
  case "layers":
4264
4558
  section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line, section.sourceSchemaVersion, section.diagnostics);
4265
4559
  return;
@@ -4274,6 +4568,28 @@
4274
4568
  throw new WorldOrbitError(`Unknown viewpoint field "${tokens[0].value}"`, line, tokens[0].column);
4275
4569
  }
4276
4570
  }
4571
+ function applyViewpointCameraField(section, tokens, line) {
4572
+ const key = requireUniqueField(tokens, section.seenCameraFields, line);
4573
+ const value = joinFieldValue(tokens, line);
4574
+ const camera = section.viewpoint.camera ?? createEmptyViewCamera2();
4575
+ switch (key) {
4576
+ case "azimuth":
4577
+ camera.azimuth = parseFiniteNumber2(value, line, tokens[0].column, "camera.azimuth");
4578
+ break;
4579
+ case "elevation":
4580
+ camera.elevation = parseFiniteNumber2(value, line, tokens[0].column, "camera.elevation");
4581
+ break;
4582
+ case "roll":
4583
+ camera.roll = parseFiniteNumber2(value, line, tokens[0].column, "camera.roll");
4584
+ break;
4585
+ case "distance":
4586
+ camera.distance = parsePositiveNumber2(value, line, tokens[0].column, "camera.distance");
4587
+ break;
4588
+ default:
4589
+ throw new WorldOrbitError(`Unknown viewpoint camera field "${tokens[0].value}"`, line, tokens[0].column);
4590
+ }
4591
+ section.viewpoint.camera = camera;
4592
+ }
4277
4593
  function applyViewpointFilterField(section, tokens, line) {
4278
4594
  const key = requireUniqueField(tokens, section.seenFilterFields, line);
4279
4595
  const filter = section.viewpoint.filter ?? createEmptyViewpointFilter2();
@@ -4384,6 +4700,12 @@
4384
4700
  section.positionsIndent = null;
4385
4701
  }
4386
4702
  if (section.activePose) {
4703
+ if (tokens[0]?.value === "epoch" || tokens[0]?.value === "referencePlane") {
4704
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, `pose.${tokens[0].value}`, {
4705
+ line,
4706
+ column: tokens[0]?.column ?? 1
4707
+ });
4708
+ }
4387
4709
  section.activePose.fields.push(parseEventPoseField(tokens, line, section.activePoseSeenFields));
4388
4710
  return;
4389
4711
  }
@@ -4438,6 +4760,20 @@
4438
4760
  case "visibility":
4439
4761
  section.event.visibility = joinFieldValue(tokens, line);
4440
4762
  return;
4763
+ case "epoch":
4764
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "event.epoch", {
4765
+ line,
4766
+ column: tokens[0].column
4767
+ });
4768
+ section.event.epoch = joinFieldValue(tokens, line);
4769
+ return;
4770
+ case "referenceplane":
4771
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "event.referencePlane", {
4772
+ line,
4773
+ column: tokens[0].column
4774
+ });
4775
+ section.event.referencePlane = joinFieldValue(tokens, line);
4776
+ return;
4441
4777
  case "tags":
4442
4778
  section.event.tags = parseTokenList(tokens.slice(1), line, "tags");
4443
4779
  return;
@@ -4565,11 +4901,15 @@
4565
4901
  }
4566
4902
  function parseProjectionValue(value, line, column) {
4567
4903
  const normalized = value.toLowerCase();
4568
- if (normalized !== "topdown" && normalized !== "isometric") {
4904
+ if (normalized !== "topdown" && normalized !== "isometric" && normalized !== "orthographic" && normalized !== "perspective") {
4569
4905
  throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
4570
4906
  }
4571
4907
  return normalized;
4572
4908
  }
4909
+ function isSchema25Projection(value) {
4910
+ const normalized = value.toLowerCase();
4911
+ return normalized === "orthographic" || normalized === "perspective";
4912
+ }
4573
4913
  function parsePresetValue(value, line, column) {
4574
4914
  const normalized = value.toLowerCase();
4575
4915
  if (normalized === "diagram" || normalized === "presentation" || normalized === "atlas-card" || normalized === "markdown") {
@@ -4599,6 +4939,48 @@
4599
4939
  groupIds: []
4600
4940
  };
4601
4941
  }
4942
+ function createEmptyViewCamera2() {
4943
+ return {
4944
+ azimuth: null,
4945
+ elevation: null,
4946
+ roll: null,
4947
+ distance: null
4948
+ };
4949
+ }
4950
+ function parseInlineViewCamera(tokens, line, current) {
4951
+ if (tokens.length === 0 || tokens.length % 2 !== 0) {
4952
+ throw new WorldOrbitError('Field "camera" expects "<field> <value>" pairs', line, tokens[0]?.column ?? 1);
4953
+ }
4954
+ const camera = current ? { ...current } : createEmptyViewCamera2();
4955
+ const seen = /* @__PURE__ */ new Set();
4956
+ for (let index = 0; index < tokens.length; index += 2) {
4957
+ const fieldToken = tokens[index];
4958
+ const valueToken = tokens[index + 1];
4959
+ const key = fieldToken.value.toLowerCase();
4960
+ if (seen.has(key)) {
4961
+ throw new WorldOrbitError(`Duplicate viewpoint camera field "${fieldToken.value}"`, line, fieldToken.column);
4962
+ }
4963
+ seen.add(key);
4964
+ const value = valueToken.value;
4965
+ switch (key) {
4966
+ case "azimuth":
4967
+ camera.azimuth = parseFiniteNumber2(value, line, fieldToken.column, "camera.azimuth");
4968
+ break;
4969
+ case "elevation":
4970
+ camera.elevation = parseFiniteNumber2(value, line, fieldToken.column, "camera.elevation");
4971
+ break;
4972
+ case "roll":
4973
+ camera.roll = parseFiniteNumber2(value, line, fieldToken.column, "camera.roll");
4974
+ break;
4975
+ case "distance":
4976
+ camera.distance = parsePositiveNumber2(value, line, fieldToken.column, "camera.distance");
4977
+ break;
4978
+ default:
4979
+ throw new WorldOrbitError(`Unknown viewpoint camera field "${fieldToken.value}"`, line, fieldToken.column);
4980
+ }
4981
+ }
4982
+ return camera;
4983
+ }
4602
4984
  function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
4603
4985
  const fields = [];
4604
4986
  let index = 0;
@@ -4731,7 +5113,7 @@
4731
5113
  object.tolerances = tolerances;
4732
5114
  if (typedBlocks && Object.keys(typedBlocks).length > 0)
4733
5115
  object.typedBlocks = typedBlocks;
4734
- if (sourceSchemaVersion !== "2.1") {
5116
+ if (isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
4735
5117
  if (object.groups || object.epoch || object.referencePlane || object.tidalLock !== void 0 || object.resonance || object.renderHints || object.deriveRules?.length || object.validationRules?.length || object.lockedFields?.length || object.tolerances?.length || object.typedBlocks) {
4736
5118
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
4737
5119
  }
@@ -4747,23 +5129,25 @@
4747
5129
  };
4748
5130
  }
4749
5131
  function normalizeDraftEventPose(rawPose) {
4750
- const fieldMap = collectDraftFields(rawPose.fields);
5132
+ const fieldMap = collectDraftFields(rawPose.fields, "event-pose");
4751
5133
  const placement = extractPlacementFromFieldMap(fieldMap);
4752
5134
  return {
4753
5135
  objectId: rawPose.objectId,
4754
5136
  placement,
4755
5137
  inner: parseOptionalUnitField(fieldMap.get("inner")?.[0], "inner"),
4756
- outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer")
5138
+ outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer"),
5139
+ epoch: parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]),
5140
+ referencePlane: parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0])
4757
5141
  };
4758
5142
  }
4759
- function collectDraftFields(fields) {
5143
+ function collectDraftFields(fields, _mode = "object") {
4760
5144
  const grouped = /* @__PURE__ */ new Map();
4761
5145
  for (const field of fields) {
4762
5146
  const spec = getDraftObjectFieldSpec(field.key);
4763
- if (!spec) {
5147
+ if (!spec && !EVENT_POSE_FIELD_KEYS.has(field.key)) {
4764
5148
  throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
4765
5149
  }
4766
- if (!spec.allowRepeat && grouped.has(field.key)) {
5150
+ if (!spec?.allowRepeat && grouped.has(field.key)) {
4767
5151
  throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
4768
5152
  }
4769
5153
  const existing = grouped.get(field.key) ?? [];
@@ -4940,7 +5324,7 @@
4940
5324
  }
4941
5325
  }
4942
5326
  function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
4943
- if (sourceSchemaVersion === "2.1") {
5327
+ if (!isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
4944
5328
  return;
4945
5329
  }
4946
5330
  diagnostics.push({
@@ -4952,6 +5336,34 @@
4952
5336
  column: location.column
4953
5337
  });
4954
5338
  }
5339
+ function warnIfSchema25Feature(sourceSchemaVersion, diagnostics, featureName, location) {
5340
+ if (!isSchemaOlderThan(sourceSchemaVersion, "2.5")) {
5341
+ return;
5342
+ }
5343
+ diagnostics.push({
5344
+ code: "parse.schema25.featureCompatibility",
5345
+ severity: "warning",
5346
+ source: "parse",
5347
+ message: `Feature "${featureName}" requires schema 2.5; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
5348
+ line: location.line,
5349
+ column: location.column
5350
+ });
5351
+ }
5352
+ function isSchemaOlderThan(sourceSchemaVersion, requiredVersion) {
5353
+ return schemaVersionRank(sourceSchemaVersion) < schemaVersionRank(requiredVersion);
5354
+ }
5355
+ function schemaVersionRank(version) {
5356
+ switch (version) {
5357
+ case "2.0-draft":
5358
+ return 0;
5359
+ case "2.0":
5360
+ return 1;
5361
+ case "2.1":
5362
+ return 2;
5363
+ case "2.5":
5364
+ return 3;
5365
+ }
5366
+ }
4955
5367
  function preprocessAtlasSource(source) {
4956
5368
  const chars = [...source];
4957
5369
  const comments = [];
@@ -5039,8 +5451,9 @@
5039
5451
  }
5040
5452
 
5041
5453
  // packages/core/dist/load.js
5042
- var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1)?$/i;
5454
+ var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1|\.5)?$/i;
5043
5455
  var ATLAS_SCHEMA_21_PATTERN = /^schema\s+2\.1$/i;
5456
+ var ATLAS_SCHEMA_25_PATTERN = /^schema\s+2\.5$/i;
5044
5457
  var LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
5045
5458
  function detectWorldOrbitSchemaVersion(source) {
5046
5459
  for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
@@ -5054,6 +5467,9 @@
5054
5467
  if (ATLAS_SCHEMA_21_PATTERN.test(trimmed)) {
5055
5468
  return "2.1";
5056
5469
  }
5470
+ if (ATLAS_SCHEMA_25_PATTERN.test(trimmed)) {
5471
+ return "2.5";
5472
+ }
5057
5473
  if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
5058
5474
  return "2.0";
5059
5475
  }
@@ -5114,7 +5530,7 @@
5114
5530
  }
5115
5531
  function loadWorldOrbitSourceWithDiagnostics(source) {
5116
5532
  const schemaVersion = detectWorldOrbitSchemaVersion(source);
5117
- if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1") {
5533
+ if (schemaVersion === "2.0" || schemaVersion === "2.0-draft" || schemaVersion === "2.1" || schemaVersion === "2.5") {
5118
5534
  return loadAtlasSourceWithDiagnostics(source, schemaVersion);
5119
5535
  }
5120
5536
  let ast;
@@ -6188,6 +6604,7 @@
6188
6604
  padding: options.padding,
6189
6605
  preset: options.preset,
6190
6606
  projection: options.projection,
6607
+ camera: options.camera ? { ...options.camera } : null,
6191
6608
  scaleModel: options.scaleModel ? { ...options.scaleModel } : void 0,
6192
6609
  theme: options.theme,
6193
6610
  layers: options.layers,
@@ -6476,6 +6893,11 @@
6476
6893
  if (currentInput.kind !== "scene" && viewpoint.projection !== scene.projection) {
6477
6894
  nextRenderOptions.projection = viewpoint.projection;
6478
6895
  }
6896
+ if (viewpoint.camera) {
6897
+ nextRenderOptions.camera = { ...viewpoint.camera };
6898
+ } else if (renderOptions.camera) {
6899
+ nextRenderOptions.camera = null;
6900
+ }
6479
6901
  if (viewpointLayers) {
6480
6902
  nextRenderOptions.layers = viewpointLayers;
6481
6903
  }
@@ -7082,6 +7504,7 @@
7082
7504
  function cloneRenderOptions(renderOptions) {
7083
7505
  return {
7084
7506
  ...renderOptions,
7507
+ camera: renderOptions.camera ? { ...renderOptions.camera } : null,
7085
7508
  filter: renderOptions.filter ? { ...renderOptions.filter } : void 0,
7086
7509
  scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0,
7087
7510
  layers: renderOptions.layers ? { ...renderOptions.layers } : void 0,
@@ -7093,6 +7516,7 @@
7093
7516
  return {
7094
7517
  ...current,
7095
7518
  ...next,
7519
+ camera: next.camera !== void 0 ? next.camera ? { ...next.camera } : null : current.camera ? { ...current.camera } : null,
7096
7520
  filter: next.filter !== void 0 ? normalizeViewerFilter(next.filter) : current.filter ? { ...current.filter } : void 0,
7097
7521
  scaleModel: next.scaleModel ? {
7098
7522
  ...current.scaleModel ?? {},
@@ -7106,7 +7530,7 @@
7106
7530
  };
7107
7531
  }
7108
7532
  function hasSceneAffectingRenderOptions(options) {
7109
- return options.width !== void 0 || options.height !== void 0 || options.padding !== void 0 || options.preset !== void 0 || options.projection !== void 0 || options.scaleModel !== void 0 || options.activeEventId !== void 0;
7533
+ 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;
7110
7534
  }
7111
7535
  function resolveSourceRenderOptions2(loaded, renderOptions) {
7112
7536
  const atlasDocument = loaded.atlasDocument ?? loaded.draftDocument;
@@ -7703,6 +8127,8 @@
7703
8127
  }
7704
8128
  function buildInspectorSnapshot() {
7705
8129
  const activeViewer = requireViewer();
8130
+ const scene = activeViewer.getScene();
8131
+ const camera = scene.camera;
7706
8132
  return {
7707
8133
  selection: activeViewer.getSelectionDetails(),
7708
8134
  activeViewpoint: activeViewer.getActiveViewpoint(),
@@ -7710,14 +8136,21 @@
7710
8136
  atlasState: activeViewer.getAtlasState(),
7711
8137
  visibleObjectIds: activeViewer.getVisibleObjects().map((object) => object.objectId),
7712
8138
  scene: {
7713
- title: activeViewer.getScene().title,
7714
- projection: activeViewer.getScene().projection,
7715
- renderPreset: activeViewer.getScene().renderPreset,
7716
- groupCount: activeViewer.getScene().groups.length,
7717
- semanticGroupCount: activeViewer.getScene().semanticGroups.length,
7718
- relationCount: activeViewer.getScene().relations.length,
7719
- eventCount: activeViewer.getScene().events.length,
7720
- viewpointCount: activeViewer.getScene().viewpoints.length
8139
+ title: scene.title,
8140
+ projection: scene.projection,
8141
+ renderProjection: scene.renderProjection,
8142
+ camera: camera ? {
8143
+ azimuth: camera.azimuth,
8144
+ elevation: camera.elevation,
8145
+ roll: camera.roll,
8146
+ distance: camera.distance
8147
+ } : null,
8148
+ renderPreset: scene.renderPreset,
8149
+ groupCount: scene.groups.length,
8150
+ semanticGroupCount: scene.semanticGroups.length,
8151
+ relationCount: scene.relations.length,
8152
+ eventCount: scene.events.length,
8153
+ viewpointCount: scene.viewpoints.length
7721
8154
  }
7722
8155
  };
7723
8156
  }