worldorbit 2.5.16 → 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.
Files changed (35) hide show
  1. package/README.md +81 -15
  2. package/dist/browser/core/dist/index.js +1228 -110
  3. package/dist/browser/editor/dist/index.js +1896 -180
  4. package/dist/browser/markdown/dist/index.js +1071 -99
  5. package/dist/browser/viewer/dist/index.js +1127 -113
  6. package/dist/unpkg/core/dist/index.js +1228 -110
  7. package/dist/unpkg/editor/dist/index.js +1896 -180
  8. package/dist/unpkg/markdown/dist/index.js +1071 -99
  9. package/dist/unpkg/viewer/dist/index.js +1127 -113
  10. package/dist/unpkg/worldorbit-core.min.js +12 -12
  11. package/dist/unpkg/worldorbit-editor.min.js +295 -203
  12. package/dist/unpkg/worldorbit-markdown.min.js +66 -58
  13. package/dist/unpkg/worldorbit-viewer.min.js +84 -76
  14. package/dist/unpkg/worldorbit.js +1304 -124
  15. package/dist/unpkg/worldorbit.min.js +88 -80
  16. package/package.json +1 -1
  17. package/packages/core/dist/atlas-edit.js +75 -1
  18. package/packages/core/dist/atlas-validate.js +211 -8
  19. package/packages/core/dist/draft-parse.js +401 -22
  20. package/packages/core/dist/draft.d.ts +5 -2
  21. package/packages/core/dist/draft.js +103 -8
  22. package/packages/core/dist/format.js +99 -6
  23. package/packages/core/dist/load.js +9 -2
  24. package/packages/core/dist/normalize.js +1 -0
  25. package/packages/core/dist/scene.js +400 -64
  26. package/packages/core/dist/types.d.ts +60 -4
  27. package/packages/editor/dist/editor.js +702 -65
  28. package/packages/editor/dist/types.d.ts +3 -1
  29. package/packages/viewer/dist/atlas-state.js +11 -2
  30. package/packages/viewer/dist/atlas-viewer.js +19 -7
  31. package/packages/viewer/dist/render.js +31 -2
  32. package/packages/viewer/dist/theme.js +1 -0
  33. package/packages/viewer/dist/tooltip.js +9 -0
  34. package/packages/viewer/dist/types.d.ts +12 -2
  35. package/packages/viewer/dist/viewer.js +28 -1
@@ -541,6 +541,7 @@
541
541
  system,
542
542
  groups: [],
543
543
  relations: [],
544
+ events: [],
544
545
  objects
545
546
  };
546
547
  }
@@ -920,12 +921,16 @@
920
921
  const height = frame.height;
921
922
  const padding = frame.padding;
922
923
  const layoutPreset = resolveLayoutPreset(document2);
923
- 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);
924
927
  const scaleModel = resolveScaleModel(layoutPreset, options.scaleModel);
925
928
  const spacingFactor = layoutPresetSpacing(layoutPreset);
926
929
  const systemId = document2.system?.id ?? null;
927
- const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
928
- const relationships = buildSceneRelationships(document2.objects, objectMap);
930
+ const activeEventId = options.activeEventId ?? null;
931
+ const effectiveObjects = createEffectiveObjects(document2.objects, document2.events ?? [], activeEventId);
932
+ const objectMap = new Map(effectiveObjects.map((object) => [object.id, object]));
933
+ const relationships = buildSceneRelationships(effectiveObjects, objectMap);
929
934
  const positions = /* @__PURE__ */ new Map();
930
935
  const orbitDrafts = [];
931
936
  const leaderDrafts = [];
@@ -934,7 +939,7 @@
934
939
  const atObjects = [];
935
940
  const surfaceChildren = /* @__PURE__ */ new Map();
936
941
  const orbitChildren = /* @__PURE__ */ new Map();
937
- for (const object of document2.objects) {
942
+ for (const object of effectiveObjects) {
938
943
  const placement = object.placement;
939
944
  if (!placement) {
940
945
  rootObjects.push(object);
@@ -961,7 +966,7 @@
961
966
  surfaceChildren,
962
967
  objectMap,
963
968
  spacingFactor,
964
- projection,
969
+ projection: renderProjection,
965
970
  scaleModel
966
971
  };
967
972
  const primaryRoot = rootObjects.find((object) => object.type === "star") ?? rootObjects[0] ?? null;
@@ -973,7 +978,7 @@
973
978
  const rootRingRadius = Math.min(width, height) * 0.28 * spacingFactor * scaleModel.orbitDistanceMultiplier;
974
979
  secondaryRoots.forEach((object, index) => {
975
980
  const angle = angleForIndex(index, secondaryRoots.length, -Math.PI / 2);
976
- const offset = projectPolarOffset(angle, rootRingRadius, projection, 1);
981
+ const offset = projectPolarOffset(angle, rootRingRadius, renderProjection, 1);
977
982
  placeObject(object, centerX + offset.x, centerY + offset.y, 0, positions, orbitDrafts, leaderDrafts, context);
978
983
  });
979
984
  }
@@ -1029,38 +1034,48 @@
1029
1034
  const objects = [...positions.values()].map((position) => createSceneObject(position, scaleModel, relationships));
1030
1035
  const orbitVisuals = orbitDrafts.map((draft) => createOrbitVisual(draft, relationships.groupIds.get(draft.object.id) ?? null));
1031
1036
  const leaders = leaderDrafts.map((draft) => createLeaderLine(draft));
1032
- const labels = createSceneLabels(objects, height, scaleModel.labelMultiplier);
1037
+ const labels = createSceneLabels(objects, width, height, scaleModel.labelMultiplier);
1033
1038
  const relations = createSceneRelations(document2, objects);
1034
- const layers = createSceneLayers(orbitVisuals, relations, leaders, objects, labels);
1035
- const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships);
1039
+ const events = createSceneEvents(document2.events ?? [], objects, activeEventId);
1040
+ const layers = createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels);
1041
+ const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, scaleModel.labelMultiplier);
1036
1042
  const semanticGroups = createSceneSemanticGroups(document2, objects);
1037
- const viewpoints = createSceneViewpoints(document2, projection, frame.preset, relationships, objectMap);
1038
- const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels);
1043
+ const viewpoints = createSceneViewpoints(document2, schemaProjection, frame.preset, relationships, objectMap);
1044
+ const contentBounds = calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, scaleModel.labelMultiplier);
1039
1045
  return {
1040
1046
  width,
1041
1047
  height,
1042
1048
  padding,
1043
1049
  renderPreset: frame.preset,
1044
- projection,
1050
+ projection: schemaProjection,
1051
+ renderProjection,
1052
+ camera,
1045
1053
  scaleModel,
1046
1054
  title: String(document2.system?.title ?? document2.system?.properties.title ?? document2.system?.id ?? "WorldOrbit") || "WorldOrbit",
1047
- subtitle: `${capitalizeLabel(projection)} view - ${capitalizeLabel(layoutPreset)} layout`,
1055
+ subtitle: buildSceneSubtitle(schemaProjection, renderProjection, layoutPreset, camera),
1048
1056
  systemId,
1049
- viewMode: projection,
1057
+ viewMode: schemaProjection,
1050
1058
  layoutPreset,
1051
1059
  metadata: {
1052
1060
  format: document2.format,
1053
1061
  version: document2.version,
1054
- view: projection,
1062
+ view: schemaProjection,
1063
+ renderProjection,
1055
1064
  scale: String(document2.system?.properties.scale ?? layoutPreset),
1056
1065
  units: String(document2.system?.properties.units ?? "mixed"),
1057
- 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) } : {}
1058
1071
  },
1059
1072
  contentBounds,
1060
1073
  layers,
1061
1074
  groups,
1062
1075
  semanticGroups,
1063
1076
  viewpoints,
1077
+ events,
1078
+ activeEventId,
1064
1079
  objects,
1065
1080
  orbitVisuals,
1066
1081
  relations,
@@ -1079,6 +1094,56 @@
1079
1094
  y: center.y + dx * sin + dy * cos
1080
1095
  };
1081
1096
  }
1097
+ function createEffectiveObjects(objects, events, activeEventId) {
1098
+ const cloned = objects.map((object) => structuredClone(object));
1099
+ if (!activeEventId) {
1100
+ return cloned;
1101
+ }
1102
+ const activeEvent = events.find((event) => event.id === activeEventId);
1103
+ if (!activeEvent) {
1104
+ return cloned;
1105
+ }
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
+ }
1124
+ for (const pose of activeEvent.positions) {
1125
+ const object = objectMap.get(pose.objectId);
1126
+ if (!object) {
1127
+ continue;
1128
+ }
1129
+ if (pose.placement) {
1130
+ object.placement = structuredClone(pose.placement);
1131
+ }
1132
+ if (pose.inner) {
1133
+ object.properties.inner = { ...pose.inner };
1134
+ }
1135
+ if (pose.outer) {
1136
+ object.properties.outer = { ...pose.outer };
1137
+ }
1138
+ if (pose.epoch) {
1139
+ object.epoch = pose.epoch;
1140
+ }
1141
+ if (pose.referencePlane) {
1142
+ object.referencePlane = pose.referencePlane;
1143
+ }
1144
+ }
1145
+ return cloned;
1146
+ }
1082
1147
  function resolveLayoutPreset(document2) {
1083
1148
  const rawScale = String(document2.system?.properties.scale ?? "balanced").toLowerCase();
1084
1149
  switch (rawScale) {
@@ -1115,10 +1180,59 @@
1115
1180
  }
1116
1181
  }
1117
1182
  function resolveProjection(document2, projection) {
1118
- if (projection === "topdown" || projection === "isometric") {
1183
+ if (projection === "topdown" || projection === "isometric" || projection === "orthographic" || projection === "perspective") {
1119
1184
  return projection;
1120
1185
  }
1121
- 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(" - ");
1122
1236
  }
1123
1237
  function resolveScaleModel(layoutPreset, overrides) {
1124
1238
  const defaults = defaultScaleModel(layoutPreset);
@@ -1234,24 +1348,14 @@
1234
1348
  hidden: draft.object.properties.hidden === true
1235
1349
  };
1236
1350
  }
1237
- function createSceneLabels(objects, sceneHeight, labelMultiplier) {
1351
+ function createSceneLabels(objects, sceneWidth, sceneHeight, labelMultiplier) {
1238
1352
  const labels = [];
1239
1353
  const occupied = [];
1240
- const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort((left, right) => left.sortKey - right.sortKey);
1354
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1355
+ const visibleObjects = [...objects].filter((object) => !object.hidden && object.object.renderHints?.renderLabel !== false).sort(compareLabelPlacementOrder);
1241
1356
  for (const object of visibleObjects) {
1242
- const direction = object.y > sceneHeight * 0.62 ? -1 : 1;
1243
- const labelHalfWidth = estimateLabelHalfWidth(object, labelMultiplier);
1244
- let labelY = object.y + direction * (object.radius + 18 * labelMultiplier);
1245
- let secondaryY = labelY + direction * (16 * labelMultiplier);
1246
- let bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1247
- let attempts = 0;
1248
- while (occupied.some((entry) => rectsOverlap(entry, bounds)) && attempts < 10) {
1249
- labelY += direction * 14 * labelMultiplier;
1250
- secondaryY += direction * 14 * labelMultiplier;
1251
- bounds = createLabelRect(object.x, labelY, secondaryY, labelHalfWidth, direction);
1252
- attempts += 1;
1253
- }
1254
- occupied.push(bounds);
1357
+ const placement = selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) ?? createLabelPlacement(object, defaultVerticalDirection(object, objectMap.get(object.parentId ?? "") ?? null, sceneHeight), 0, labelMultiplier);
1358
+ occupied.push(createLabelRect(object, placement, labelMultiplier));
1255
1359
  labels.push({
1256
1360
  renderId: `${object.renderId}-label`,
1257
1361
  objectId: object.objectId,
@@ -1260,17 +1364,128 @@
1260
1364
  semanticGroupIds: [...object.semanticGroupIds],
1261
1365
  label: object.label,
1262
1366
  secondaryLabel: object.secondaryLabel,
1263
- x: object.x,
1264
- y: labelY,
1265
- secondaryY,
1266
- textAnchor: "middle",
1267
- direction: direction < 0 ? "above" : "below",
1367
+ x: placement.x,
1368
+ y: placement.labelY,
1369
+ secondaryY: placement.secondaryY,
1370
+ textAnchor: placement.textAnchor,
1371
+ direction: placement.direction,
1268
1372
  hidden: object.hidden
1269
1373
  });
1270
1374
  }
1271
1375
  return labels;
1272
1376
  }
1273
- function createSceneLayers(orbitVisuals, relations, leaders, objects, labels) {
1377
+ function compareLabelPlacementOrder(left, right) {
1378
+ const priorityDiff = labelPlacementPriority(left) - labelPlacementPriority(right);
1379
+ if (priorityDiff !== 0) {
1380
+ return priorityDiff;
1381
+ }
1382
+ const renderPriorityDiff = (right.object.renderHints?.renderPriority ?? 0) - (left.object.renderHints?.renderPriority ?? 0);
1383
+ if (renderPriorityDiff !== 0) {
1384
+ return renderPriorityDiff;
1385
+ }
1386
+ return left.sortKey - right.sortKey;
1387
+ }
1388
+ function labelPlacementPriority(object) {
1389
+ switch (object.object.type) {
1390
+ case "star":
1391
+ return 0;
1392
+ case "planet":
1393
+ return 1;
1394
+ case "moon":
1395
+ return 2;
1396
+ case "belt":
1397
+ case "ring":
1398
+ return 3;
1399
+ case "asteroid":
1400
+ case "comet":
1401
+ return 4;
1402
+ case "structure":
1403
+ case "phenomenon":
1404
+ return 5;
1405
+ }
1406
+ }
1407
+ function selectLabelPlacement(object, objectMap, occupied, sceneWidth, sceneHeight, labelMultiplier) {
1408
+ for (const direction of preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight)) {
1409
+ const maxAttempts = direction === "left" || direction === "right" ? 4 : 6;
1410
+ for (let attempt = 0; attempt <= maxAttempts; attempt += 1) {
1411
+ const placement = createLabelPlacement(object, direction, attempt, labelMultiplier);
1412
+ const rect = createLabelRect(object, placement, labelMultiplier);
1413
+ if (!occupied.some((entry) => rectsOverlap(entry, rect))) {
1414
+ return placement;
1415
+ }
1416
+ }
1417
+ }
1418
+ return null;
1419
+ }
1420
+ function preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight) {
1421
+ const parent = object.parentId ? objectMap.get(object.parentId) ?? null : null;
1422
+ const vertical = defaultVerticalDirection(object, parent, sceneHeight);
1423
+ const oppositeVertical = vertical === "below" ? "above" : "below";
1424
+ const horizontal = defaultHorizontalDirection(object, parent, sceneWidth);
1425
+ const oppositeHorizontal = horizontal === "right" ? "left" : "right";
1426
+ const preferHorizontal = object.object.type === "structure" || object.object.type === "phenomenon" || object.object.placement?.mode === "at" || object.object.placement?.mode === "surface" || object.object.placement?.mode === "free";
1427
+ return preferHorizontal ? [horizontal, vertical, oppositeHorizontal, oppositeVertical] : [vertical, horizontal, oppositeVertical, oppositeHorizontal];
1428
+ }
1429
+ function defaultVerticalDirection(object, parent, sceneHeight) {
1430
+ if (parent && Math.abs(object.y - parent.y) > 6) {
1431
+ return object.y >= parent.y ? "below" : "above";
1432
+ }
1433
+ return object.y > sceneHeight * 0.62 ? "above" : "below";
1434
+ }
1435
+ function defaultHorizontalDirection(object, parent, sceneWidth) {
1436
+ if (parent && Math.abs(object.x - parent.x) > 6) {
1437
+ return object.x >= parent.x ? "right" : "left";
1438
+ }
1439
+ return object.x >= sceneWidth / 2 ? "right" : "left";
1440
+ }
1441
+ function createLabelPlacement(object, direction, attempt, labelMultiplier) {
1442
+ const step = 14 * labelMultiplier;
1443
+ switch (direction) {
1444
+ case "above": {
1445
+ const labelY = object.y - (object.radius + 18 * labelMultiplier + attempt * step);
1446
+ return {
1447
+ x: object.x,
1448
+ labelY,
1449
+ secondaryY: labelY - 16 * labelMultiplier,
1450
+ textAnchor: "middle",
1451
+ direction
1452
+ };
1453
+ }
1454
+ case "below": {
1455
+ const labelY = object.y + object.radius + 18 * labelMultiplier + attempt * step;
1456
+ return {
1457
+ x: object.x,
1458
+ labelY,
1459
+ secondaryY: labelY + 16 * labelMultiplier,
1460
+ textAnchor: "middle",
1461
+ direction
1462
+ };
1463
+ }
1464
+ case "left": {
1465
+ const x = object.x - (object.visualRadius + 16 * labelMultiplier + attempt * step);
1466
+ const labelY = object.y - 4 * labelMultiplier;
1467
+ return {
1468
+ x,
1469
+ labelY,
1470
+ secondaryY: labelY + 16 * labelMultiplier,
1471
+ textAnchor: "end",
1472
+ direction
1473
+ };
1474
+ }
1475
+ case "right": {
1476
+ const x = object.x + object.visualRadius + 16 * labelMultiplier + attempt * step;
1477
+ const labelY = object.y - 4 * labelMultiplier;
1478
+ return {
1479
+ x,
1480
+ labelY,
1481
+ secondaryY: labelY + 16 * labelMultiplier,
1482
+ textAnchor: "start",
1483
+ direction
1484
+ };
1485
+ }
1486
+ }
1487
+ }
1488
+ function createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels) {
1274
1489
  const backOrbitIds = orbitVisuals.filter((visual) => !visual.hidden && Boolean(visual.backArcPath)).map((visual) => visual.renderId);
1275
1490
  const frontOrbitIds = orbitVisuals.filter((visual) => !visual.hidden).map((visual) => visual.renderId);
1276
1491
  return [
@@ -1285,6 +1500,10 @@
1285
1500
  id: "relations",
1286
1501
  renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId)
1287
1502
  },
1503
+ {
1504
+ id: "events",
1505
+ renderIds: events.filter((event) => !event.hidden).map((event) => event.renderId)
1506
+ },
1288
1507
  {
1289
1508
  id: "objects",
1290
1509
  renderIds: objects.filter((object) => !object.hidden).map((object) => object.renderId)
@@ -1296,7 +1515,7 @@
1296
1515
  { id: "metadata", renderIds: ["wo-title", "wo-subtitle", "wo-meta"] }
1297
1516
  ];
1298
1517
  }
1299
- function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships) {
1518
+ function createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, labelMultiplier) {
1300
1519
  const groups = /* @__PURE__ */ new Map();
1301
1520
  const ensureGroup = (groupId) => {
1302
1521
  if (!groupId) {
@@ -1345,7 +1564,7 @@
1345
1564
  }
1346
1565
  }
1347
1566
  for (const group of groups.values()) {
1348
- group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels);
1567
+ group.contentBounds = calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier);
1349
1568
  }
1350
1569
  return [...groups.values()].sort((left, right) => left.label.localeCompare(right.label));
1351
1570
  }
@@ -1379,6 +1598,29 @@
1379
1598
  };
1380
1599
  }).sort((left, right) => left.relation.id.localeCompare(right.relation.id));
1381
1600
  }
1601
+ function createSceneEvents(events, objects, activeEventId) {
1602
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
1603
+ return events.map((event) => {
1604
+ const objectIds = [.../* @__PURE__ */ new Set([
1605
+ ...event.targetObjectId ? [event.targetObjectId] : [],
1606
+ ...event.participantObjectIds
1607
+ ])];
1608
+ const positions = objectIds.map((objectId) => objectMap.get(objectId)).filter(Boolean);
1609
+ const centroidX = positions.length > 0 ? positions.reduce((sum, object) => sum + object.x, 0) / positions.length : 0;
1610
+ const centroidY = positions.length > 0 ? positions.reduce((sum, object) => sum + object.y, 0) / positions.length : 0;
1611
+ return {
1612
+ renderId: `${createRenderId(event.id)}-event`,
1613
+ eventId: event.id,
1614
+ event,
1615
+ objectIds,
1616
+ participantIds: [...event.participantObjectIds],
1617
+ targetObjectId: event.targetObjectId,
1618
+ x: centroidX,
1619
+ y: centroidY,
1620
+ hidden: event.hidden || positions.length === 0 || positions.every((object) => object.hidden) || activeEventId !== null && event.id !== activeEventId
1621
+ };
1622
+ }).sort((left, right) => left.event.id.localeCompare(right.event.id));
1623
+ }
1382
1624
  function createSceneViewpoints(document2, projection, preset, relationships, objectMap) {
1383
1625
  const generatedOverview = createGeneratedOverviewViewpoint(document2, projection, preset);
1384
1626
  const drafts = /* @__PURE__ */ new Map();
@@ -1426,13 +1668,18 @@
1426
1668
  function createGeneratedOverviewViewpoint(document2, projection, preset) {
1427
1669
  const title = document2.system?.title ?? document2.system?.properties.title;
1428
1670
  const label = title ? `${String(title)} Overview` : "Overview";
1671
+ const camera = normalizeViewCamera(null);
1672
+ const renderProjection = resolveRenderProjection(projection, camera);
1429
1673
  return {
1430
1674
  id: "overview",
1431
1675
  label,
1432
1676
  summary: "Fit the whole system with the current atlas defaults.",
1433
1677
  objectId: null,
1434
1678
  selectedObjectId: null,
1679
+ eventIds: [],
1435
1680
  projection,
1681
+ renderProjection,
1682
+ camera,
1436
1683
  preset,
1437
1684
  rotationDeg: 0,
1438
1685
  scale: null,
@@ -1468,6 +1715,9 @@
1468
1715
  draft.select = normalizedValue;
1469
1716
  }
1470
1717
  return;
1718
+ case "events":
1719
+ draft.eventIds = splitListValue(normalizedValue);
1720
+ return;
1471
1721
  case "projection":
1472
1722
  case "view":
1473
1723
  draft.projection = parseViewProjection(normalizedValue) ?? projection;
@@ -1479,6 +1729,30 @@
1479
1729
  case "angle":
1480
1730
  draft.rotationDeg = parseFiniteNumber(normalizedValue) ?? draft.rotationDeg ?? 0;
1481
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;
1482
1756
  case "zoom":
1483
1757
  case "scale":
1484
1758
  draft.scale = parsePositiveNumber(normalizedValue);
@@ -1518,13 +1792,19 @@
1518
1792
  const selectedObjectId = draft.select && objectMap.has(draft.select) ? draft.select : objectId;
1519
1793
  const filter = normalizeViewpointFilter(draft.filter);
1520
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);
1521
1798
  return {
1522
1799
  id: draft.id,
1523
1800
  label,
1524
1801
  summary: draft.summary?.trim() || createViewpointSummary(label, objectId, filter),
1525
1802
  objectId,
1526
1803
  selectedObjectId,
1527
- projection: draft.projection ?? projection,
1804
+ eventIds: [...new Set(draft.eventIds ?? [])],
1805
+ projection: resolvedProjection,
1806
+ renderProjection,
1807
+ camera,
1528
1808
  preset: draft.preset ?? preset,
1529
1809
  rotationDeg: draft.rotationDeg ?? 0,
1530
1810
  scale: draft.scale ?? null,
@@ -1541,6 +1821,14 @@
1541
1821
  groupIds: []
1542
1822
  };
1543
1823
  }
1824
+ function createEmptyViewCamera() {
1825
+ return {
1826
+ azimuth: null,
1827
+ elevation: null,
1828
+ roll: null,
1829
+ distance: null
1830
+ };
1831
+ }
1544
1832
  function normalizeViewpointFilter(filter) {
1545
1833
  if (!filter) {
1546
1834
  return null;
@@ -1554,7 +1842,18 @@
1554
1842
  return normalized.query || normalized.objectTypes.length > 0 || normalized.tags.length > 0 || normalized.groupIds.length > 0 ? normalized : null;
1555
1843
  }
1556
1844
  function parseViewProjection(value) {
1557
- 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
+ }
1558
1857
  }
1559
1858
  function parseRenderPreset(value) {
1560
1859
  const normalized = value.toLowerCase();
@@ -1581,7 +1880,7 @@
1581
1880
  next["orbits-front"] = enabled;
1582
1881
  continue;
1583
1882
  }
1584
- if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1883
+ if (rawLayer === "background" || rawLayer === "guides" || rawLayer === "orbits-back" || rawLayer === "orbits-front" || rawLayer === "relations" || rawLayer === "events" || rawLayer === "objects" || rawLayer === "labels" || rawLayer === "metadata") {
1585
1884
  next[rawLayer] = enabled;
1586
1885
  }
1587
1886
  }
@@ -1592,7 +1891,7 @@
1592
1891
  }
1593
1892
  function parseViewpointGroups(value, document2, relationships, objectMap) {
1594
1893
  return splitListValue(value).map((entry) => {
1595
- 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)) {
1596
1895
  return entry;
1597
1896
  }
1598
1897
  if (entry.startsWith("wo-") && entry.endsWith("-group")) {
@@ -1629,7 +1928,7 @@
1629
1928
  }
1630
1929
  return parts.join(" - ");
1631
1930
  }
1632
- function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels) {
1931
+ function calculateContentBounds(width, height, objects, orbitVisuals, leaders, labels, labelMultiplier) {
1633
1932
  let minX = Number.POSITIVE_INFINITY;
1634
1933
  let minY = Number.POSITIVE_INFINITY;
1635
1934
  let maxX = Number.NEGATIVE_INFINITY;
@@ -1659,7 +1958,7 @@
1659
1958
  for (const label of labels) {
1660
1959
  if (label.hidden)
1661
1960
  continue;
1662
- includeLabelBounds(label, include);
1961
+ includeLabelBounds(label, include, labelMultiplier);
1663
1962
  }
1664
1963
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
1665
1964
  return createBounds(0, 0, width, height);
@@ -1697,13 +1996,10 @@
1697
1996
  include(object.x - object.visualRadius - 24, object.y - object.visualRadius - 16);
1698
1997
  include(object.x + object.visualRadius + 24, object.y + object.visualRadius + 36);
1699
1998
  }
1700
- function includeLabelBounds(label, include) {
1701
- const labelScale = 1;
1702
- const labelHalfWidth = estimateLabelHalfWidthFromText(label.label, label.secondaryLabel, labelScale);
1703
- include(label.x - labelHalfWidth, label.y - 18);
1704
- include(label.x + labelHalfWidth, label.y + 8);
1705
- include(label.x - labelHalfWidth, label.secondaryY - 14);
1706
- include(label.x + labelHalfWidth, label.secondaryY + 8);
1999
+ function includeLabelBounds(label, include, labelMultiplier) {
2000
+ const bounds = createLabelRectFromText(label.x, label.y, label.secondaryY, label.textAnchor, label.direction, label.label, label.secondaryLabel, labelMultiplier);
2001
+ include(bounds.left, bounds.top);
2002
+ include(bounds.right, bounds.bottom);
1707
2003
  }
1708
2004
  function placeObject(object, x, y, depth, positions, orbitDrafts, leaderDrafts, context) {
1709
2005
  if (positions.has(object.id)) {
@@ -2093,7 +2389,7 @@
2093
2389
  return null;
2094
2390
  }
2095
2391
  }
2096
- function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels) {
2392
+ function calculateGroupBounds(group, objects, orbitVisuals, leaders, labels, labelMultiplier) {
2097
2393
  let minX = Number.POSITIVE_INFINITY;
2098
2394
  let minY = Number.POSITIVE_INFINITY;
2099
2395
  let maxX = Number.NEGATIVE_INFINITY;
@@ -2122,7 +2418,7 @@
2122
2418
  }
2123
2419
  for (const label of labels) {
2124
2420
  if (!label.hidden && group.labelIds.includes(label.objectId)) {
2125
- includeLabelBounds(label, include);
2421
+ includeLabelBounds(label, include, labelMultiplier);
2126
2422
  }
2127
2423
  }
2128
2424
  if (!Number.isFinite(minX) || !Number.isFinite(minY)) {
@@ -2147,12 +2443,28 @@
2147
2443
  }
2148
2444
  return current.id;
2149
2445
  }
2150
- function createLabelRect(x, labelY, secondaryY, labelHalfWidth, direction) {
2446
+ function createLabelRect(object, placement, labelMultiplier) {
2447
+ return createLabelRectFromText(placement.x, placement.labelY, placement.secondaryY, placement.textAnchor, placement.direction, object.label, object.secondaryLabel, labelMultiplier);
2448
+ }
2449
+ function createLabelRectFromText(x, labelY, secondaryY, textAnchor, direction, label, secondaryLabel, labelMultiplier) {
2450
+ const labelHalfWidth = estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier);
2451
+ const labelWidth = labelHalfWidth * 2;
2452
+ const topPadding = direction === "above" ? 18 : 12;
2453
+ const bottomPadding = direction === "above" ? 8 : 12;
2454
+ let left = x - labelHalfWidth;
2455
+ let right = x + labelHalfWidth;
2456
+ if (textAnchor === "start") {
2457
+ left = x;
2458
+ right = x + labelWidth;
2459
+ } else if (textAnchor === "end") {
2460
+ left = x - labelWidth;
2461
+ right = x;
2462
+ }
2151
2463
  return {
2152
- left: x - labelHalfWidth,
2153
- right: x + labelHalfWidth,
2154
- top: Math.min(labelY, secondaryY) - (direction < 0 ? 18 : 12),
2155
- bottom: Math.max(labelY, secondaryY) + (direction < 0 ? 8 : 12)
2464
+ left,
2465
+ right,
2466
+ top: Math.min(labelY, secondaryY) - topPadding,
2467
+ bottom: Math.max(labelY, secondaryY) + bottomPadding
2156
2468
  };
2157
2469
  }
2158
2470
  function rectsOverlap(left, right) {
@@ -2339,11 +2651,6 @@
2339
2651
  function customColorFor(value) {
2340
2652
  return typeof value === "string" && value.trim() ? value : void 0;
2341
2653
  }
2342
- function estimateLabelHalfWidth(object, labelMultiplier) {
2343
- const primaryWidth = object.label.length * 4.6 * labelMultiplier + 18;
2344
- const secondaryWidth = object.secondaryLabel.length * 3.9 * labelMultiplier + 18;
2345
- return Math.max(primaryWidth, secondaryWidth, object.visualRadius + 18);
2346
- }
2347
2654
  function estimateLabelHalfWidthFromText(label, secondaryLabel, labelMultiplier) {
2348
2655
  const primaryWidth = label.length * 4.6 * labelMultiplier + 18;
2349
2656
  const secondaryWidth = secondaryLabel.length * 3.9 * labelMultiplier + 18;
@@ -2377,12 +2684,13 @@
2377
2684
  }
2378
2685
  return {
2379
2686
  format: "worldorbit",
2380
- version: "2.0",
2381
- schemaVersion: "2.0",
2687
+ version: "2.5",
2688
+ schemaVersion: "2.5",
2382
2689
  sourceVersion: document2.version,
2383
2690
  system,
2384
2691
  groups: structuredClone(document2.groups ?? []),
2385
2692
  relations: structuredClone(document2.relations ?? []),
2693
+ events: structuredClone(document2.events ?? []),
2386
2694
  objects: document2.objects.map(cloneWorldOrbitObject),
2387
2695
  diagnostics
2388
2696
  };
@@ -2390,7 +2698,7 @@
2390
2698
  function upgradeDocumentToDraftV2(document2, options = {}) {
2391
2699
  return convertAtlasDocumentToLegacyDraft(upgradeDocumentToV2(document2, options));
2392
2700
  }
2393
- function materializeAtlasDocument(document2) {
2701
+ function materializeAtlasDocument(document2, options = {}) {
2394
2702
  const system = document2.system ? {
2395
2703
  type: "system",
2396
2704
  id: document2.system.id,
@@ -2401,6 +2709,8 @@
2401
2709
  properties: materializeDraftSystemProperties(document2.system),
2402
2710
  info: materializeDraftSystemInfo(document2.system)
2403
2711
  } : null;
2712
+ const objects = document2.objects.map(cloneWorldOrbitObject);
2713
+ applyEventPoseOverrides(objects, document2.events ?? [], options.activeEventId ?? null);
2404
2714
  return {
2405
2715
  format: "worldorbit",
2406
2716
  version: "1.0",
@@ -2408,7 +2718,8 @@
2408
2718
  system,
2409
2719
  groups: structuredClone(document2.groups ?? []),
2410
2720
  relations: structuredClone(document2.relations ?? []),
2411
- objects: document2.objects.map(cloneWorldOrbitObject)
2721
+ events: document2.events.map(cloneWorldOrbitEvent),
2722
+ objects
2412
2723
  };
2413
2724
  }
2414
2725
  function createDraftSystem(document2, defaults, atlasMetadata, annotations, diagnostics, preset) {
@@ -2430,8 +2741,9 @@
2430
2741
  };
2431
2742
  }
2432
2743
  function createDraftDefaults(document2, preset, projection) {
2744
+ const rawView = typeof document2.system?.properties.view === "string" ? document2.system.properties.view.toLowerCase() : null;
2433
2745
  return {
2434
- 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,
2435
2747
  scale: typeof document2.system?.properties.scale === "string" ? document2.system.properties.scale : null,
2436
2748
  units: typeof document2.system?.properties.units === "string" ? document2.system.properties.units : null,
2437
2749
  preset,
@@ -2533,10 +2845,12 @@
2533
2845
  summary: viewpoint.summary,
2534
2846
  focusObjectId: viewpoint.objectId,
2535
2847
  selectedObjectId: viewpoint.selectedObjectId,
2848
+ events: [...viewpoint.eventIds],
2536
2849
  projection: viewpoint.projection,
2537
2850
  preset: viewpoint.preset,
2538
2851
  zoom: viewpoint.scale,
2539
2852
  rotationDeg: viewpoint.rotationDeg,
2853
+ camera: viewpoint.camera ? { ...viewpoint.camera } : null,
2540
2854
  layers: { ...viewpoint.layers },
2541
2855
  filter: viewpoint.filter ? {
2542
2856
  query: viewpoint.filter.query,
@@ -2565,6 +2879,75 @@
2565
2879
  info: { ...object.info }
2566
2880
  };
2567
2881
  }
2882
+ function cloneWorldOrbitEvent(event) {
2883
+ return {
2884
+ ...event,
2885
+ participantObjectIds: [...event.participantObjectIds],
2886
+ tags: [...event.tags],
2887
+ positions: event.positions.map(cloneWorldOrbitEventPose)
2888
+ };
2889
+ }
2890
+ function cloneWorldOrbitEventPose(pose) {
2891
+ return {
2892
+ objectId: pose.objectId,
2893
+ placement: clonePlacement(pose.placement),
2894
+ inner: pose.inner ? { ...pose.inner } : void 0,
2895
+ outer: pose.outer ? { ...pose.outer } : void 0,
2896
+ epoch: pose.epoch ?? null,
2897
+ referencePlane: pose.referencePlane ?? null
2898
+ };
2899
+ }
2900
+ function clonePlacement(placement) {
2901
+ return placement ? structuredClone(placement) : null;
2902
+ }
2903
+ function applyEventPoseOverrides(objects, events, activeEventId) {
2904
+ if (!activeEventId) {
2905
+ return;
2906
+ }
2907
+ const event = events.find((entry) => entry.id === activeEventId);
2908
+ if (!event) {
2909
+ return;
2910
+ }
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
+ }
2929
+ for (const pose of event.positions) {
2930
+ const object = objectMap.get(pose.objectId);
2931
+ if (!object) {
2932
+ continue;
2933
+ }
2934
+ if (pose.placement) {
2935
+ object.placement = clonePlacement(pose.placement);
2936
+ }
2937
+ if (pose.inner) {
2938
+ object.properties.inner = { ...pose.inner };
2939
+ }
2940
+ if (pose.outer) {
2941
+ object.properties.outer = { ...pose.outer };
2942
+ }
2943
+ if (pose.epoch) {
2944
+ object.epoch = pose.epoch;
2945
+ }
2946
+ if (pose.referencePlane) {
2947
+ object.referencePlane = pose.referencePlane;
2948
+ }
2949
+ }
2950
+ }
2568
2951
  function cloneProperties(properties) {
2569
2952
  const next = {};
2570
2953
  for (const [key, value] of Object.entries(properties)) {
@@ -2646,6 +3029,18 @@
2646
3029
  if (viewpoint.rotationDeg !== 0) {
2647
3030
  info2[`${prefix}.rotation`] = String(viewpoint.rotationDeg);
2648
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
+ }
2649
3044
  const serializedLayers = serializeViewpointLayers(viewpoint.layers);
2650
3045
  if (serializedLayers) {
2651
3046
  info2[`${prefix}.layers`] = serializedLayers;
@@ -2662,6 +3057,9 @@
2662
3057
  if ((viewpoint.filter?.groupIds.length ?? 0) > 0) {
2663
3058
  info2[`${prefix}.groups`] = viewpoint.filter?.groupIds.join(" ") ?? "";
2664
3059
  }
3060
+ if (viewpoint.events.length > 0) {
3061
+ info2[`${prefix}.events`] = viewpoint.events.join(" ");
3062
+ }
2665
3063
  }
2666
3064
  for (const annotation of system.annotations) {
2667
3065
  const prefix = `annotation.${annotation.id}`;
@@ -2686,7 +3084,7 @@
2686
3084
  if (orbitFront !== void 0 || orbitBack !== void 0) {
2687
3085
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
2688
3086
  }
2689
- for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
3087
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
2690
3088
  if (layers[key] !== void 0) {
2691
3089
  tokens.push(layers[key] ? key : `-${key}`);
2692
3090
  }
@@ -2739,26 +3137,26 @@
2739
3137
  ];
2740
3138
  function formatDocument(document2, options = {}) {
2741
3139
  const schema = options.schema ?? "auto";
2742
- 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";
2743
3141
  if (useDraft) {
2744
3142
  if (schema === "2.0-draft") {
2745
- 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" ? {
2746
3144
  ...document2,
2747
3145
  version: "2.0-draft",
2748
3146
  schemaVersion: "2.0-draft"
2749
3147
  } : upgradeDocumentToDraftV2(document2);
2750
3148
  return formatDraftDocument(legacyDraftDocument);
2751
3149
  }
2752
- 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" ? {
2753
3151
  ...document2,
2754
3152
  version: "2.0",
2755
3153
  schemaVersion: "2.0"
2756
3154
  } : upgradeDocumentToV2(document2);
2757
- if (schema === "2.1" && atlasDocument.version !== "2.1") {
3155
+ if ((schema === "2.0" || schema === "2.1" || schema === "2.5") && atlasDocument.version !== schema) {
2758
3156
  return formatAtlasDocument({
2759
3157
  ...atlasDocument,
2760
- version: "2.1",
2761
- schemaVersion: "2.1"
3158
+ version: schema,
3159
+ schemaVersion: schema
2762
3160
  });
2763
3161
  }
2764
3162
  return formatAtlasDocument(atlasDocument);
@@ -2790,6 +3188,10 @@
2790
3188
  lines.push("");
2791
3189
  lines.push(...formatAtlasRelation(relation));
2792
3190
  }
3191
+ for (const event of [...document2.events].sort(compareIdLike)) {
3192
+ lines.push("");
3193
+ lines.push(...formatAtlasEvent(event));
3194
+ }
2793
3195
  const sortedObjects = [...document2.objects].sort(compareObjects);
2794
3196
  if (sortedObjects.length > 0 && lines.at(-1) !== "") {
2795
3197
  lines.push("");
@@ -2820,6 +3222,10 @@
2820
3222
  lines.push("");
2821
3223
  lines.push(...formatAtlasRelation(relation));
2822
3224
  }
3225
+ for (const event of [...legacy.events].sort(compareIdLike)) {
3226
+ lines.push("");
3227
+ lines.push(...formatAtlasEvent(event));
3228
+ }
2823
3229
  const sortedObjects = [...legacy.objects].sort(compareObjects);
2824
3230
  if (sortedObjects.length > 0 && lines.at(-1) !== "") {
2825
3231
  lines.push("");
@@ -3027,10 +3433,28 @@
3027
3433
  if (viewpoint.rotationDeg !== 0) {
3028
3434
  lines.push(` rotation ${viewpoint.rotationDeg}`);
3029
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
+ }
3030
3451
  const layerTokens = formatDraftLayers(viewpoint.layers);
3031
3452
  if (layerTokens.length > 0) {
3032
3453
  lines.push(` layers ${layerTokens.join(" ")}`);
3033
3454
  }
3455
+ if (viewpoint.events.length > 0) {
3456
+ lines.push(` events ${viewpoint.events.join(" ")}`);
3457
+ }
3034
3458
  if (viewpoint.filter) {
3035
3459
  lines.push(" filter");
3036
3460
  if (viewpoint.filter.query) {
@@ -3103,6 +3527,65 @@
3103
3527
  }
3104
3528
  return lines;
3105
3529
  }
3530
+ function formatAtlasEvent(event) {
3531
+ const lines = [`event ${event.id}`, ` kind ${quoteIfNeeded(event.kind)}`];
3532
+ if (event.label) {
3533
+ lines.push(` label ${quoteIfNeeded(event.label)}`);
3534
+ }
3535
+ if (event.summary) {
3536
+ lines.push(` summary ${quoteIfNeeded(event.summary)}`);
3537
+ }
3538
+ if (event.targetObjectId) {
3539
+ lines.push(` target ${event.targetObjectId}`);
3540
+ }
3541
+ if (event.participantObjectIds.length > 0) {
3542
+ lines.push(` participants ${event.participantObjectIds.join(" ")}`);
3543
+ }
3544
+ if (event.timing) {
3545
+ lines.push(` timing ${quoteIfNeeded(event.timing)}`);
3546
+ }
3547
+ if (event.visibility) {
3548
+ lines.push(` visibility ${quoteIfNeeded(event.visibility)}`);
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
+ }
3556
+ if (event.tags.length > 0) {
3557
+ lines.push(` tags ${event.tags.map(quoteIfNeeded).join(" ")}`);
3558
+ }
3559
+ if (event.color) {
3560
+ lines.push(` color ${quoteIfNeeded(event.color)}`);
3561
+ }
3562
+ if (event.hidden) {
3563
+ lines.push(" hidden true");
3564
+ }
3565
+ if (event.positions.length > 0) {
3566
+ lines.push("");
3567
+ lines.push(" positions");
3568
+ for (const pose of [...event.positions].sort(comparePoseObjectId)) {
3569
+ lines.push(` pose ${pose.objectId}`);
3570
+ for (const fieldLine of formatEventPoseFields(pose)) {
3571
+ lines.push(` ${fieldLine}`);
3572
+ }
3573
+ }
3574
+ }
3575
+ return lines;
3576
+ }
3577
+ function formatEventPoseFields(pose) {
3578
+ return [
3579
+ ...formatPlacement(pose.placement),
3580
+ ...pose.epoch ? [`epoch ${quoteIfNeeded(pose.epoch)}`] : [],
3581
+ ...pose.referencePlane ? [`referencePlane ${quoteIfNeeded(pose.referencePlane)}`] : [],
3582
+ ...formatOptionalUnit("inner", pose.inner),
3583
+ ...formatOptionalUnit("outer", pose.outer)
3584
+ ];
3585
+ }
3586
+ function hasCameraValues(camera) {
3587
+ return camera.azimuth !== null || camera.elevation !== null || camera.roll !== null || camera.distance !== null;
3588
+ }
3106
3589
  function formatValue(value) {
3107
3590
  if (Array.isArray(value)) {
3108
3591
  return value.map((item) => quoteIfNeeded(item)).join(" ");
@@ -3144,7 +3627,7 @@
3144
3627
  if (orbitFront !== void 0 || orbitBack !== void 0) {
3145
3628
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
3146
3629
  }
3147
- for (const key of ["background", "guides", "relations", "objects", "labels", "metadata"]) {
3630
+ for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
3148
3631
  if (layers[key] !== void 0) {
3149
3632
  tokens.push(layers[key] ? key : `-${key}`);
3150
3633
  }
@@ -3172,6 +3655,9 @@
3172
3655
  function compareIdLike(left, right) {
3173
3656
  return left.id.localeCompare(right.id);
3174
3657
  }
3658
+ function comparePoseObjectId(left, right) {
3659
+ return left.objectId.localeCompare(right.objectId);
3660
+ }
3175
3661
  function objectTypeIndex(objectType) {
3176
3662
  switch (objectType) {
3177
3663
  case "star":
@@ -3367,6 +3853,7 @@
3367
3853
  const diagnostics = [];
3368
3854
  const objectMap = new Map(document2.objects.map((object) => [object.id, object]));
3369
3855
  const groupIds = new Set(document2.groups.map((group) => group.id));
3856
+ const eventIds = new Set(document2.events.map((event) => event.id));
3370
3857
  if (!document2.system) {
3371
3858
  diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
3372
3859
  }
@@ -3376,6 +3863,7 @@
3376
3863
  ["viewpoint", document2.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []],
3377
3864
  ["annotation", document2.system?.annotations.map((annotation) => annotation.id) ?? []],
3378
3865
  ["relation", document2.relations.map((relation) => relation.id)],
3866
+ ["event", document2.events.map((event) => event.id)],
3379
3867
  ["object", document2.objects.map((object) => object.id)]
3380
3868
  ]) {
3381
3869
  for (const id of ids) {
@@ -3391,11 +3879,14 @@
3391
3879
  validateRelation(relation, objectMap, diagnostics);
3392
3880
  }
3393
3881
  for (const viewpoint of document2.system?.viewpoints ?? []) {
3394
- validateViewpointFilter(viewpoint.filter, groupIds, sourceSchemaVersion, diagnostics, viewpoint.id);
3882
+ validateViewpoint(viewpoint, groupIds, eventIds, sourceSchemaVersion, diagnostics, objectMap);
3395
3883
  }
3396
3884
  for (const object of document2.objects) {
3397
3885
  validateObject(object, document2.system, objectMap, groupIds, diagnostics);
3398
3886
  }
3887
+ for (const event of document2.events) {
3888
+ validateEvent(event, document2.system, objectMap, diagnostics);
3889
+ }
3399
3890
  return diagnostics;
3400
3891
  }
3401
3892
  function validateRelation(relation, objectMap, diagnostics) {
@@ -3413,15 +3904,24 @@
3413
3904
  diagnostics.push(error("validate.relation.kind.required", `Relation "${relation.id}" is missing a "kind" value.`));
3414
3905
  }
3415
3906
  }
3416
- function validateViewpointFilter(filter, groupIds, sourceSchemaVersion, diagnostics, viewpointId) {
3417
- if (!filter || sourceSchemaVersion !== "2.1") {
3418
- return;
3419
- }
3420
- for (const groupId of filter.groupIds) {
3421
- if (!groupIds.has(groupId)) {
3422
- diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpointId}".`));
3907
+ function validateViewpoint(viewpoint, groupIds, eventIds, sourceSchemaVersion, diagnostics, objectMap) {
3908
+ const filter = viewpoint.filter;
3909
+ if (sourceSchemaVersion === "2.1" || sourceSchemaVersion === "2.5") {
3910
+ if (filter) {
3911
+ for (const groupId of filter.groupIds) {
3912
+ if (!groupIds.has(groupId)) {
3913
+ diagnostics.push(warn("validate.viewpoint.group.unknown", `Unknown group "${groupId}" in viewpoint "${viewpoint.id}".`, void 0, `viewpoint.${viewpoint.id}.groups`));
3914
+ }
3915
+ }
3916
+ }
3917
+ for (const eventId of viewpoint.events ?? []) {
3918
+ if (!eventIds.has(eventId)) {
3919
+ diagnostics.push(warn("validate.viewpoint.event.unknown", `Unknown event "${eventId}" in viewpoint "${viewpoint.id}".`, void 0, `viewpoint.${viewpoint.id}.events`));
3920
+ }
3423
3921
  }
3424
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);
3425
3925
  }
3426
3926
  function validateObject(object, system, objectMap, groupIds, diagnostics) {
3427
3927
  const placement = object.placement;
@@ -3434,6 +3934,12 @@
3434
3934
  }
3435
3935
  }
3436
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
+ }
3437
3943
  if (orbitPlacement) {
3438
3944
  if (!objectMap.has(orbitPlacement.target)) {
3439
3945
  diagnostics.push(error("validate.orbit.target.unknown", `Unknown placement target "${orbitPlacement.target}" on "${object.id}".`, object.id, "orbit"));
@@ -3505,6 +4011,122 @@
3505
4011
  }
3506
4012
  }
3507
4013
  }
4014
+ function validateEvent(event, system, objectMap, diagnostics) {
4015
+ const fieldPrefix = `event.${event.id}`;
4016
+ const referencedIds = /* @__PURE__ */ new Set();
4017
+ if (!event.kind.trim()) {
4018
+ diagnostics.push(error("validate.event.kind.required", `Event "${event.id}" is missing a "kind" value.`, void 0, `${fieldPrefix}.kind`));
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
+ }
4026
+ if (!event.targetObjectId && event.participantObjectIds.length === 0) {
4027
+ diagnostics.push(error("validate.event.references.required", `Event "${event.id}" must define a "target" or at least one participant.`, void 0, `${fieldPrefix}.participants`));
4028
+ }
4029
+ if (event.targetObjectId) {
4030
+ referencedIds.add(event.targetObjectId);
4031
+ if (!objectMap.has(event.targetObjectId)) {
4032
+ diagnostics.push(error("validate.event.target.unknown", `Unknown event target "${event.targetObjectId}" on "${event.id}".`, void 0, `${fieldPrefix}.target`));
4033
+ }
4034
+ }
4035
+ const seenParticipants = /* @__PURE__ */ new Set();
4036
+ for (const participantId of event.participantObjectIds) {
4037
+ referencedIds.add(participantId);
4038
+ if (seenParticipants.has(participantId)) {
4039
+ diagnostics.push(warn("validate.event.participants.duplicate", `Event "${event.id}" repeats participant "${participantId}".`, void 0, `${fieldPrefix}.participants`));
4040
+ continue;
4041
+ }
4042
+ seenParticipants.add(participantId);
4043
+ if (!objectMap.has(participantId)) {
4044
+ diagnostics.push(error("validate.event.participants.unknown", `Unknown event participant "${participantId}" on "${event.id}".`, void 0, `${fieldPrefix}.participants`));
4045
+ }
4046
+ }
4047
+ if (event.targetObjectId && event.participantObjectIds.length > 0 && !event.participantObjectIds.includes(event.targetObjectId)) {
4048
+ diagnostics.push(warn("validate.event.target.notParticipant", `Event "${event.id}" defines a target outside its participants list.`, void 0, `${fieldPrefix}.target`));
4049
+ }
4050
+ if (event.positions.length === 0) {
4051
+ diagnostics.push(warn("validate.event.positions.missing", `Event "${event.id}" has no positions block and cannot drive a scene snapshot.`, void 0, `${fieldPrefix}.positions`));
4052
+ }
4053
+ if (/(?:^|[-_])(solar-eclipse|lunar-eclipse|transit|occultation)(?:$|[-_])/.test(event.kind) && referencedIds.size < 3) {
4054
+ diagnostics.push(warn("validate.event.kind.participants", `Event "${event.id}" looks like an eclipse or transit but references fewer than three bodies.`, void 0, `${fieldPrefix}.participants`));
4055
+ }
4056
+ const poseIds = /* @__PURE__ */ new Set();
4057
+ for (const pose of event.positions) {
4058
+ const poseFieldPrefix = `${fieldPrefix}.pose.${pose.objectId}`;
4059
+ if (poseIds.has(pose.objectId)) {
4060
+ diagnostics.push(error("validate.event.pose.duplicate", `Event "${event.id}" defines "${pose.objectId}" more than once in positions.`, void 0, poseFieldPrefix));
4061
+ continue;
4062
+ }
4063
+ poseIds.add(pose.objectId);
4064
+ const object = objectMap.get(pose.objectId);
4065
+ if (!object) {
4066
+ diagnostics.push(error("validate.event.pose.object.unknown", `Unknown event pose object "${pose.objectId}" on "${event.id}".`, void 0, poseFieldPrefix));
4067
+ continue;
4068
+ }
4069
+ if (!referencedIds.has(pose.objectId)) {
4070
+ diagnostics.push(warn("validate.event.pose.unreferenced", `Event pose "${pose.objectId}" on "${event.id}" is not listed in target/participants.`, void 0, poseFieldPrefix));
4071
+ }
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`));
4077
+ }
4078
+ }
4079
+ function validateEventPose(pose, object, event, system, objectMap, diagnostics, fieldPrefix, eventId) {
4080
+ const placement = pose.placement;
4081
+ if (!placement) {
4082
+ diagnostics.push(error("validate.event.pose.placement.required", `Event "${eventId}" pose "${pose.objectId}" is missing a placement mode.`, void 0, fieldPrefix));
4083
+ return;
4084
+ }
4085
+ if (placement.mode === "orbit") {
4086
+ if (!objectMap.has(placement.target)) {
4087
+ diagnostics.push(error("validate.event.pose.orbit.target.unknown", `Unknown event orbit target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.orbit`));
4088
+ }
4089
+ if (placement.distance && placement.semiMajor) {
4090
+ diagnostics.push(error("validate.event.pose.orbit.distanceConflict", `Event "${eventId}" pose "${pose.objectId}" cannot declare both "distance" and "semiMajor".`, void 0, `${fieldPrefix}.distance`));
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
+ }
4101
+ return;
4102
+ }
4103
+ if (placement.mode === "surface") {
4104
+ const target = objectMap.get(placement.target);
4105
+ if (!target) {
4106
+ diagnostics.push(error("validate.event.pose.surface.target.unknown", `Unknown event surface target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.surface`));
4107
+ } else if (!SURFACE_TARGET_TYPES2.has(target.type)) {
4108
+ diagnostics.push(error("validate.event.pose.surface.target.invalid", `Event surface target "${placement.target}" on "${eventId}:${pose.objectId}" is not surface-capable.`, void 0, `${fieldPrefix}.surface`));
4109
+ }
4110
+ return;
4111
+ }
4112
+ if (placement.mode === "at") {
4113
+ if (object.type !== "structure" && object.type !== "phenomenon") {
4114
+ diagnostics.push(error("validate.event.pose.at.objectType", `Only structures and phenomena may use "at" placement in events; found "${object.type}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
4115
+ }
4116
+ const reference = placement.reference;
4117
+ if (reference.kind === "named" && !objectMap.has(reference.name)) {
4118
+ diagnostics.push(error("validate.event.pose.at.target.unknown", `Unknown event at-reference target "${placement.target}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
4119
+ } else if (reference.kind === "anchor" && !objectMap.has(reference.objectId)) {
4120
+ diagnostics.push(error("validate.event.pose.anchor.target.unknown", `Unknown event anchor target "${reference.objectId}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
4121
+ } else if (reference.kind === "lagrange") {
4122
+ if (!objectMap.has(reference.primary)) {
4123
+ diagnostics.push(error("validate.event.pose.lagrange.primary.unknown", `Unknown event Lagrange target "${reference.primary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
4124
+ } else if (reference.secondary && !objectMap.has(reference.secondary)) {
4125
+ diagnostics.push(error("validate.event.pose.lagrange.secondary.unknown", `Unknown event Lagrange target "${reference.secondary}" on "${eventId}:${pose.objectId}".`, void 0, `${fieldPrefix}.at`));
4126
+ }
4127
+ }
4128
+ }
4129
+ }
3508
4130
  function validateAtTarget(object, objectMap, diagnostics) {
3509
4131
  const reference = object.placement?.mode === "at" ? object.placement.reference : null;
3510
4132
  if (!reference) {
@@ -3610,6 +4232,52 @@
3610
4232
  return null;
3611
4233
  }
3612
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
+ }
3613
4281
  function toleranceForField(object, field) {
3614
4282
  const tolerance = object.tolerances?.find((entry) => entry.field === field)?.value;
3615
4283
  if (typeof tolerance === "number") {
@@ -3705,6 +4373,23 @@
3705
4373
  });
3706
4374
  }
3707
4375
  var DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
4376
+ var EVENT_POSE_FIELD_KEYS = /* @__PURE__ */ new Set([
4377
+ "orbit",
4378
+ "distance",
4379
+ "semiMajor",
4380
+ "eccentricity",
4381
+ "period",
4382
+ "angle",
4383
+ "inclination",
4384
+ "phase",
4385
+ "at",
4386
+ "surface",
4387
+ "free",
4388
+ "inner",
4389
+ "outer",
4390
+ "epoch",
4391
+ "referencePlane"
4392
+ ]);
3708
4393
  function parseWorldOrbitAtlas(source) {
3709
4394
  return parseAtlasSource(source);
3710
4395
  }
@@ -3719,12 +4404,15 @@
3719
4404
  const objectNodes = [];
3720
4405
  const groups = [];
3721
4406
  const relations = [];
4407
+ const events = [];
4408
+ const eventPoseNodes = /* @__PURE__ */ new Map();
3722
4409
  let sawDefaults = false;
3723
4410
  let sawAtlas = false;
3724
4411
  const viewpointIds = /* @__PURE__ */ new Set();
3725
4412
  const annotationIds = /* @__PURE__ */ new Set();
3726
4413
  const groupIds = /* @__PURE__ */ new Set();
3727
4414
  const relationIds = /* @__PURE__ */ new Set();
4415
+ const eventIds = /* @__PURE__ */ new Set();
3728
4416
  for (let index = 0; index < lines.length; index++) {
3729
4417
  const rawLine = lines[index];
3730
4418
  const lineNumber = index + 1;
@@ -3742,7 +4430,7 @@
3742
4430
  if (!sawSchemaHeader) {
3743
4431
  sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
3744
4432
  sawSchemaHeader = true;
3745
- if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
4433
+ if (prepared.comments.length > 0 && isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
3746
4434
  diagnostics.push({
3747
4435
  code: "parse.schema21.commentCompatibility",
3748
4436
  severity: "warning",
@@ -3755,7 +4443,7 @@
3755
4443
  continue;
3756
4444
  }
3757
4445
  if (indent === 0) {
3758
- section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, { sawDefaults, sawAtlas });
4446
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, { sawDefaults, sawAtlas });
3759
4447
  if (section.kind === "system") {
3760
4448
  system = section.system;
3761
4449
  } else if (section.kind === "defaults") {
@@ -3774,6 +4462,7 @@
3774
4462
  throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
3775
4463
  }
3776
4464
  const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
4465
+ const normalizedEvents = events.map((event) => normalizeDraftEvent(event, eventPoseNodes.get(event.id) ?? []));
3777
4466
  const outputVersion = forcedOutputVersion ?? (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
3778
4467
  const baseDocument = {
3779
4468
  format: "worldorbit",
@@ -3781,6 +4470,7 @@
3781
4470
  system,
3782
4471
  groups,
3783
4472
  relations,
4473
+ events: normalizedEvents,
3784
4474
  objects,
3785
4475
  diagnostics
3786
4476
  };
@@ -3810,13 +4500,13 @@
3810
4500
  return document2;
3811
4501
  }
3812
4502
  function assertDraftSchemaHeader(tokens, line) {
3813
- if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "schema" || !["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
3814
- 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);
3815
4505
  }
3816
4506
  const version = tokens[1].value.toLowerCase();
3817
- 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";
3818
4508
  }
3819
- function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, flags) {
4509
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, flags) {
3820
4510
  const keyword = tokens[0]?.value.toLowerCase();
3821
4511
  switch (keyword) {
3822
4512
  case "system":
@@ -3834,6 +4524,8 @@
3834
4524
  return {
3835
4525
  kind: "defaults",
3836
4526
  system,
4527
+ sourceSchemaVersion,
4528
+ diagnostics,
3837
4529
  seenFields: /* @__PURE__ */ new Set()
3838
4530
  };
3839
4531
  case "atlas":
@@ -3853,7 +4545,7 @@
3853
4545
  if (!system) {
3854
4546
  throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
3855
4547
  }
3856
- return startViewpointSection(tokens, line, system, viewpointIds);
4548
+ return startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics);
3857
4549
  case "annotation":
3858
4550
  if (!system) {
3859
4551
  throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
@@ -3865,6 +4557,9 @@
3865
4557
  case "relation":
3866
4558
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
3867
4559
  return startRelationSection(tokens, line, relations, relationIds);
4560
+ case "event":
4561
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "event", { line, column: tokens[0].column });
4562
+ return startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics);
3868
4563
  case "object":
3869
4564
  return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
3870
4565
  default:
@@ -3901,7 +4596,7 @@
3901
4596
  seenFields: /* @__PURE__ */ new Set()
3902
4597
  };
3903
4598
  }
3904
- function startViewpointSection(tokens, line, system, viewpointIds) {
4599
+ function startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics) {
3905
4600
  if (tokens.length !== 2) {
3906
4601
  throw new WorldOrbitError("Invalid viewpoint declaration", line, tokens[0]?.column ?? 1);
3907
4602
  }
@@ -3918,10 +4613,12 @@
3918
4613
  summary: "",
3919
4614
  focusObjectId: null,
3920
4615
  selectedObjectId: null,
4616
+ events: [],
3921
4617
  projection: system.defaults.view,
3922
4618
  preset: system.defaults.preset,
3923
4619
  zoom: null,
3924
4620
  rotationDeg: 0,
4621
+ camera: null,
3925
4622
  layers: {},
3926
4623
  filter: null
3927
4624
  };
@@ -3930,10 +4627,15 @@
3930
4627
  return {
3931
4628
  kind: "viewpoint",
3932
4629
  viewpoint,
4630
+ sourceSchemaVersion,
4631
+ diagnostics,
3933
4632
  seenFields: /* @__PURE__ */ new Set(),
3934
4633
  inFilter: false,
3935
4634
  filterIndent: null,
3936
- seenFilterFields: /* @__PURE__ */ new Set()
4635
+ seenFilterFields: /* @__PURE__ */ new Set(),
4636
+ inCamera: false,
4637
+ cameraIndent: null,
4638
+ seenCameraFields: /* @__PURE__ */ new Set()
3937
4639
  };
3938
4640
  }
3939
4641
  function startAnnotationSection(tokens, line, system, annotationIds) {
@@ -4020,6 +4722,51 @@
4020
4722
  seenFields: /* @__PURE__ */ new Set()
4021
4723
  };
4022
4724
  }
4725
+ function startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics) {
4726
+ if (tokens.length !== 2) {
4727
+ throw new WorldOrbitError("Invalid event declaration", line, tokens[0]?.column ?? 1);
4728
+ }
4729
+ const id = normalizeIdentifier2(tokens[1].value);
4730
+ if (!id) {
4731
+ throw new WorldOrbitError("Event id must not be empty", line, tokens[1].column);
4732
+ }
4733
+ if (eventIds.has(id)) {
4734
+ throw new WorldOrbitError(`Duplicate event id "${id}"`, line, tokens[1].column);
4735
+ }
4736
+ const event = {
4737
+ id,
4738
+ kind: "",
4739
+ label: humanizeIdentifier3(id),
4740
+ summary: null,
4741
+ targetObjectId: null,
4742
+ participantObjectIds: [],
4743
+ timing: null,
4744
+ visibility: null,
4745
+ epoch: null,
4746
+ referencePlane: null,
4747
+ tags: [],
4748
+ color: null,
4749
+ hidden: false,
4750
+ positions: []
4751
+ };
4752
+ const rawPoses = [];
4753
+ events.push(event);
4754
+ eventPoseNodes.set(id, rawPoses);
4755
+ eventIds.add(id);
4756
+ return {
4757
+ kind: "event",
4758
+ event,
4759
+ sourceSchemaVersion,
4760
+ diagnostics,
4761
+ seenFields: /* @__PURE__ */ new Set(),
4762
+ rawPoses,
4763
+ inPositions: false,
4764
+ positionsIndent: null,
4765
+ activePose: null,
4766
+ poseIndent: null,
4767
+ activePoseSeenFields: /* @__PURE__ */ new Set()
4768
+ };
4769
+ }
4023
4770
  function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
4024
4771
  if (tokens.length < 3) {
4025
4772
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
@@ -4076,6 +4823,9 @@
4076
4823
  case "relation":
4077
4824
  applyRelationField(section, tokens, line);
4078
4825
  return;
4826
+ case "event":
4827
+ applyEventField(section, indent, tokens, line);
4828
+ return;
4079
4829
  case "object":
4080
4830
  applyObjectField(section, indent, tokens, line);
4081
4831
  return;
@@ -4118,6 +4868,12 @@
4118
4868
  const value = joinFieldValue(tokens, line);
4119
4869
  switch (key) {
4120
4870
  case "view":
4871
+ if (isSchema25Projection(value)) {
4872
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "defaults.view", {
4873
+ line,
4874
+ column: tokens[0].column
4875
+ });
4876
+ }
4121
4877
  section.system.defaults.view = parseProjectionValue(value, line, tokens[0].column);
4122
4878
  return;
4123
4879
  case "scale":
@@ -4157,14 +4913,36 @@
4157
4913
  throw new WorldOrbitError(`Unknown atlas field "${tokens[0].value}"`, line, tokens[0].column);
4158
4914
  }
4159
4915
  function applyViewpointField2(section, indent, tokens, line) {
4916
+ if (section.inCamera && indent <= (section.cameraIndent ?? 0)) {
4917
+ section.inCamera = false;
4918
+ section.cameraIndent = null;
4919
+ }
4160
4920
  if (section.inFilter && indent <= (section.filterIndent ?? 0)) {
4161
4921
  section.inFilter = false;
4162
4922
  section.filterIndent = null;
4163
4923
  }
4924
+ if (section.inCamera) {
4925
+ applyViewpointCameraField(section, tokens, line);
4926
+ return;
4927
+ }
4164
4928
  if (section.inFilter) {
4165
4929
  applyViewpointFilterField(section, tokens, line);
4166
4930
  return;
4167
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
+ }
4168
4946
  if (tokens.length === 1 && tokens[0].value.toLowerCase() === "filter") {
4169
4947
  if (section.seenFields.has("filter")) {
4170
4948
  throw new WorldOrbitError('Duplicate viewpoint field "filter"', line, tokens[0].column);
@@ -4190,6 +4968,12 @@
4190
4968
  section.viewpoint.selectedObjectId = value;
4191
4969
  return;
4192
4970
  case "projection":
4971
+ if (isSchema25Projection(value)) {
4972
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "projection", {
4973
+ line,
4974
+ column: tokens[0].column
4975
+ });
4976
+ }
4193
4977
  section.viewpoint.projection = parseProjectionValue(value, line, tokens[0].column);
4194
4978
  return;
4195
4979
  case "preset":
@@ -4201,13 +4985,49 @@
4201
4985
  case "rotation":
4202
4986
  section.viewpoint.rotationDeg = parseFiniteNumber2(value, line, tokens[0].column, "rotation");
4203
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;
4204
4995
  case "layers":
4205
- section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line);
4996
+ section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line, section.sourceSchemaVersion, section.diagnostics);
4997
+ return;
4998
+ case "events":
4999
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.events", {
5000
+ line,
5001
+ column: tokens[0].column
5002
+ });
5003
+ section.viewpoint.events = parseTokenList(tokens.slice(1), line, "events");
4206
5004
  return;
4207
5005
  default:
4208
5006
  throw new WorldOrbitError(`Unknown viewpoint field "${tokens[0].value}"`, line, tokens[0].column);
4209
5007
  }
4210
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
+ }
4211
5031
  function applyViewpointFilterField(section, tokens, line) {
4212
5032
  const key = requireUniqueField(tokens, section.seenFilterFields, line);
4213
5033
  const filter = section.viewpoint.filter ?? createEmptyViewpointFilter2();
@@ -4307,6 +5127,126 @@
4307
5127
  throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
4308
5128
  }
4309
5129
  }
5130
+ function applyEventField(section, indent, tokens, line) {
5131
+ if (section.activePose && indent <= (section.poseIndent ?? 0)) {
5132
+ section.activePose = null;
5133
+ section.poseIndent = null;
5134
+ section.activePoseSeenFields.clear();
5135
+ }
5136
+ if (!section.activePose && section.inPositions && indent <= (section.positionsIndent ?? 0)) {
5137
+ section.inPositions = false;
5138
+ section.positionsIndent = null;
5139
+ }
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
+ }
5147
+ section.activePose.fields.push(parseEventPoseField(tokens, line, section.activePoseSeenFields));
5148
+ return;
5149
+ }
5150
+ if (section.inPositions) {
5151
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "pose") {
5152
+ throw new WorldOrbitError(`Unknown event positions field "${tokens[0].value}"`, line, tokens[0]?.column ?? 1);
5153
+ }
5154
+ const objectId = tokens[1].value;
5155
+ if (!objectId.trim()) {
5156
+ throw new WorldOrbitError("Event pose object id must not be empty", line, tokens[1].column);
5157
+ }
5158
+ const rawPose = {
5159
+ objectId,
5160
+ fields: [],
5161
+ location: { line, column: tokens[0].column }
5162
+ };
5163
+ section.rawPoses.push(rawPose);
5164
+ section.activePose = rawPose;
5165
+ section.poseIndent = indent;
5166
+ section.activePoseSeenFields = /* @__PURE__ */ new Set();
5167
+ return;
5168
+ }
5169
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "positions") {
5170
+ if (section.seenFields.has("positions")) {
5171
+ throw new WorldOrbitError('Duplicate event field "positions"', line, tokens[0].column);
5172
+ }
5173
+ section.seenFields.add("positions");
5174
+ section.inPositions = true;
5175
+ section.positionsIndent = indent;
5176
+ return;
5177
+ }
5178
+ const key = requireUniqueField(tokens, section.seenFields, line);
5179
+ switch (key) {
5180
+ case "kind":
5181
+ section.event.kind = joinFieldValue(tokens, line);
5182
+ return;
5183
+ case "label":
5184
+ section.event.label = joinFieldValue(tokens, line);
5185
+ return;
5186
+ case "summary":
5187
+ section.event.summary = joinFieldValue(tokens, line);
5188
+ return;
5189
+ case "target":
5190
+ section.event.targetObjectId = joinFieldValue(tokens, line);
5191
+ return;
5192
+ case "participants":
5193
+ section.event.participantObjectIds = parseTokenList(tokens.slice(1), line, "participants");
5194
+ return;
5195
+ case "timing":
5196
+ section.event.timing = joinFieldValue(tokens, line);
5197
+ return;
5198
+ case "visibility":
5199
+ section.event.visibility = joinFieldValue(tokens, line);
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;
5215
+ case "tags":
5216
+ section.event.tags = parseTokenList(tokens.slice(1), line, "tags");
5217
+ return;
5218
+ case "color":
5219
+ section.event.color = joinFieldValue(tokens, line);
5220
+ return;
5221
+ case "hidden":
5222
+ section.event.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
5223
+ line,
5224
+ column: tokens[0].column
5225
+ });
5226
+ return;
5227
+ default:
5228
+ throw new WorldOrbitError(`Unknown event field "${tokens[0].value}"`, line, tokens[0].column);
5229
+ }
5230
+ }
5231
+ function parseEventPoseField(tokens, line, seenFields) {
5232
+ if (tokens.length < 2) {
5233
+ throw new WorldOrbitError("Invalid event pose field line", line, tokens[0]?.column ?? 1);
5234
+ }
5235
+ const key = tokens[0].value;
5236
+ if (!EVENT_POSE_FIELD_KEYS.has(key)) {
5237
+ throw new WorldOrbitError(`Unknown event pose field "${key}"`, line, tokens[0].column);
5238
+ }
5239
+ if (seenFields.has(key)) {
5240
+ throw new WorldOrbitError(`Duplicate event pose field "${key}"`, line, tokens[0].column);
5241
+ }
5242
+ seenFields.add(key);
5243
+ return {
5244
+ type: "field",
5245
+ key,
5246
+ values: tokens.slice(1).map((token) => token.value),
5247
+ location: { line, column: tokens[0].column }
5248
+ };
5249
+ }
4310
5250
  function applyObjectField(section, indent, tokens, line) {
4311
5251
  if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
4312
5252
  section.activeBlock = null;
@@ -4365,7 +5305,7 @@
4365
5305
  function parseObjectTypeTokens(tokens, line) {
4366
5306
  return parseTokenList(tokens, line, "objectTypes").filter((value) => value === "star" || value === "planet" || value === "moon" || value === "belt" || value === "asteroid" || value === "comet" || value === "ring" || value === "structure" || value === "phenomenon");
4367
5307
  }
4368
- function parseLayerTokens(tokens, line) {
5308
+ function parseLayerTokens(tokens, line, sourceSchemaVersion, diagnostics) {
4369
5309
  const layers = {};
4370
5310
  for (const token of parseTokenList(tokens, line, "layers")) {
4371
5311
  const enabled = !token.startsWith("-") && !token.startsWith("!");
@@ -4375,7 +5315,13 @@
4375
5315
  layers["orbits-front"] = enabled;
4376
5316
  continue;
4377
5317
  }
4378
- if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "objects" || raw === "labels" || raw === "metadata") {
5318
+ if (raw === "background" || raw === "guides" || raw === "orbits-back" || raw === "orbits-front" || raw === "relations" || raw === "events" || raw === "objects" || raw === "labels" || raw === "metadata") {
5319
+ if (raw === "events" && sourceSchemaVersion && diagnostics) {
5320
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "layers.events", {
5321
+ line,
5322
+ column: tokens[0]?.column ?? 1
5323
+ });
5324
+ }
4379
5325
  layers[raw] = enabled;
4380
5326
  }
4381
5327
  }
@@ -4393,11 +5339,15 @@
4393
5339
  }
4394
5340
  function parseProjectionValue(value, line, column) {
4395
5341
  const normalized = value.toLowerCase();
4396
- if (normalized !== "topdown" && normalized !== "isometric") {
5342
+ if (normalized !== "topdown" && normalized !== "isometric" && normalized !== "orthographic" && normalized !== "perspective") {
4397
5343
  throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
4398
5344
  }
4399
5345
  return normalized;
4400
5346
  }
5347
+ function isSchema25Projection(value) {
5348
+ const normalized = value.toLowerCase();
5349
+ return normalized === "orthographic" || normalized === "perspective";
5350
+ }
4401
5351
  function parsePresetValue(value, line, column) {
4402
5352
  const normalized = value.toLowerCase();
4403
5353
  if (normalized === "diagram" || normalized === "presentation" || normalized === "atlas-card" || normalized === "markdown") {
@@ -4427,6 +5377,48 @@
4427
5377
  groupIds: []
4428
5378
  };
4429
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
+ }
4430
5422
  function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
4431
5423
  const fields = [];
4432
5424
  let index = 0;
@@ -4514,7 +5506,7 @@
4514
5506
  }
4515
5507
  function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
4516
5508
  const fieldMap = collectDraftFields(node.fields);
4517
- const placement = extractDraftPlacement(node.objectType, fieldMap);
5509
+ const placement = extractPlacementFromFieldMap(fieldMap);
4518
5510
  const properties = normalizeDraftProperties(node.objectType, fieldMap);
4519
5511
  const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
4520
5512
  const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
@@ -4559,21 +5551,41 @@
4559
5551
  object.tolerances = tolerances;
4560
5552
  if (typedBlocks && Object.keys(typedBlocks).length > 0)
4561
5553
  object.typedBlocks = typedBlocks;
4562
- if (sourceSchemaVersion !== "2.1") {
5554
+ if (isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
4563
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) {
4564
5556
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
4565
5557
  }
4566
5558
  }
4567
5559
  return object;
4568
5560
  }
4569
- function collectDraftFields(fields) {
5561
+ function normalizeDraftEvent(event, rawPoses) {
5562
+ return {
5563
+ ...event,
5564
+ participantObjectIds: [...new Set(event.participantObjectIds)],
5565
+ tags: [...new Set(event.tags)],
5566
+ positions: rawPoses.map((pose) => normalizeDraftEventPose(pose))
5567
+ };
5568
+ }
5569
+ function normalizeDraftEventPose(rawPose) {
5570
+ const fieldMap = collectDraftFields(rawPose.fields, "event-pose");
5571
+ const placement = extractPlacementFromFieldMap(fieldMap);
5572
+ return {
5573
+ objectId: rawPose.objectId,
5574
+ placement,
5575
+ inner: parseOptionalUnitField(fieldMap.get("inner")?.[0], "inner"),
5576
+ outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer"),
5577
+ epoch: parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]),
5578
+ referencePlane: parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0])
5579
+ };
5580
+ }
5581
+ function collectDraftFields(fields, _mode = "object") {
4570
5582
  const grouped = /* @__PURE__ */ new Map();
4571
5583
  for (const field of fields) {
4572
5584
  const spec = getDraftObjectFieldSpec(field.key);
4573
- if (!spec) {
5585
+ if (!spec && !EVENT_POSE_FIELD_KEYS.has(field.key)) {
4574
5586
  throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
4575
5587
  }
4576
- if (!spec.allowRepeat && grouped.has(field.key)) {
5588
+ if (!spec?.allowRepeat && grouped.has(field.key)) {
4577
5589
  throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
4578
5590
  }
4579
5591
  const existing = grouped.get(field.key) ?? [];
@@ -4582,7 +5594,7 @@
4582
5594
  }
4583
5595
  return grouped;
4584
5596
  }
4585
- function extractDraftPlacement(objectType, fieldMap) {
5597
+ function extractPlacementFromFieldMap(fieldMap) {
4586
5598
  const orbitField = fieldMap.get("orbit")?.[0];
4587
5599
  const atField = fieldMap.get("at")?.[0];
4588
5600
  const surfaceField = fieldMap.get("surface")?.[0];
@@ -4750,7 +5762,7 @@
4750
5762
  }
4751
5763
  }
4752
5764
  function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
4753
- if (sourceSchemaVersion === "2.1") {
5765
+ if (!isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
4754
5766
  return;
4755
5767
  }
4756
5768
  diagnostics.push({
@@ -4762,6 +5774,34 @@
4762
5774
  column: location.column
4763
5775
  });
4764
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
+ }
4765
5805
  function preprocessAtlasSource(source) {
4766
5806
  const chars = [...source];
4767
5807
  const comments = [];
@@ -4849,7 +5889,7 @@
4849
5889
  }
4850
5890
 
4851
5891
  // packages/core/dist/atlas-edit.js
4852
- function createEmptyAtlasDocument(systemId = "WorldOrbit", version = "2.0") {
5892
+ function createEmptyAtlasDocument(systemId = "WorldOrbit", version = "2.5") {
4853
5893
  return {
4854
5894
  format: "worldorbit",
4855
5895
  version,
@@ -4875,6 +5915,7 @@
4875
5915
  },
4876
5916
  groups: [],
4877
5917
  relations: [],
5918
+ events: [],
4878
5919
  objects: [],
4879
5920
  diagnostics: []
4880
5921
  };
@@ -4892,6 +5933,10 @@
4892
5933
  return path.key ? document2.system?.atlasMetadata[path.key] ?? null : null;
4893
5934
  case "group":
4894
5935
  return path.id ? findGroup(document2, path.id) : null;
5936
+ case "event":
5937
+ return path.id ? findEvent(document2, path.id) : null;
5938
+ case "event-pose":
5939
+ return path.id && path.key ? findEventPose(document2, path.id, path.key) : null;
4895
5940
  case "object":
4896
5941
  return path.id ? findObject(document2, path.id) : null;
4897
5942
  case "viewpoint":
@@ -4921,6 +5966,19 @@
4921
5966
  next.groups = next.groups.filter((group) => group.id !== path.id);
4922
5967
  }
4923
5968
  return next;
5969
+ case "event":
5970
+ if (path.id) {
5971
+ next.events = next.events.filter((event) => event.id !== path.id);
5972
+ }
5973
+ return next;
5974
+ case "event-pose":
5975
+ if (path.id && path.key) {
5976
+ const event = findEvent(next, path.id);
5977
+ if (event) {
5978
+ event.positions = event.positions.filter((pose) => pose.objectId !== path.key);
5979
+ }
5980
+ }
5981
+ return next;
4924
5982
  case "viewpoint":
4925
5983
  if (path.id) {
4926
5984
  system.viewpoints = system.viewpoints.filter((viewpoint) => viewpoint.id !== path.id);
@@ -4989,6 +6047,22 @@
4989
6047
  };
4990
6048
  }
4991
6049
  }
6050
+ if (diagnostic.field?.startsWith("event.")) {
6051
+ const parts = diagnostic.field.split(".");
6052
+ if (parts[1] && findEvent(document2, parts[1])) {
6053
+ if (parts[2] === "pose" && parts[3] && findEventPose(document2, parts[1], parts[3])) {
6054
+ return {
6055
+ kind: "event-pose",
6056
+ id: parts[1],
6057
+ key: parts[3]
6058
+ };
6059
+ }
6060
+ return {
6061
+ kind: "event",
6062
+ id: parts[1]
6063
+ };
6064
+ }
6065
+ }
4992
6066
  if (diagnostic.field && diagnostic.field in ensureSystem(document2).atlasMetadata) {
4993
6067
  return {
4994
6068
  kind: "metadata",
@@ -5020,6 +6094,12 @@
5020
6094
  function findRelation(document2, relationId) {
5021
6095
  return document2.relations.find((relation) => relation.id === relationId) ?? null;
5022
6096
  }
6097
+ function findEvent(document2, eventId) {
6098
+ return document2.events.find((event) => event.id === eventId) ?? null;
6099
+ }
6100
+ function findEventPose(document2, eventId, objectId) {
6101
+ return findEvent(document2, eventId)?.positions.find((pose) => pose.objectId === objectId) ?? null;
6102
+ }
5023
6103
  function findViewpoint(system, viewpointId) {
5024
6104
  return system?.viewpoints.find((viewpoint) => viewpoint.id === viewpointId) ?? null;
5025
6105
  }
@@ -5028,8 +6108,9 @@
5028
6108
  }
5029
6109
 
5030
6110
  // packages/core/dist/load.js
5031
- var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1)?$/i;
6111
+ var ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1|\.5)?$/i;
5032
6112
  var ATLAS_SCHEMA_21_PATTERN = /^schema\s+2\.1$/i;
6113
+ var ATLAS_SCHEMA_25_PATTERN = /^schema\s+2\.5$/i;
5033
6114
  var LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
5034
6115
  function detectWorldOrbitSchemaVersion(source) {
5035
6116
  for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
@@ -5043,6 +6124,9 @@
5043
6124
  if (ATLAS_SCHEMA_21_PATTERN.test(trimmed)) {
5044
6125
  return "2.1";
5045
6126
  }
6127
+ if (ATLAS_SCHEMA_25_PATTERN.test(trimmed)) {
6128
+ return "2.5";
6129
+ }
5046
6130
  if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
5047
6131
  return "2.0";
5048
6132
  }
@@ -5103,7 +6187,7 @@
5103
6187
  }
5104
6188
  function loadWorldOrbitSourceWithDiagnostics(source) {
5105
6189
  const schemaVersion = detectWorldOrbitSchemaVersion(source);
5106
- 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") {
5107
6191
  return loadAtlasSourceWithDiagnostics(source, schemaVersion);
5108
6192
  }
5109
6193
  let ast;
@@ -5197,6 +6281,7 @@
5197
6281
  background: true,
5198
6282
  guides: true,
5199
6283
  relations: true,
6284
+ events: true,
5200
6285
  orbits: true,
5201
6286
  objects: true,
5202
6287
  labels: true,
@@ -5343,14 +6428,17 @@
5343
6428
  }
5344
6429
  function createAtlasStateSnapshot(viewerState, renderOptions, filter, viewpointId) {
5345
6430
  return {
5346
- version: "2.0",
6431
+ version: "2.5",
5347
6432
  viewpointId,
6433
+ activeEventId: renderOptions.activeEventId ?? null,
5348
6434
  viewerState: { ...viewerState },
5349
6435
  renderOptions: {
5350
6436
  preset: renderOptions.preset,
5351
6437
  projection: renderOptions.projection,
6438
+ camera: renderOptions.camera ? { ...renderOptions.camera } : null,
5352
6439
  layers: renderOptions.layers ? { ...renderOptions.layers } : void 0,
5353
- scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0
6440
+ scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0,
6441
+ activeEventId: renderOptions.activeEventId ?? null
5354
6442
  },
5355
6443
  filter: normalizeViewerFilter(filter)
5356
6444
  };
@@ -5361,8 +6449,9 @@
5361
6449
  function deserializeViewerAtlasState(serialized) {
5362
6450
  const raw = JSON.parse(decodeURIComponent(serialized));
5363
6451
  return {
5364
- version: "2.0",
6452
+ version: raw.version === "2.0" ? "2.0" : "2.5",
5365
6453
  viewpointId: raw.viewpointId ?? null,
6454
+ activeEventId: raw.activeEventId ?? raw.renderOptions?.activeEventId ?? null,
5366
6455
  viewerState: {
5367
6456
  scale: raw.viewerState?.scale ?? 1,
5368
6457
  rotationDeg: raw.viewerState?.rotationDeg ?? 0,
@@ -5373,8 +6462,10 @@
5373
6462
  renderOptions: {
5374
6463
  preset: raw.renderOptions?.preset,
5375
6464
  projection: raw.renderOptions?.projection,
6465
+ camera: raw.renderOptions?.camera ? { ...raw.renderOptions.camera } : null,
5376
6466
  layers: raw.renderOptions?.layers ? { ...raw.renderOptions.layers } : void 0,
5377
- scaleModel: raw.renderOptions?.scaleModel ? { ...raw.renderOptions.scaleModel } : void 0
6467
+ scaleModel: raw.renderOptions?.scaleModel ? { ...raw.renderOptions.scaleModel } : void 0,
6468
+ activeEventId: raw.activeEventId ?? raw.renderOptions?.activeEventId ?? null
5378
6469
  },
5379
6470
  filter: normalizeViewerFilter(raw.filter ?? null)
5380
6471
  };
@@ -5389,8 +6480,10 @@
5389
6480
  viewerState: { ...atlasState.viewerState },
5390
6481
  renderOptions: {
5391
6482
  ...atlasState.renderOptions,
6483
+ camera: atlasState.renderOptions.camera ? { ...atlasState.renderOptions.camera } : null,
5392
6484
  layers: atlasState.renderOptions.layers ? { ...atlasState.renderOptions.layers } : void 0,
5393
- scaleModel: atlasState.renderOptions.scaleModel ? { ...atlasState.renderOptions.scaleModel } : void 0
6485
+ scaleModel: atlasState.renderOptions.scaleModel ? { ...atlasState.renderOptions.scaleModel } : void 0,
6486
+ activeEventId: atlasState.renderOptions.activeEventId ?? null
5394
6487
  },
5395
6488
  filter: atlasState.filter ? { ...atlasState.filter } : null
5396
6489
  }
@@ -5408,6 +6501,7 @@
5408
6501
  background: viewpoint.layers.background,
5409
6502
  guides: viewpoint.layers.guides,
5410
6503
  relations: viewpoint.layers.relations,
6504
+ events: viewpoint.layers.events,
5411
6505
  orbits: viewpoint.layers["orbits-front"] === void 0 && viewpoint.layers["orbits-back"] === void 0 ? void 0 : viewpoint.layers["orbits-front"] !== false || viewpoint.layers["orbits-back"] !== false,
5412
6506
  objects: viewpoint.layers.objects,
5413
6507
  labels: viewpoint.layers.labels,
@@ -5694,6 +6788,7 @@
5694
6788
  const orbitMarkup = layers.orbits ? renderOrbitLayer(scene, visibleObjectIds, layers.structures) : { back: "", front: "" };
5695
6789
  const leaderMarkup = layers.guides ? scene.leaders.filter((leader) => !leader.hidden).filter((leader) => visibleObjectIds.has(leader.objectId)).filter((leader) => layers.structures || !isStructureLike(leader.object)).map((leader) => `<line class="wo-leader wo-leader-${leader.mode}" x1="${leader.x1}" y1="${leader.y1}" x2="${leader.x2}" y2="${leader.y2}" data-render-id="${escapeXml(leader.renderId)}" data-group-id="${escapeAttribute(leader.groupId ?? "")}" />`).join("") : "";
5696
6790
  const relationMarkup = layers.relations ? scene.relations.filter((relation) => !relation.hidden).filter((relation) => visibleObjectIds.has(relation.fromObjectId) && visibleObjectIds.has(relation.toObjectId)).map((relation) => `<line class="wo-relation" x1="${relation.x1}" y1="${relation.y1}" x2="${relation.x2}" y2="${relation.y2}" data-render-id="${escapeXml(relation.renderId)}" data-relation-id="${escapeAttribute(relation.relationId)}" />`).join("") : "";
6791
+ const eventMarkup = layers.events ? scene.events.filter((event) => !event.hidden).map((event) => renderSceneEventOverlay(scene, event, visibleObjectIds, theme)).join("") : "";
5697
6792
  const objectMarkup = layers.objects ? visibleObjects.map((object) => renderSceneObject(object, options.selectedObjectId ?? null, theme)).join("") : "";
5698
6793
  const labelMarkup = layers.labels ? visibleLabels.map((label) => renderSceneLabel(scene, label, options.selectedObjectId ?? null)).join("") : "";
5699
6794
  const metadataMarkup = layers.metadata ? `<text class="wo-title" x="56" y="64">${escapeXml(scene.title)}</text>
@@ -5729,6 +6824,9 @@
5729
6824
  .wo-orbit-front { opacity: 0.9; }
5730
6825
  .wo-orbit-band { stroke: ${theme.orbitBand}; stroke-linecap: round; }
5731
6826
  .wo-relation { stroke: ${theme.relation}; stroke-width: 2; stroke-dasharray: 10 6; }
6827
+ .wo-event-line { stroke: ${theme.accent}; stroke-width: 1.6; stroke-dasharray: 5 5; opacity: 0.72; }
6828
+ .wo-event-node { fill: ${theme.accent}; stroke: ${theme.selected}; stroke-width: 1.4; opacity: 0.92; }
6829
+ .wo-event-label { fill: ${theme.accent}; font-family: ${theme.fontFamily}; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
5732
6830
  .wo-leader { stroke: ${theme.leader}; stroke-width: 1.5; stroke-dasharray: 6 5; }
5733
6831
  .wo-label { fill: ${theme.ink}; font-family: ${theme.fontFamily}; font-weight: 600; letter-spacing: 0.02em; }
5734
6832
  .wo-label-secondary { fill: ${theme.muted}; font-family: ${theme.fontFamily}; font-weight: 500; }
@@ -5763,6 +6861,7 @@
5763
6861
  ${layers.orbits ? `<g data-layer-id="orbits-back">${orbitMarkup.back}</g>` : ""}
5764
6862
  ${layers.guides ? `<g data-layer-id="guides">${leaderMarkup}</g>` : ""}
5765
6863
  ${layers.relations ? `<g data-layer-id="relations">${relationMarkup}</g>` : ""}
6864
+ ${layers.events ? `<g data-layer-id="events">${eventMarkup}</g>` : ""}
5766
6865
  ${layers.objects ? `<g data-layer-id="objects">${objectMarkup}</g>` : ""}
5767
6866
  ${layers.orbits ? `<g data-layer-id="orbits-front">${orbitMarkup.front}</g>` : ""}
5768
6867
  ${layers.labels ? `<g data-layer-id="labels">${labelMarkup}</g>` : ""}
@@ -5770,6 +6869,20 @@
5770
6869
  </g>
5771
6870
  </g>
5772
6871
  </svg>`;
6872
+ }
6873
+ function renderSceneEventOverlay(scene, event, visibleObjectIds, theme) {
6874
+ const participants = event.objectIds.filter((objectId) => visibleObjectIds.has(objectId)).map((objectId) => scene.objects.find((object) => object.objectId === objectId && !object.hidden)).filter(Boolean);
6875
+ if (participants.length === 0) {
6876
+ return "";
6877
+ }
6878
+ const stroke = event.event.color || theme.accent;
6879
+ const label = event.event.label || event.event.id;
6880
+ const lineMarkup = participants.map((object) => `<line class="wo-event-line" x1="${event.x}" y1="${event.y}" x2="${object.x}" y2="${object.y}" stroke="${escapeAttribute(stroke)}" data-event-id="${escapeAttribute(event.eventId)}" data-object-id="${escapeAttribute(object.objectId)}" />`).join("");
6881
+ return `<g class="wo-event" data-render-id="${escapeXml(event.renderId)}" data-event-id="${escapeAttribute(event.eventId)}">
6882
+ ${lineMarkup}
6883
+ <circle class="wo-event-node" cx="${event.x}" cy="${event.y}" r="5" fill="${escapeAttribute(stroke)}" />
6884
+ <text class="wo-event-label" x="${event.x}" y="${event.y - 10}" text-anchor="middle" font-size="10">${escapeXml(label)}</text>
6885
+ </g>`;
5773
6886
  }
5774
6887
  function renderOrbitLayer(scene, visibleObjectIds, includeStructures) {
5775
6888
  const backParts = [];
@@ -6352,6 +7465,13 @@
6352
7465
  value: `${details.object.resonance.targetObjectId} ${details.object.resonance.ratio}`
6353
7466
  });
6354
7467
  }
7468
+ if (details.relatedEvents.length > 0) {
7469
+ fields.set("events", {
7470
+ key: "events",
7471
+ label: "Events",
7472
+ value: details.relatedEvents.map((event) => event.event.label || event.event.id).join(", ")
7473
+ });
7474
+ }
6355
7475
  if (placement?.mode === "at") {
6356
7476
  fields.set("placement", {
6357
7477
  key: "placement",
@@ -6456,6 +7576,7 @@
6456
7576
  padding: options.padding,
6457
7577
  preset: options.preset,
6458
7578
  projection: options.projection,
7579
+ camera: options.camera ? { ...options.camera } : null,
6459
7580
  scaleModel: options.scaleModel ? { ...options.scaleModel } : void 0,
6460
7581
  theme: options.theme,
6461
7582
  layers: options.layers,
@@ -6744,6 +7865,11 @@
6744
7865
  if (currentInput.kind !== "scene" && viewpoint.projection !== scene.projection) {
6745
7866
  nextRenderOptions.projection = viewpoint.projection;
6746
7867
  }
7868
+ if (viewpoint.camera) {
7869
+ nextRenderOptions.camera = { ...viewpoint.camera };
7870
+ } else if (renderOptions.camera) {
7871
+ nextRenderOptions.camera = null;
7872
+ }
6747
7873
  if (viewpointLayers) {
6748
7874
  nextRenderOptions.layers = viewpointLayers;
6749
7875
  }
@@ -6766,6 +7892,12 @@
6766
7892
  emitAtlasStateChange();
6767
7893
  return true;
6768
7894
  },
7895
+ getActiveEventId() {
7896
+ return renderOptions.activeEventId ?? null;
7897
+ },
7898
+ setActiveEvent(id) {
7899
+ api.setRenderOptions({ activeEventId: id });
7900
+ },
6769
7901
  search(query, limit = 12) {
6770
7902
  return searchSceneObjects(scene, query, limit);
6771
7903
  },
@@ -7030,6 +8162,7 @@
7030
8162
  orbit: scene.orbitVisuals.find((orbit) => orbit.objectId === renderObject.objectId && !orbit.hidden) ?? null,
7031
8163
  relatedOrbits: scene.orbitVisuals.filter((orbit) => !orbit.hidden && (orbit.objectId === renderObject.objectId || renderObject.ancestorIds.includes(orbit.objectId) || renderObject.childIds.includes(orbit.objectId))),
7032
8164
  relations: scene.relations.filter((relation) => !relation.hidden && (relation.fromObjectId === renderObject.objectId || relation.toObjectId === renderObject.objectId)),
8165
+ relatedEvents: scene.events.filter((event) => !event.hidden && (event.targetObjectId === renderObject.objectId || event.objectIds.includes(renderObject.objectId))),
7033
8166
  parent: getObjectById(renderObject.parentId),
7034
8167
  children: renderObject.childIds.map((childId) => getObjectById(childId)).filter(Boolean),
7035
8168
  ancestors: renderObject.ancestorIds.map((ancestorId) => getObjectById(ancestorId)).filter(Boolean),
@@ -7343,16 +8476,19 @@
7343
8476
  function cloneRenderOptions(renderOptions) {
7344
8477
  return {
7345
8478
  ...renderOptions,
8479
+ camera: renderOptions.camera ? { ...renderOptions.camera } : null,
7346
8480
  filter: renderOptions.filter ? { ...renderOptions.filter } : void 0,
7347
8481
  scaleModel: renderOptions.scaleModel ? { ...renderOptions.scaleModel } : void 0,
7348
8482
  layers: renderOptions.layers ? { ...renderOptions.layers } : void 0,
7349
- theme: renderOptions.theme && typeof renderOptions.theme === "object" ? { ...renderOptions.theme } : renderOptions.theme
8483
+ theme: renderOptions.theme && typeof renderOptions.theme === "object" ? { ...renderOptions.theme } : renderOptions.theme,
8484
+ activeEventId: renderOptions.activeEventId ?? null
7350
8485
  };
7351
8486
  }
7352
8487
  function mergeRenderOptions(current, next) {
7353
8488
  return {
7354
8489
  ...current,
7355
8490
  ...next,
8491
+ camera: next.camera !== void 0 ? next.camera ? { ...next.camera } : null : current.camera ? { ...current.camera } : null,
7356
8492
  filter: next.filter !== void 0 ? normalizeViewerFilter(next.filter) : current.filter ? { ...current.filter } : void 0,
7357
8493
  scaleModel: next.scaleModel ? {
7358
8494
  ...current.scaleModel ?? {},
@@ -7366,7 +8502,7 @@
7366
8502
  };
7367
8503
  }
7368
8504
  function hasSceneAffectingRenderOptions(options) {
7369
- 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;
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;
7370
8506
  }
7371
8507
  function resolveSourceRenderOptions(loaded, renderOptions) {
7372
8508
  const atlasDocument = loaded.atlasDocument ?? loaded.draftDocument;
@@ -7677,7 +8813,11 @@
7677
8813
  var FIELD_HELP = {
7678
8814
  "defaults-view": {
7679
8815
  description: "Sets the default camera projection for the atlas.",
7680
- 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
+ ]
7681
8821
  },
7682
8822
  "defaults-scale": {
7683
8823
  description: "Chooses the overall spacing/style preset used by the renderer.",
@@ -7689,15 +8829,71 @@
7689
8829
  },
7690
8830
  "viewpoint-projection": {
7691
8831
  description: "Overrides the projection for this saved viewpoint.",
7692
- 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
+ ]
7693
8837
  },
7694
8838
  "viewpoint-zoom": {
7695
8839
  description: "Controls how closely this viewpoint frames the system.",
7696
8840
  references: ["1 = scene fit", "2+ = close-up"]
7697
8841
  },
7698
8842
  "viewpoint-rotation": {
7699
- description: "Rotates the saved camera angle in degrees.",
7700
- 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"]
8861
+ },
8862
+ "viewpoint-events": {
8863
+ description: "Lists event IDs that this viewpoint should feature in its detail panel.",
8864
+ references: ["solar-eclipse-naar", "transit-window conjunction"]
8865
+ },
8866
+ "event-kind": {
8867
+ description: "Short semantic event type for tooling and viewer overlays.",
8868
+ references: ["solar-eclipse", "lunar-eclipse", "transit"]
8869
+ },
8870
+ "event-target": {
8871
+ description: "Primary object this event is centered on.",
8872
+ references: ["Naar", "Seyra"]
8873
+ },
8874
+ "event-participants": {
8875
+ description: "Objects that participate in the event snapshot or description.",
8876
+ references: ["Iyath Naar Seyra", "Naar Seyra Orun"]
8877
+ },
8878
+ "event-timing": {
8879
+ description: "Free-text timing note for the event.",
8880
+ references: ['"Every late bloom season"', '"At local midyear"']
8881
+ },
8882
+ "event-visibility": {
8883
+ description: "Notes where or how the event is visible.",
8884
+ references: ['"Visible from Naar"', '"Southern hemisphere only"']
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
+ },
8894
+ "event-viewpoints": {
8895
+ description: "Viewpoint IDs that should list this event prominently.",
8896
+ references: ["naar-system", "overview inner-system"]
7701
8897
  },
7702
8898
  "placement-target": {
7703
8899
  description: "Names the body or reference this object is attached to.",
@@ -7735,6 +8931,14 @@
7735
8931
  description: "Starting position of the object along its orbit.",
7736
8932
  references: ["0deg = start position", "180deg = opposite side"]
7737
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
+ },
7738
8942
  "prop-radius": {
7739
8943
  description: "Visual body size or real-world-inspired radius value.",
7740
8944
  references: ["1re = Earth radius", "1sol = Sun radius"]
@@ -7817,12 +9021,23 @@
7817
9021
  minimap: true,
7818
9022
  tooltipMode: "hover",
7819
9023
  onSelectionChange(selectedObject) {
9024
+ const activeEventId = selection ? selectionEventId(selection) : null;
7820
9025
  if (ignoreViewerSelection || !selectedObject) {
7821
- if (!ignoreViewerSelection && selection?.kind === "object") {
7822
- setSelection(null, false, true);
9026
+ if (!ignoreViewerSelection) {
9027
+ if (selection?.kind === "event-pose" && selection.id) {
9028
+ setSelection({ kind: "event", id: selection.id }, false, true);
9029
+ } else if (selection?.kind === "object") {
9030
+ setSelection(activeEventId ? { kind: "event", id: activeEventId } : null, false, true);
9031
+ } else if (selection?.kind === "event" && selection.id) {
9032
+ setSelection({ kind: "event", id: selection.id }, false, true);
9033
+ }
7823
9034
  }
7824
9035
  return;
7825
9036
  }
9037
+ if (activeEventId && findEventPose2(atlasDocument, activeEventId, selectedObject.objectId)) {
9038
+ setSelection({ kind: "event-pose", id: activeEventId, key: selectedObject.objectId }, false, true);
9039
+ return;
9040
+ }
7826
9041
  setSelection({ kind: "object", id: selectedObject.objectId }, false, true);
7827
9042
  },
7828
9043
  onViewChange() {
@@ -7832,6 +9047,7 @@
7832
9047
  toolbar.addEventListener("click", handleToolbarClick);
7833
9048
  outline.addEventListener("click", handleOutlineClick);
7834
9049
  overlay.addEventListener("pointerdown", handleOverlayPointerDown);
9050
+ inspector?.addEventListener("click", handleInspectorClick);
7835
9051
  inspector?.addEventListener("input", handleInspectorInput);
7836
9052
  inspector?.addEventListener("change", handleInspectorChange);
7837
9053
  sourcePane?.addEventListener("input", handleSourceInput);
@@ -7906,6 +9122,30 @@
7906
9122
  replaceAtlasDocument(nextDocument, true, { kind: "object", id });
7907
9123
  return id;
7908
9124
  },
9125
+ addEvent() {
9126
+ const id = createUniqueId("event", atlasDocument.events.map((event) => event.id));
9127
+ const created = {
9128
+ id,
9129
+ kind: "",
9130
+ label: humanizeIdentifier4(id),
9131
+ summary: null,
9132
+ targetObjectId: null,
9133
+ participantObjectIds: [],
9134
+ timing: null,
9135
+ visibility: null,
9136
+ epoch: null,
9137
+ referencePlane: null,
9138
+ tags: [],
9139
+ color: null,
9140
+ hidden: false,
9141
+ positions: []
9142
+ };
9143
+ const nextDocument = cloneAtlasDocument(atlasDocument);
9144
+ nextDocument.events.push(created);
9145
+ nextDocument.events.sort(compareEvents);
9146
+ replaceAtlasDocument(nextDocument, true, { kind: "event", id });
9147
+ return id;
9148
+ },
7909
9149
  addViewpoint() {
7910
9150
  const id = createUniqueId("viewpoint", atlasDocument.system?.viewpoints.map((viewpoint) => viewpoint.id) ?? []);
7911
9151
  const created = {
@@ -7914,10 +9154,12 @@
7914
9154
  summary: "",
7915
9155
  focusObjectId: null,
7916
9156
  selectedObjectId: null,
9157
+ events: [],
7917
9158
  projection: atlasDocument.system?.defaults.view ?? "topdown",
7918
9159
  preset: atlasDocument.system?.defaults.preset ?? null,
7919
9160
  zoom: null,
7920
9161
  rotationDeg: 0,
9162
+ camera: null,
7921
9163
  layers: {},
7922
9164
  filter: null
7923
9165
  };
@@ -7974,6 +9216,7 @@
7974
9216
  toolbar.removeEventListener("click", handleToolbarClick);
7975
9217
  outline.removeEventListener("click", handleOutlineClick);
7976
9218
  overlay.removeEventListener("pointerdown", handleOverlayPointerDown);
9219
+ inspector?.removeEventListener("click", handleInspectorClick);
7977
9220
  inspector?.removeEventListener("input", handleInspectorInput);
7978
9221
  inspector?.removeEventListener("change", handleInspectorChange);
7979
9222
  sourcePane?.removeEventListener("input", handleSourceInput);
@@ -8019,7 +9262,7 @@
8019
9262
  }
8020
9263
  function getCurrentSourceForExport() {
8021
9264
  if (dragState?.changed) {
8022
- return formatDocument(atlasDocument, { schema: "2.0" });
9265
+ return formatAtlasSource(atlasDocument);
8023
9266
  }
8024
9267
  return canonicalSource;
8025
9268
  }
@@ -8058,7 +9301,7 @@
8058
9301
  }
8059
9302
  clearSourceInputTimer();
8060
9303
  atlasDocument = cloneAtlasDocument(nextDocument);
8061
- canonicalSource = formatDocument(atlasDocument, { schema: "2.0" });
9304
+ canonicalSource = formatAtlasSource(atlasDocument);
8062
9305
  if (!preserveSourceText) {
8063
9306
  sourceText = canonicalSource;
8064
9307
  }
@@ -8099,10 +9342,10 @@
8099
9342
  if (commitHistory) {
8100
9343
  history.push(createHistoryEntry());
8101
9344
  future.length = 0;
8102
- sourceText = formatDocument(nextDocument, { schema: "2.0" });
9345
+ sourceText = formatAtlasSource(nextDocument);
8103
9346
  }
8104
9347
  atlasDocument = cloneAtlasDocument(nextDocument);
8105
- canonicalSource = formatDocument(atlasDocument, { schema: "2.0" });
9348
+ canonicalSource = formatAtlasSource(atlasDocument);
8106
9349
  diagnostics = mergeDiagnostics(loadedDiagnostics, collectDocumentDiagnostics(atlasDocument));
8107
9350
  selection = normalizeSelection(selection);
8108
9351
  syncViewer({
@@ -8121,11 +9364,13 @@
8121
9364
  const previousState = viewer.getState();
8122
9365
  const currentRenderOptions = viewer.getRenderOptions();
8123
9366
  const nextPreset = atlasDocument.system?.defaults.preset ?? "atlas-card";
9367
+ const nextActiveEventId = selection ? selectionEventId(selection) : null;
8124
9368
  ignoreViewerSelection = true;
8125
- if (currentRenderOptions.preset !== nextPreset || currentRenderOptions.projection !== "document") {
9369
+ if (currentRenderOptions.preset !== nextPreset || currentRenderOptions.projection !== "document" || (currentRenderOptions.activeEventId ?? null) !== nextActiveEventId) {
8126
9370
  viewer.setRenderOptions({
8127
9371
  preset: nextPreset,
8128
- projection: "document"
9372
+ projection: "document",
9373
+ activeEventId: nextActiveEventId
8129
9374
  });
8130
9375
  }
8131
9376
  viewer.setDocument(materializeAtlasDocument(atlasDocument));
@@ -8134,10 +9379,12 @@
8134
9379
  } else if (options2.preserveCamera !== false) {
8135
9380
  viewer.setState({
8136
9381
  ...previousState,
8137
- selectedObjectId: selection?.kind === "object" ? selection.id ?? null : null
9382
+ selectedObjectId: selection?.kind === "object" ? selection.id ?? null : selection?.kind === "event-pose" ? selection.key ?? null : null
8138
9383
  });
8139
9384
  } else if (selection?.kind === "object" && selection.id) {
8140
9385
  viewer.focusObject(selection.id);
9386
+ } else if (selection?.kind === "event-pose" && selection.key) {
9387
+ viewer.focusObject(selection.key);
8141
9388
  }
8142
9389
  ignoreViewerSelection = false;
8143
9390
  }
@@ -8156,8 +9403,11 @@
8156
9403
  selection = normalizeSelection(nextSelection);
8157
9404
  if (syncViewerSelection) {
8158
9405
  ignoreViewerSelection = true;
9406
+ viewer.setRenderOptions({ activeEventId: selection ? selectionEventId(selection) : null });
8159
9407
  if (selection?.kind === "object" && selection.id) {
8160
9408
  viewer.focusObject(selection.id);
9409
+ } else if (selection?.kind === "event-pose" && selection.key) {
9410
+ viewer.focusObject(selection.key);
8161
9411
  } else if (selection?.kind === "viewpoint" && selection.id) {
8162
9412
  viewer.goToViewpoint(selection.id);
8163
9413
  }
@@ -8207,6 +9457,7 @@
8207
9457
  ${OBJECT_TYPES.map((type) => `<option value="${escapeHtml3(type)}"${type === objectType ? " selected" : ""}>${escapeHtml3(humanizeIdentifier4(type))}</option>`).join("")}
8208
9458
  </select>
8209
9459
  <button type="button" data-editor-action="add-object">Add object</button>
9460
+ <button type="button" data-editor-action="add-event">Add event</button>
8210
9461
  <button type="button" data-editor-action="add-viewpoint">Add viewpoint</button>
8211
9462
  <button type="button" data-editor-action="add-annotation">Add annotation</button>
8212
9463
  <button type="button" data-editor-action="add-metadata">Add metadata</button>
@@ -8241,6 +9492,10 @@
8241
9492
  <h3>Annotations</h3>
8242
9493
  ${(atlasDocument.system?.annotations.length ?? 0) > 0 ? atlasDocument.system?.annotations.map((annotation) => renderOutlineButton({ kind: "annotation", id: annotation.id }, annotation.label, activeKey, diagnosticBuckets)).join("") : `<p class="wo-editor-empty">No annotations yet.</p>`}
8243
9494
  </div>
9495
+ <div class="wo-editor-outline-section">
9496
+ <h3>Events</h3>
9497
+ ${atlasDocument.events.length > 0 ? atlasDocument.events.map((eventEntry) => renderEventOutlineItems(eventEntry, activeKey, diagnosticBuckets)).join("") : `<p class="wo-editor-empty">No events yet.</p>`}
9498
+ </div>
8244
9499
  <div class="wo-editor-outline-section">
8245
9500
  <h3>Objects</h3>
8246
9501
  ${atlasDocument.objects.length > 0 ? atlasDocument.objects.map((object) => renderOutlineButton({ kind: "object", id: object.id }, `${object.id} - ${object.type}`, activeKey, diagnosticBuckets)).join("") : `<p class="wo-editor-empty">No objects yet.</p>`}
@@ -8278,6 +9533,7 @@
8278
9533
  selection: selection ? { path: { ...selection } } : null,
8279
9534
  system: atlasDocument.system,
8280
9535
  viewpoints: atlasDocument.system?.viewpoints ?? [],
9536
+ events: atlasDocument.events,
8281
9537
  objects: atlasDocument.objects
8282
9538
  };
8283
9539
  if (!selection) {
@@ -8306,6 +9562,16 @@
8306
9562
  applyInspectorSectionState(inspector, inspectorSectionState);
8307
9563
  decorateInspectorDiagnostics(selection, diagnostics);
8308
9564
  return;
9565
+ case "event":
9566
+ inspector.innerHTML = diagnosticSummary + renderEventInspector(formState, selection.id ?? "");
9567
+ applyInspectorSectionState(inspector, inspectorSectionState);
9568
+ decorateInspectorDiagnostics(selection, diagnostics);
9569
+ return;
9570
+ case "event-pose":
9571
+ inspector.innerHTML = diagnosticSummary + renderEventPoseInspector(formState, selection.id ?? "", selection.key ?? "");
9572
+ applyInspectorSectionState(inspector, inspectorSectionState);
9573
+ decorateInspectorDiagnostics(selection, diagnostics);
9574
+ return;
8309
9575
  case "annotation":
8310
9576
  inspector.innerHTML = diagnosticSummary + renderAnnotationInspector(formState, selection.id ?? "");
8311
9577
  applyInspectorSectionState(inspector, inspectorSectionState);
@@ -8338,10 +9604,11 @@
8338
9604
  return;
8339
9605
  }
8340
9606
  overlay.innerHTML = "";
8341
- if (selection?.kind !== "object" || !selection.id) {
9607
+ const selectedObjectId = selection?.kind === "object" ? selection.id ?? null : selection?.kind === "event-pose" ? selection.key ?? null : null;
9608
+ if (!selectedObjectId) {
8342
9609
  return;
8343
9610
  }
8344
- const details = viewer.getObjectDetails(selection.id);
9611
+ const details = viewer.getObjectDetails(selectedObjectId);
8345
9612
  if (!details) {
8346
9613
  return;
8347
9614
  }
@@ -8453,6 +9720,9 @@
8453
9720
  case "add-viewpoint":
8454
9721
  api.addViewpoint();
8455
9722
  return;
9723
+ case "add-event":
9724
+ api.addEvent();
9725
+ return;
8456
9726
  case "add-annotation":
8457
9727
  api.addAnnotation();
8458
9728
  return;
@@ -8487,6 +9757,32 @@
8487
9757
  key: button.dataset.pathKey || void 0
8488
9758
  }, true, true);
8489
9759
  }
9760
+ function handleInspectorClick(event) {
9761
+ const pathButton = event.target?.closest("[data-path-kind]");
9762
+ if (pathButton) {
9763
+ setSelection({
9764
+ kind: pathButton.dataset.pathKind,
9765
+ id: pathButton.dataset.pathId || void 0,
9766
+ key: pathButton.dataset.pathKey || void 0
9767
+ }, true, true);
9768
+ return;
9769
+ }
9770
+ const actionButton = event.target?.closest("[data-editor-action]");
9771
+ if (!actionButton) {
9772
+ return;
9773
+ }
9774
+ if (actionButton.dataset.editorAction === "add-event-pose") {
9775
+ const eventId = actionButton.dataset.editorEventId || (selection?.kind === "event" || selection?.kind === "event-pose" ? selection.id ?? "" : "");
9776
+ if (!eventId) {
9777
+ return;
9778
+ }
9779
+ const nextDocument = addEventPose(atlasDocument, eventId);
9780
+ const createdEvent = nextDocument.events.find((entry) => entry.id === eventId);
9781
+ const createdPose = createdEvent?.positions.at(-1) ?? createdEvent?.positions[0];
9782
+ replaceAtlasDocument(nextDocument, true, createdPose ? { kind: "event-pose", id: eventId, key: createdPose.objectId } : { kind: "event", id: eventId });
9783
+ return;
9784
+ }
9785
+ }
8490
9786
  function handleInspectorInput() {
8491
9787
  applyInspectorState(false);
8492
9788
  }
@@ -8510,6 +9806,12 @@
8510
9806
  case "viewpoint":
8511
9807
  replaceAtlasDocument(buildViewpointDocumentFromInspector(selection.id ?? ""), commitHistory, selection, false);
8512
9808
  return;
9809
+ case "event":
9810
+ replaceAtlasDocument(buildEventDocumentFromInspector(selection.id ?? ""), commitHistory, selection, false);
9811
+ return;
9812
+ case "event-pose":
9813
+ replaceAtlasDocument(buildEventPoseDocumentFromInspector(selection.id ?? "", selection.key ?? ""), commitHistory, selection, false);
9814
+ return;
8513
9815
  case "annotation":
8514
9816
  replaceAtlasDocument(buildAnnotationDocumentFromInspector(selection.id ?? ""), commitHistory, selection, false);
8515
9817
  return;
@@ -8578,6 +9880,7 @@
8578
9880
  kind,
8579
9881
  objectId,
8580
9882
  pointerId: event.pointerId,
9883
+ path: selection ? { ...selection } : { kind: "object", id: objectId },
8581
9884
  startedFrom: createHistoryEntry(),
8582
9885
  changed: false,
8583
9886
  orbitRadiusContext: kind === "orbit-radius" && details ? createOrbitRadiusDragContext(atlasDocument, viewer.getScene(), details) : null
@@ -8586,7 +9889,7 @@
8586
9889
  event.preventDefault();
8587
9890
  }
8588
9891
  function handleWindowPointerMove(event) {
8589
- if (!dragState || dragState.pointerId !== event.pointerId || selection?.kind !== "object" || selection.id !== dragState.objectId) {
9892
+ if (!dragState || dragState.pointerId !== event.pointerId || !selection || selectionKey(selection) !== selectionKey(dragState.path)) {
8590
9893
  return;
8591
9894
  }
8592
9895
  const details = viewer.getObjectDetails(dragState.objectId);
@@ -8598,27 +9901,27 @@
8598
9901
  switch (dragState.kind) {
8599
9902
  case "orbit-phase":
8600
9903
  if (details.object.placement?.mode === "orbit" && details.orbit) {
8601
- nextDocument = updateOrbitPhase(atlasDocument, dragState.objectId, details, pointer);
9904
+ nextDocument = updateOrbitPhase(atlasDocument, dragState.path, dragState.objectId, details, pointer);
8602
9905
  }
8603
9906
  break;
8604
9907
  case "orbit-radius":
8605
9908
  if (details.object.placement?.mode === "orbit" && details.orbit) {
8606
- nextDocument = updateOrbitRadius(atlasDocument, dragState.objectId, details, pointer, dragState.orbitRadiusContext ?? null);
9909
+ nextDocument = updateOrbitRadius(atlasDocument, dragState.path, dragState.objectId, details, pointer, dragState.orbitRadiusContext ?? null);
8607
9910
  }
8608
9911
  break;
8609
9912
  case "at-reference":
8610
9913
  if (details.object.placement?.mode === "at") {
8611
- nextDocument = updateAtReference(atlasDocument, dragState.objectId, viewer.getScene(), pointer);
9914
+ nextDocument = updateAtReference(atlasDocument, dragState.path, dragState.objectId, viewer.getScene(), pointer);
8612
9915
  }
8613
9916
  break;
8614
9917
  case "surface-target":
8615
9918
  if (details.object.placement?.mode === "surface") {
8616
- nextDocument = updateSurfaceTarget(atlasDocument, dragState.objectId, viewer.getScene(), pointer);
9919
+ nextDocument = updateSurfaceTarget(atlasDocument, dragState.path, dragState.objectId, viewer.getScene(), pointer);
8617
9920
  }
8618
9921
  break;
8619
9922
  case "free-distance":
8620
9923
  if (details.object.placement?.mode === "free") {
8621
- nextDocument = updateFreeDistance(atlasDocument, dragState.objectId, viewer.getScene(), details, pointer);
9924
+ nextDocument = updateFreeDistance(atlasDocument, dragState.path, dragState.objectId, viewer.getScene(), details, pointer);
8622
9925
  }
8623
9926
  break;
8624
9927
  }
@@ -8650,7 +9953,7 @@
8650
9953
  }
8651
9954
  history.push(dragState.startedFrom);
8652
9955
  future.length = 0;
8653
- canonicalSource = formatDocument(atlasDocument, { schema: "2.0" });
9956
+ canonicalSource = formatAtlasSource(atlasDocument);
8654
9957
  sourceText = canonicalSource;
8655
9958
  dragState = null;
8656
9959
  renderAll();
@@ -8748,15 +10051,18 @@
8748
10051
  preset: readOptionalTextInput(form, "viewpoint-preset") ?? null,
8749
10052
  zoom: parseNullableNumber(readOptionalTextInput(form, "viewpoint-zoom")),
8750
10053
  rotationDeg: parseNullableNumber(readOptionalTextInput(form, "viewpoint-rotation")) ?? 0,
10054
+ camera: buildViewCameraFromForm(form),
8751
10055
  layers: {
8752
10056
  background: readCheckbox(form, "layer-background"),
8753
10057
  guides: readCheckbox(form, "layer-guides"),
8754
10058
  "orbits-back": readCheckbox(form, "layer-orbits-back"),
8755
10059
  "orbits-front": readCheckbox(form, "layer-orbits-front"),
10060
+ events: readCheckbox(form, "layer-events"),
8756
10061
  objects: readCheckbox(form, "layer-objects"),
8757
10062
  labels: readCheckbox(form, "layer-labels"),
8758
10063
  metadata: readCheckbox(form, "layer-metadata")
8759
10064
  },
10065
+ events: splitTokens(readOptionalTextInput(form, "viewpoint-events")),
8760
10066
  filter: {
8761
10067
  query: readOptionalTextInput(form, "filter-query"),
8762
10068
  objectTypes: parseObjectTypes(readOptionalTextInput(form, "filter-object-types")),
@@ -8770,6 +10076,70 @@
8770
10076
  }
8771
10077
  return nextDocument;
8772
10078
  }
10079
+ function buildEventDocumentFromInspector(currentId) {
10080
+ const nextDocument = cloneAtlasDocument(atlasDocument);
10081
+ const form = inspector?.querySelector("form[data-editor-form='event']");
10082
+ const current = nextDocument.events.find((entry) => entry.id === currentId);
10083
+ if (!form || !current) {
10084
+ return nextDocument;
10085
+ }
10086
+ const nextId = readTextInput(form, "event-id") || current.id;
10087
+ const replacement = {
10088
+ ...current,
10089
+ id: nextId,
10090
+ kind: readTextInput(form, "event-kind"),
10091
+ label: readTextInput(form, "event-label") || current.label,
10092
+ summary: readOptionalTextInput(form, "event-summary"),
10093
+ targetObjectId: readOptionalTextInput(form, "event-target"),
10094
+ participantObjectIds: splitTokens(readOptionalTextInput(form, "event-participants")),
10095
+ timing: readOptionalTextInput(form, "event-timing"),
10096
+ visibility: readOptionalTextInput(form, "event-visibility"),
10097
+ epoch: readOptionalTextInput(form, "event-epoch"),
10098
+ referencePlane: readOptionalTextInput(form, "event-referencePlane"),
10099
+ tags: splitTokens(readOptionalTextInput(form, "event-tags")),
10100
+ color: readOptionalTextInput(form, "event-color"),
10101
+ hidden: readCheckbox(form, "event-hidden")
10102
+ };
10103
+ nextDocument.events = nextDocument.events.filter((entry) => entry.id !== current.id).concat(replacement).sort(compareEvents);
10104
+ syncEventViewpointReferences(nextDocument, current.id, replacement.id, splitTokens(readOptionalTextInput(form, "event-viewpoints")));
10105
+ if (current.id !== replacement.id) {
10106
+ selection = { kind: "event", id: replacement.id };
10107
+ }
10108
+ return nextDocument;
10109
+ }
10110
+ function buildEventPoseDocumentFromInspector(eventId, objectId) {
10111
+ const nextDocument = cloneAtlasDocument(atlasDocument);
10112
+ const form = inspector?.querySelector("form[data-editor-form='event-pose']");
10113
+ const eventEntry = nextDocument.events.find((entry) => entry.id === eventId);
10114
+ const currentPose = eventEntry?.positions.find((entry) => entry.objectId === objectId);
10115
+ if (!form || !eventEntry || !currentPose) {
10116
+ return nextDocument;
10117
+ }
10118
+ const nextObjectId = readTextInput(form, "pose-object-id") || currentPose.objectId;
10119
+ const replacement = {
10120
+ objectId: nextObjectId,
10121
+ placement: buildPlacementFromPoseForm(form, currentPose),
10122
+ epoch: readOptionalTextInput(form, "pose-epoch"),
10123
+ referencePlane: readOptionalTextInput(form, "pose-referencePlane")
10124
+ };
10125
+ const inner = parseOptionalUnit(readOptionalTextInput(form, "prop-inner"));
10126
+ const outer = parseOptionalUnit(readOptionalTextInput(form, "prop-outer"));
10127
+ if (inner) {
10128
+ replacement.inner = inner;
10129
+ }
10130
+ if (outer) {
10131
+ replacement.outer = outer;
10132
+ }
10133
+ eventEntry.positions = eventEntry.positions.filter((entry) => entry.objectId !== currentPose.objectId).concat(replacement).sort(compareEventPoses);
10134
+ if (eventEntry.targetObjectId !== replacement.objectId && !eventEntry.participantObjectIds.includes(replacement.objectId)) {
10135
+ eventEntry.participantObjectIds.push(replacement.objectId);
10136
+ eventEntry.participantObjectIds.sort((left, right) => left.localeCompare(right));
10137
+ }
10138
+ if (currentPose.objectId !== replacement.objectId) {
10139
+ selection = { kind: "event-pose", id: eventId, key: replacement.objectId };
10140
+ }
10141
+ return nextDocument;
10142
+ }
8773
10143
  function buildAnnotationDocumentFromInspector(currentId) {
8774
10144
  const nextDocument = cloneAtlasDocument(atlasDocument);
8775
10145
  const form = inspector?.querySelector("form[data-editor-form='annotation']");
@@ -8962,7 +10332,7 @@
8962
10332
  const atlasDocument2 = cloneAtlasDocument(options.atlasDocument);
8963
10333
  return {
8964
10334
  atlasDocument: atlasDocument2,
8965
- source: formatDocument(atlasDocument2, { schema: "2.0" }),
10335
+ source: formatAtlasSource(atlasDocument2),
8966
10336
  diagnostics: collectDocumentDiagnostics(atlasDocument2)
8967
10337
  };
8968
10338
  }
@@ -8972,7 +10342,7 @@
8972
10342
  const atlasDocument2 = loaded.value.atlasDocument ?? upgradeDocumentToV2(loaded.value.document);
8973
10343
  return {
8974
10344
  atlasDocument: atlasDocument2,
8975
- source: formatDocument(atlasDocument2, { schema: "2.0" }),
10345
+ source: formatAtlasSource(atlasDocument2),
8976
10346
  diagnostics: mergeDiagnostics(resolveAtlasDiagnostics(atlasDocument2, loaded.diagnostics), collectDocumentDiagnostics(atlasDocument2))
8977
10347
  };
8978
10348
  }
@@ -8980,10 +10350,13 @@
8980
10350
  const atlasDocument = createEmptyAtlasDocument("WorldOrbit");
8981
10351
  return {
8982
10352
  atlasDocument,
8983
- source: formatDocument(atlasDocument, { schema: "2.0" }),
10353
+ source: formatAtlasSource(atlasDocument),
8984
10354
  diagnostics: collectDocumentDiagnostics(atlasDocument)
8985
10355
  };
8986
10356
  }
10357
+ function formatAtlasSource(document2) {
10358
+ return formatDocument(document2, { schema: document2.version });
10359
+ }
8987
10360
  function buildEditorMarkup() {
8988
10361
  const previewOpen = shouldPreviewSectionBeOpenByDefault();
8989
10362
  return `<section class="wo-editor-shell">
@@ -9098,6 +10471,12 @@
9098
10471
  const badge = bucket && (bucket.errors > 0 || bucket.warnings > 0) ? `<span class="wo-editor-outline-badge${bucket.errors > 0 ? " is-error" : " is-warning"}">${bucket.errors > 0 ? bucket.errors : bucket.warnings}</span>` : "";
9099
10472
  return `<button type="button" class="wo-editor-outline-item${key === activeKey ? " is-active" : ""}" data-path-kind="${escapeHtml3(path.kind)}"${path.id ? ` data-path-id="${escapeHtml3(path.id)}"` : ""}${path.key ? ` data-path-key="${escapeHtml3(path.key)}"` : ""}><span>${escapeHtml3(label)}</span>${badge}</button>`;
9100
10473
  }
10474
+ function renderEventOutlineItems(eventEntry, activeKey, diagnosticBuckets) {
10475
+ return `<div class="wo-editor-outline-group">
10476
+ ${renderOutlineButton({ kind: "event", id: eventEntry.id }, eventEntry.label || eventEntry.id, activeKey, diagnosticBuckets)}
10477
+ ${eventEntry.positions.length > 0 ? `<div class="wo-editor-outline-children">${[...eventEntry.positions].sort(compareEventPoses).map((pose) => renderOutlineButton({ kind: "event-pose", id: eventEntry.id, key: pose.objectId }, pose.objectId, activeKey, diagnosticBuckets)).join("")}</div>` : ""}
10478
+ </div>`;
10479
+ }
9101
10480
  function renderSystemInspector(formState) {
9102
10481
  return `<form class="wo-editor-form" data-editor-form="system">
9103
10482
  <h2>System</h2>
@@ -9111,7 +10490,9 @@
9111
10490
  <h2>Defaults</h2>
9112
10491
  ${renderInspectorSection("defaults", "basics", "Basics", `${renderSelectField("Projection", "defaults-view", [
9113
10492
  ["topdown", "Topdown"],
9114
- ["isometric", "Isometric"]
10493
+ ["isometric", "Isometric"],
10494
+ ["orthographic", "Orthographic"],
10495
+ ["perspective", "Perspective"]
9115
10496
  ], defaults?.view ?? "topdown")}
9116
10497
  ${renderTextField("Scale preset", "defaults-scale", defaults?.scale ?? "")}
9117
10498
  ${renderTextField("Units", "defaults-units", defaults?.units ?? "")}
@@ -9147,7 +10528,9 @@
9147
10528
  ${renderTextField("Selected object", "viewpoint-select", viewpoint.selectedObjectId ?? "")}
9148
10529
  ${renderSelectField("Projection", "viewpoint-projection", [
9149
10530
  ["topdown", "Topdown"],
9150
- ["isometric", "Isometric"]
10531
+ ["isometric", "Isometric"],
10532
+ ["orthographic", "Orthographic"],
10533
+ ["perspective", "Perspective"]
9151
10534
  ], viewpoint.projection)}
9152
10535
  ${renderSelectField("Preset", "viewpoint-preset", [
9153
10536
  ["", "Document default"],
@@ -9158,12 +10541,18 @@
9158
10541
  ], viewpoint.preset ?? "")}
9159
10542
  ${renderTextField("Zoom", "viewpoint-zoom", viewpoint.zoom === null ? "" : String(viewpoint.zoom))}
9160
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>`)}
9161
10549
  ${renderInspectorSection("viewpoint", "layers", "Layers", `<fieldset class="wo-editor-fieldset">
9162
10550
  <legend>Layers</legend>
9163
10551
  ${renderCheckboxField("Background", "layer-background", viewpoint.layers.background !== false)}
9164
10552
  ${renderCheckboxField("Guides", "layer-guides", viewpoint.layers.guides !== false)}
9165
10553
  ${renderCheckboxField("Orbits back", "layer-orbits-back", viewpoint.layers["orbits-back"] !== false)}
9166
10554
  ${renderCheckboxField("Orbits front", "layer-orbits-front", viewpoint.layers["orbits-front"] !== false)}
10555
+ ${renderCheckboxField("Events", "layer-events", viewpoint.layers.events !== false)}
9167
10556
  ${renderCheckboxField("Objects", "layer-objects", viewpoint.layers.objects !== false)}
9168
10557
  ${renderCheckboxField("Labels", "layer-labels", viewpoint.layers.labels !== false)}
9169
10558
  ${renderCheckboxField("Metadata", "layer-metadata", viewpoint.layers.metadata !== false)}
@@ -9171,7 +10560,75 @@
9171
10560
  ${renderInspectorSection("viewpoint", "filter", "Filter", `${renderTextField("Filter query", "filter-query", viewpoint.filter?.query ?? "")}
9172
10561
  ${renderTextField("Filter object types", "filter-object-types", viewpoint.filter?.objectTypes.join(" ") ?? "")}
9173
10562
  ${renderTextField("Filter tags", "filter-tags", viewpoint.filter?.tags.join(" ") ?? "")}
9174
- ${renderTextField("Filter groups", "filter-groups", viewpoint.filter?.groupIds.join(" ") ?? "")}`)}
10563
+ ${renderTextField("Filter groups", "filter-groups", viewpoint.filter?.groupIds.join(" ") ?? "")}
10564
+ ${renderTextField("Events", "viewpoint-events", viewpoint.events.join(" "))}`)}
10565
+ </form>`;
10566
+ }
10567
+ function renderEventInspector(formState, id) {
10568
+ const eventEntry = formState.events.find((entry) => entry.id === id);
10569
+ if (!eventEntry) {
10570
+ return `<p class="wo-editor-empty">Event not found.</p>`;
10571
+ }
10572
+ const linkedViewpoints = formState.viewpoints.filter((viewpoint) => viewpoint.events.includes(eventEntry.id)).map((viewpoint) => viewpoint.id).join(" ");
10573
+ return `<form class="wo-editor-form" data-editor-form="event">
10574
+ <h2>Event</h2>
10575
+ ${renderInspectorSection("event", "basics", "Basics", `${renderTextField("ID", "event-id", eventEntry.id)}
10576
+ ${renderTextField("Kind", "event-kind", eventEntry.kind)}
10577
+ ${renderTextField("Label", "event-label", eventEntry.label)}
10578
+ ${renderTextAreaField("Summary", "event-summary", eventEntry.summary ?? "")}
10579
+ ${renderTextField("Target object", "event-target", eventEntry.targetObjectId ?? "")}
10580
+ ${renderTextField("Participants", "event-participants", eventEntry.participantObjectIds.join(" "))}
10581
+ ${renderTextField("Timing", "event-timing", eventEntry.timing ?? "")}
10582
+ ${renderTextField("Visibility", "event-visibility", eventEntry.visibility ?? "")}
10583
+ ${renderTextField("Epoch", "event-epoch", eventEntry.epoch ?? "")}
10584
+ ${renderTextField("Reference plane", "event-referencePlane", eventEntry.referencePlane ?? "")}
10585
+ ${renderTextField("Tags", "event-tags", eventEntry.tags.join(" "))}
10586
+ ${renderTextField("Color", "event-color", eventEntry.color ?? "")}
10587
+ ${renderCheckboxField("Hidden", "event-hidden", eventEntry.hidden === true)}`, true)}
10588
+ ${renderInspectorSection("event", "viewpoints", "Viewpoints", `${renderTextField("Viewpoints", "event-viewpoints", linkedViewpoints)}`)}
10589
+ ${renderInspectorSection("event", "positions", "Positions", `${eventEntry.positions.length > 0 ? `<div class="wo-editor-inline-list">${eventEntry.positions.map((pose) => renderOutlineButton({ kind: "event-pose", id: eventEntry.id, key: pose.objectId }, pose.objectId, null, /* @__PURE__ */ new Map())).join("")}</div>` : `<p class="wo-editor-empty">No event poses yet.</p>`}
10590
+ <div class="wo-editor-inline-actions">
10591
+ <button type="button" data-editor-action="add-event-pose" data-editor-event-id="${escapeHtml3(eventEntry.id)}">Add pose</button>
10592
+ </div>`)}
10593
+ </form>`;
10594
+ }
10595
+ function renderEventPoseInspector(formState, eventId, objectId) {
10596
+ const eventEntry = formState.events.find((entry) => entry.id === eventId);
10597
+ const pose = eventEntry?.positions.find((entry) => entry.objectId === objectId);
10598
+ if (!eventEntry || !pose) {
10599
+ return `<p class="wo-editor-empty">Event pose not found.</p>`;
10600
+ }
10601
+ const placementMode = pose.placement?.mode ?? "";
10602
+ const placementTarget = pose.placement?.mode === "orbit" || pose.placement?.mode === "surface" || pose.placement?.mode === "at" ? pose.placement.target : "";
10603
+ const freeValue = pose.placement?.mode === "free" ? pose.placement.distance ? formatUnitValue3(pose.placement.distance) : pose.placement.descriptor ?? "" : "";
10604
+ return `<form class="wo-editor-form" data-editor-form="event-pose">
10605
+ <h2>Event Pose</h2>
10606
+ <p class="wo-editor-inline-note">Event <strong>${escapeHtml3(eventEntry.label || eventEntry.id)}</strong></p>
10607
+ ${renderInspectorSection("event-pose", "identity", "Identity", `${renderTextField("Object", "pose-object-id", pose.objectId)}
10608
+ <div class="wo-editor-inline-actions">
10609
+ <button type="button" data-path-kind="event" data-path-id="${escapeHtml3(eventEntry.id)}">Select event</button>
10610
+ </div>`, true)}
10611
+ ${renderInspectorSection("event-pose", "placement", "Placement", `${renderSelectField("Placement mode", "placement-mode", [
10612
+ ["", "None"],
10613
+ ["orbit", "Orbit"],
10614
+ ["at", "At"],
10615
+ ["surface", "Surface"],
10616
+ ["free", "Free"]
10617
+ ], placementMode)}
10618
+ ${renderTextField("Placement target", "placement-target", placementTarget)}
10619
+ ${renderTextField("Free value", "placement-free", freeValue)}
10620
+ ${renderTextField("Distance", "placement-distance", pose.placement?.mode === "orbit" && pose.placement.distance ? formatUnitValue3(pose.placement.distance) : "")}
10621
+ ${renderTextField("Semi-major", "placement-semiMajor", pose.placement?.mode === "orbit" && pose.placement.semiMajor ? formatUnitValue3(pose.placement.semiMajor) : "")}
10622
+ ${renderTextField("Eccentricity", "placement-eccentricity", pose.placement?.mode === "orbit" && pose.placement.eccentricity !== void 0 ? String(pose.placement.eccentricity) : "")}
10623
+ ${renderTextField("Period", "placement-period", pose.placement?.mode === "orbit" && pose.placement.period ? formatUnitValue3(pose.placement.period) : "")}
10624
+ ${renderTextField("Angle", "placement-angle", pose.placement?.mode === "orbit" && pose.placement.angle ? formatUnitValue3(pose.placement.angle) : "")}
10625
+ ${renderTextField("Inclination", "placement-inclination", pose.placement?.mode === "orbit" && pose.placement.inclination ? formatUnitValue3(pose.placement.inclination) : "")}
10626
+ ${renderTextField("Phase", "placement-phase", pose.placement?.mode === "orbit" && pose.placement.phase ? formatUnitValue3(pose.placement.phase) : "")}
10627
+ ${renderTextField("Inner", "prop-inner", pose.inner ? formatUnitValue3(pose.inner) : "")}
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>`)}
9175
10632
  </form>`;
9176
10633
  }
9177
10634
  function renderAnnotationInspector(formState, id) {
@@ -9276,13 +10733,19 @@
9276
10733
  return form.elements.namedItem(name)?.checked ?? false;
9277
10734
  }
9278
10735
  function buildPlacementFromForm(form, current) {
10736
+ return buildPlacementFromValues(form, current.placement, current.id);
10737
+ }
10738
+ function buildPlacementFromPoseForm(form, current) {
10739
+ return buildPlacementFromValues(form, current.placement, current.objectId);
10740
+ }
10741
+ function buildPlacementFromValues(form, currentPlacement, fallbackTarget) {
9279
10742
  const mode = readTextInput(form, "placement-mode");
9280
10743
  const target = readOptionalTextInput(form, "placement-target");
9281
10744
  switch (mode) {
9282
10745
  case "orbit":
9283
10746
  return {
9284
10747
  mode,
9285
- target: target ?? (current.placement?.mode === "orbit" ? current.placement.target : current.id),
10748
+ target: target ?? (currentPlacement?.mode === "orbit" ? currentPlacement.target : fallbackTarget),
9286
10749
  distance: parseOptionalUnit(readOptionalTextInput(form, "placement-distance")),
9287
10750
  semiMajor: parseOptionalUnit(readOptionalTextInput(form, "placement-semiMajor")),
9288
10751
  eccentricity: parseNullableNumber(readOptionalTextInput(form, "placement-eccentricity")) ?? void 0,
@@ -9294,13 +10757,13 @@
9294
10757
  case "at":
9295
10758
  return {
9296
10759
  mode,
9297
- target: target ?? current.id,
9298
- reference: parseAtReferenceString(target ?? current.id)
10760
+ target: target ?? fallbackTarget,
10761
+ reference: parseAtReferenceString(target ?? fallbackTarget)
9299
10762
  };
9300
10763
  case "surface":
9301
10764
  return {
9302
10765
  mode,
9303
- target: target ?? current.id
10766
+ target: target ?? fallbackTarget
9304
10767
  };
9305
10768
  case "free": {
9306
10769
  const freeValue = readOptionalTextInput(form, "placement-free");
@@ -9350,6 +10813,15 @@
9350
10813
  const parsed = Number(value);
9351
10814
  return Number.isFinite(parsed) ? parsed : null;
9352
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
+ }
9353
10825
  function parseObjectTypes(value) {
9354
10826
  const tokens = splitTokens(value);
9355
10827
  return tokens.filter((token) => OBJECT_TYPES.includes(token));
@@ -9431,9 +10903,48 @@
9431
10903
  annotation.sourceObjectId = toId;
9432
10904
  }
9433
10905
  }
10906
+ for (const eventEntry of document2.events) {
10907
+ if (eventEntry.targetObjectId === fromId) {
10908
+ eventEntry.targetObjectId = toId;
10909
+ }
10910
+ eventEntry.participantObjectIds = eventEntry.participantObjectIds.map((entry) => entry === fromId ? toId : entry);
10911
+ for (const pose of eventEntry.positions) {
10912
+ if (pose.objectId === fromId) {
10913
+ pose.objectId = toId;
10914
+ }
10915
+ if (pose.placement?.mode === "orbit" && pose.placement.target === fromId) {
10916
+ pose.placement.target = toId;
10917
+ }
10918
+ if (pose.placement?.mode === "surface" && pose.placement.target === fromId) {
10919
+ pose.placement.target = toId;
10920
+ }
10921
+ if (pose.placement?.mode === "at") {
10922
+ const reference = pose.placement.reference;
10923
+ if (reference.kind === "anchor" && reference.objectId === fromId) {
10924
+ reference.objectId = toId;
10925
+ }
10926
+ if (reference.kind === "lagrange") {
10927
+ if (reference.primary === fromId) {
10928
+ reference.primary = toId;
10929
+ }
10930
+ if (reference.secondary === fromId) {
10931
+ reference.secondary = toId;
10932
+ }
10933
+ }
10934
+ pose.placement.target = formatAtReference2(reference);
10935
+ }
10936
+ }
10937
+ eventEntry.positions.sort(compareEventPoses);
10938
+ }
9434
10939
  }
9435
10940
  function removeSelectedNode(document2, selection) {
9436
10941
  const next = removeAtlasDocumentNode(document2, selection);
10942
+ if (selection.kind === "event" && selection.id) {
10943
+ for (const viewpoint of next.system?.viewpoints ?? []) {
10944
+ viewpoint.events = viewpoint.events.filter((eventId) => eventId !== selection.id);
10945
+ }
10946
+ return next;
10947
+ }
9437
10948
  if (selection.kind !== "object" || !selection.id) {
9438
10949
  return next;
9439
10950
  }
@@ -9468,9 +10979,45 @@
9468
10979
  annotation.sourceObjectId = null;
9469
10980
  }
9470
10981
  }
10982
+ for (const eventEntry of next.events) {
10983
+ if (eventEntry.targetObjectId === selection.id) {
10984
+ eventEntry.targetObjectId = null;
10985
+ }
10986
+ eventEntry.participantObjectIds = eventEntry.participantObjectIds.filter((entry) => entry !== selection.id);
10987
+ eventEntry.positions = eventEntry.positions.filter((entry) => entry.objectId !== selection.id);
10988
+ for (const pose of eventEntry.positions) {
10989
+ if (pose.placement?.mode === "orbit" && pose.placement.target === selection.id) {
10990
+ pose.placement = null;
10991
+ }
10992
+ if (pose.placement?.mode === "surface" && pose.placement.target === selection.id) {
10993
+ pose.placement = null;
10994
+ }
10995
+ if (pose.placement?.mode === "at") {
10996
+ const reference = pose.placement.reference;
10997
+ const touchesSelection = reference.kind === "anchor" && reference.objectId === selection.id || reference.kind === "lagrange" && (reference.primary === selection.id || reference.secondary === selection.id);
10998
+ if (touchesSelection) {
10999
+ pose.placement = null;
11000
+ }
11001
+ }
11002
+ }
11003
+ }
9471
11004
  return next;
9472
11005
  }
9473
- function updateOrbitPhase(document2, objectId, details, pointer) {
11006
+ function findEditablePlacementOwner(document2, path, objectId) {
11007
+ if (path.kind === "event-pose" && path.id && path.key) {
11008
+ const pose = findEventPose2(document2, path.id, path.key);
11009
+ if (pose?.placement) {
11010
+ return { placement: pose.placement };
11011
+ }
11012
+ return null;
11013
+ }
11014
+ const object = findObject2(document2, objectId);
11015
+ if (object?.placement) {
11016
+ return { placement: object.placement };
11017
+ }
11018
+ return null;
11019
+ }
11020
+ function updateOrbitPhase(document2, path, objectId, details, pointer) {
9474
11021
  const orbit = details.orbit;
9475
11022
  if (!orbit || details.object.placement?.mode !== "orbit") {
9476
11023
  return document2;
@@ -9481,17 +11028,17 @@
9481
11028
  const radians = Math.atan2((unrotated.y - orbit.cy) / Math.max(ry, 1), (unrotated.x - orbit.cx) / Math.max(rx, 1));
9482
11029
  const phaseDeg = normalizeDegrees(radians * 180 / Math.PI);
9483
11030
  const next = cloneAtlasDocument(document2);
9484
- const object = next.objects.find((entry) => entry.id === objectId);
9485
- if (!object || object.placement?.mode !== "orbit") {
11031
+ const placementOwner = findEditablePlacementOwner(next, path, objectId);
11032
+ if (!placementOwner || placementOwner.placement.mode !== "orbit") {
9486
11033
  return document2;
9487
11034
  }
9488
- object.placement.phase = {
11035
+ placementOwner.placement.phase = {
9489
11036
  value: roundNumber(phaseDeg, 2),
9490
11037
  unit: "deg"
9491
11038
  };
9492
11039
  return next;
9493
11040
  }
9494
- function updateOrbitRadius(document2, objectId, details, pointer, dragContext) {
11041
+ function updateOrbitRadius(document2, path, objectId, details, pointer, dragContext) {
9495
11042
  const orbit = details.orbit;
9496
11043
  if (!orbit || details.object.placement?.mode !== "orbit" || !dragContext) {
9497
11044
  return document2;
@@ -9501,47 +11048,47 @@
9501
11048
  const nextBaseRadius = Math.max(nextDisplayedRadius - dragContext.radiusOffsetPx, dragContext.innerPx);
9502
11049
  const nextMetric = orbitRadiusPxToMetric(nextBaseRadius, dragContext.innerPx, dragContext.stepPx);
9503
11050
  const next = cloneAtlasDocument(document2);
9504
- const object = next.objects.find((entry) => entry.id === objectId);
9505
- if (!object || object.placement?.mode !== "orbit") {
11051
+ const placementOwner = findEditablePlacementOwner(next, path, objectId);
11052
+ if (!placementOwner || placementOwner.placement.mode !== "orbit") {
9506
11053
  return document2;
9507
11054
  }
9508
- const currentValue = object.placement.semiMajor ?? object.placement.distance ?? {
11055
+ const currentValue = placementOwner.placement.semiMajor ?? placementOwner.placement.distance ?? {
9509
11056
  value: 1,
9510
11057
  unit: "au"
9511
11058
  };
9512
11059
  const scaled = distanceMetricToUnitValue(Math.max(nextMetric, 0), dragContext.preferredUnit ?? currentValue.unit);
9513
- if (object.placement.semiMajor) {
9514
- object.placement.semiMajor = scaled;
11060
+ if (placementOwner.placement.semiMajor) {
11061
+ placementOwner.placement.semiMajor = scaled;
9515
11062
  } else {
9516
- object.placement.distance = scaled;
11063
+ placementOwner.placement.distance = scaled;
9517
11064
  }
9518
11065
  return next;
9519
11066
  }
9520
- function updateAtReference(document2, objectId, scene, pointer) {
11067
+ function updateAtReference(document2, path, objectId, scene, pointer) {
9521
11068
  const candidate = findNearestAtCandidate(scene, objectId, pointer);
9522
11069
  if (!candidate) {
9523
11070
  return document2;
9524
11071
  }
9525
11072
  const next = cloneAtlasDocument(document2);
9526
- const object = next.objects.find((entry) => entry.id === objectId);
9527
- if (!object || object.placement?.mode !== "at") {
11073
+ const placementOwner = findEditablePlacementOwner(next, path, objectId);
11074
+ if (!placementOwner || placementOwner.placement.mode !== "at") {
9528
11075
  return document2;
9529
11076
  }
9530
- object.placement.reference = candidate.reference;
9531
- object.placement.target = formatAtReference2(candidate.reference);
11077
+ placementOwner.placement.reference = candidate.reference;
11078
+ placementOwner.placement.target = formatAtReference2(candidate.reference);
9532
11079
  return next;
9533
11080
  }
9534
- function updateSurfaceTarget(document2, objectId, scene, pointer) {
11081
+ function updateSurfaceTarget(document2, path, objectId, scene, pointer) {
9535
11082
  const target = findNearestSceneObject(scene, objectId, pointer, (entry) => SURFACE_TARGET_TYPES3.has(entry.object.type));
9536
11083
  if (!target) {
9537
11084
  return document2;
9538
11085
  }
9539
11086
  const next = cloneAtlasDocument(document2);
9540
- const object = next.objects.find((entry) => entry.id === objectId);
9541
- if (!object || object.placement?.mode !== "surface") {
11087
+ const placementOwner = findEditablePlacementOwner(next, path, objectId);
11088
+ if (!placementOwner || placementOwner.placement.mode !== "surface") {
9542
11089
  return document2;
9543
11090
  }
9544
- object.placement.target = target.objectId;
11091
+ placementOwner.placement.target = target.objectId;
9545
11092
  return next;
9546
11093
  }
9547
11094
  function createOrbitRadiusDragContext(document2, scene, details) {
@@ -9549,7 +11096,7 @@
9549
11096
  return null;
9550
11097
  }
9551
11098
  const targetId = details.object.placement.target;
9552
- const siblingCount = document2.objects.filter((entry) => entry.placement?.mode === "orbit" && entry.placement.target === targetId).length;
11099
+ const siblingCount = scene.objects.filter((entry) => entry.object.placement?.mode === "orbit" && entry.object.placement.target === targetId && !entry.hidden).length;
9553
11100
  const spacingFactor = layoutPresetSpacingForScene(scene.layoutPreset);
9554
11101
  const stepPx = (siblingCount > 2 ? 54 : 64) * spacingFactor * scene.scaleModel.orbitDistanceMultiplier;
9555
11102
  const innerPx = details.parent.radius + 56 * spacingFactor * scene.scaleModel.orbitDistanceMultiplier;
@@ -9564,28 +11111,28 @@
9564
11111
  preferredUnit: currentValue?.unit ?? null
9565
11112
  };
9566
11113
  }
9567
- function updateFreeDistance(document2, objectId, scene, details, pointer) {
11114
+ function updateFreeDistance(document2, path, objectId, scene, details, pointer) {
9568
11115
  if (details.object.placement?.mode !== "free") {
9569
11116
  return document2;
9570
11117
  }
9571
11118
  const railX = scene.width - scene.padding - 140;
9572
11119
  const offsetPx = Math.max(0, railX - pointer.x);
9573
11120
  const next = cloneAtlasDocument(document2);
9574
- const object = next.objects.find((entry) => entry.id === objectId);
9575
- if (!object || object.placement?.mode !== "free") {
11121
+ const placementOwner = findEditablePlacementOwner(next, path, objectId);
11122
+ if (!placementOwner || placementOwner.placement.mode !== "free") {
9576
11123
  return document2;
9577
11124
  }
9578
- const preferredUnit = normalizeFreeDistanceUnit(object.placement.distance?.unit ?? null);
11125
+ const preferredUnit = normalizeFreeDistanceUnit(placementOwner.placement.distance?.unit ?? null);
9579
11126
  const metric = offsetPx / Math.max(FREE_DISTANCE_PIXEL_FACTOR * scene.scaleModel.freePlacementMultiplier, 1);
9580
11127
  if (metric < 0.01) {
9581
- object.placement.distance = void 0;
9582
- if (!object.placement.descriptor) {
9583
- delete object.placement.descriptor;
11128
+ placementOwner.placement.distance = void 0;
11129
+ if (!placementOwner.placement.descriptor) {
11130
+ delete placementOwner.placement.descriptor;
9584
11131
  }
9585
11132
  return next;
9586
11133
  }
9587
- object.placement.distance = distanceMetricToUnitValue(metric, preferredUnit);
9588
- delete object.placement.descriptor;
11134
+ placementOwner.placement.distance = distanceMetricToUnitValue(metric, preferredUnit);
11135
+ delete placementOwner.placement.descriptor;
9589
11136
  return next;
9590
11137
  }
9591
11138
  function findNearestSceneObject(scene, selectedObjectId, pointer, predicate = () => true) {
@@ -9857,9 +11404,85 @@
9857
11404
  return ["viewpoint-zoom"];
9858
11405
  case "rotationDeg":
9859
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"];
11422
+ case "events":
11423
+ return ["viewpoint-events"];
11424
+ default:
11425
+ return [];
11426
+ }
11427
+ case "event":
11428
+ switch (field) {
11429
+ case "id":
11430
+ return ["event-id"];
11431
+ case "kind":
11432
+ return ["event-kind"];
11433
+ case "label":
11434
+ return ["event-label"];
11435
+ case "summary":
11436
+ return ["event-summary"];
11437
+ case "targetObjectId":
11438
+ case "target":
11439
+ return ["event-target"];
11440
+ case "participantObjectIds":
11441
+ case "participants":
11442
+ return ["event-participants"];
11443
+ case "timing":
11444
+ return ["event-timing"];
11445
+ case "visibility":
11446
+ return ["event-visibility"];
11447
+ case "epoch":
11448
+ return ["event-epoch"];
11449
+ case "referencePlane":
11450
+ return ["event-referencePlane"];
11451
+ case "tags":
11452
+ return ["event-tags"];
11453
+ case "color":
11454
+ return ["event-color"];
11455
+ case "hidden":
11456
+ return ["event-hidden"];
9860
11457
  default:
9861
11458
  return [];
9862
11459
  }
11460
+ case "event-pose":
11461
+ if (field === "objectId") {
11462
+ return ["pose-object-id"];
11463
+ }
11464
+ if (field === "placement") {
11465
+ return ["placement-mode"];
11466
+ }
11467
+ if (field === "reference" || field === "target") {
11468
+ return ["placement-target"];
11469
+ }
11470
+ if (field === "descriptor") {
11471
+ return ["placement-free"];
11472
+ }
11473
+ if (PLACEMENT_DIAGNOSTIC_FIELDS.has(field)) {
11474
+ return [`placement-${field}`];
11475
+ }
11476
+ if (field === "inner" || field === "outer") {
11477
+ return [`prop-${field}`];
11478
+ }
11479
+ if (field === "epoch") {
11480
+ return ["pose-epoch"];
11481
+ }
11482
+ if (field === "referencePlane") {
11483
+ return ["pose-referencePlane"];
11484
+ }
11485
+ return [];
9863
11486
  case "annotation":
9864
11487
  switch (field) {
9865
11488
  case "id":
@@ -9945,6 +11568,10 @@
9945
11568
  return `Metadata: ${path.key ?? ""}`;
9946
11569
  case "group":
9947
11570
  return `Group: ${path.id ?? ""}`;
11571
+ case "event":
11572
+ return `Event: ${path.id ?? ""}`;
11573
+ case "event-pose":
11574
+ return `Event Pose: ${path.id ?? ""} / ${path.key ?? ""}`;
9948
11575
  case "object":
9949
11576
  return `Object: ${path.id ?? ""}`;
9950
11577
  case "viewpoint":
@@ -9956,11 +11583,70 @@
9956
11583
  }
9957
11584
  }
9958
11585
  function selectionKey(path) {
9959
- return path ? `${path.kind}:${path.id ?? path.key ?? ""}` : null;
11586
+ return path ? `${path.kind}:${path.id ?? ""}:${path.key ?? ""}` : null;
11587
+ }
11588
+ function selectionEventId(path) {
11589
+ if (!path) {
11590
+ return null;
11591
+ }
11592
+ return path.kind === "event" || path.kind === "event-pose" ? path.id ?? null : null;
9960
11593
  }
9961
11594
  function compareObjects2(left, right) {
9962
11595
  return left.id.localeCompare(right.id);
9963
11596
  }
11597
+ function compareEvents(left, right) {
11598
+ return left.id.localeCompare(right.id);
11599
+ }
11600
+ function compareEventPoses(left, right) {
11601
+ return left.objectId.localeCompare(right.objectId);
11602
+ }
11603
+ function findEvent2(document2, eventId) {
11604
+ return document2.events.find((entry) => entry.id === eventId) ?? null;
11605
+ }
11606
+ function findEventPose2(document2, eventId, objectId) {
11607
+ return findEvent2(document2, eventId)?.positions.find((entry) => entry.objectId === objectId) ?? null;
11608
+ }
11609
+ function findObject2(document2, objectId) {
11610
+ return document2.objects.find((entry) => entry.id === objectId) ?? null;
11611
+ }
11612
+ function addEventPose(document2, eventId) {
11613
+ const next = cloneAtlasDocument(document2);
11614
+ const eventEntry = next.events.find((entry) => entry.id === eventId);
11615
+ if (!eventEntry) {
11616
+ return document2;
11617
+ }
11618
+ const baseObject = next.objects.find((object) => !eventEntry.positions.some((pose) => pose.objectId === object.id)) ?? next.objects[0];
11619
+ if (!baseObject) {
11620
+ return document2;
11621
+ }
11622
+ if (eventEntry.targetObjectId !== baseObject.id && !eventEntry.participantObjectIds.includes(baseObject.id)) {
11623
+ eventEntry.participantObjectIds.push(baseObject.id);
11624
+ eventEntry.participantObjectIds.sort((left, right) => left.localeCompare(right));
11625
+ }
11626
+ eventEntry.positions.push(createEventPoseFromObject(baseObject));
11627
+ eventEntry.positions.sort(compareEventPoses);
11628
+ return next;
11629
+ }
11630
+ function createEventPoseFromObject(object) {
11631
+ return {
11632
+ objectId: object.id,
11633
+ placement: object.placement ? structuredClone(object.placement) : null,
11634
+ inner: readUnitValue(object.properties.inner),
11635
+ outer: readUnitValue(object.properties.outer)
11636
+ };
11637
+ }
11638
+ function syncEventViewpointReferences(document2, previousEventId, nextEventId, viewpointIds) {
11639
+ const desired = new Set(viewpointIds);
11640
+ for (const viewpoint of document2.system?.viewpoints ?? []) {
11641
+ const currentIds = new Set(viewpoint.events);
11642
+ currentIds.delete(previousEventId);
11643
+ currentIds.delete(nextEventId);
11644
+ if (desired.has(viewpoint.id)) {
11645
+ currentIds.add(nextEventId);
11646
+ }
11647
+ viewpoint.events = [...currentIds].sort((left, right) => left.localeCompare(right));
11648
+ }
11649
+ }
9964
11650
  function createUniqueId(prefix, existing) {
9965
11651
  const safePrefix = prefix.trim() || "item";
9966
11652
  let counter = 1;
@@ -9989,6 +11675,9 @@
9989
11675
  function readUnitProperty(value) {
9990
11676
  return value && typeof value === "object" && "value" in value ? formatUnitValue3(value) : "";
9991
11677
  }
11678
+ function readUnitValue(value) {
11679
+ return value && typeof value === "object" && "value" in value ? value : void 0;
11680
+ }
9992
11681
  function readNumberProperty(value) {
9993
11682
  return typeof value === "number" ? String(value) : "";
9994
11683
  }
@@ -10294,6 +11983,12 @@
10294
11983
  .wo-editor-overlay-diagnostic-warning { border: 1px solid rgba(240, 180, 100, 0.24); }
10295
11984
  .wo-editor-outline { display: grid; gap: 14px; }
10296
11985
  .wo-editor-outline-section { display: grid; gap: 8px; }
11986
+ .wo-editor-outline-group { display: grid; gap: 6px; }
11987
+ .wo-editor-outline-children {
11988
+ display: grid;
11989
+ gap: 6px;
11990
+ padding-left: 16px;
11991
+ }
10297
11992
  .wo-editor-outline-section h3 {
10298
11993
  margin: 0;
10299
11994
  color: rgba(237,246,255,0.68);
@@ -10333,6 +12028,27 @@
10333
12028
  background: rgba(255, 120, 120, 0.18);
10334
12029
  color: #ffb2b2;
10335
12030
  }
12031
+ .wo-editor-inline-list { display: grid; gap: 8px; }
12032
+ .wo-editor-inline-actions {
12033
+ display: flex;
12034
+ flex-wrap: wrap;
12035
+ gap: 10px;
12036
+ margin-top: 12px;
12037
+ }
12038
+ .wo-editor-inline-actions button {
12039
+ border: 1px solid rgba(255,255,255,0.14);
12040
+ border-radius: 999px;
12041
+ background: rgba(255,255,255,0.06);
12042
+ color: #edf6ff;
12043
+ cursor: pointer;
12044
+ font: 600 12px/1.2 "Segoe UI Variable", "Segoe UI", sans-serif;
12045
+ padding: 8px 12px;
12046
+ }
12047
+ .wo-editor-inline-note {
12048
+ margin: 0 0 12px;
12049
+ color: rgba(237,246,255,0.72);
12050
+ font: 500 12px/1.5 "Segoe UI Variable", "Segoe UI", sans-serif;
12051
+ }
10336
12052
  .wo-editor-diagnostics { display: grid; gap: 10px; }
10337
12053
  .wo-editor-diagnostic {
10338
12054
  display: grid;