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
@@ -74,6 +74,23 @@ for (const spec of [
74
74
  });
75
75
  }
76
76
  const DRAFT_OBJECT_FIELD_KEYS = new Set(DRAFT_OBJECT_FIELD_SPECS.keys());
77
+ const EVENT_POSE_FIELD_KEYS = new Set([
78
+ "orbit",
79
+ "distance",
80
+ "semiMajor",
81
+ "eccentricity",
82
+ "period",
83
+ "angle",
84
+ "inclination",
85
+ "phase",
86
+ "at",
87
+ "surface",
88
+ "free",
89
+ "inner",
90
+ "outer",
91
+ "epoch",
92
+ "referencePlane",
93
+ ]);
77
94
  export function parseWorldOrbitAtlas(source) {
78
95
  return parseAtlasSource(source);
79
96
  }
@@ -91,12 +108,15 @@ function parseAtlasSource(source, forcedOutputVersion) {
91
108
  const objectNodes = [];
92
109
  const groups = [];
93
110
  const relations = [];
111
+ const events = [];
112
+ const eventPoseNodes = new Map();
94
113
  let sawDefaults = false;
95
114
  let sawAtlas = false;
96
115
  const viewpointIds = new Set();
97
116
  const annotationIds = new Set();
98
117
  const groupIds = new Set();
99
118
  const relationIds = new Set();
119
+ const eventIds = new Set();
100
120
  for (let index = 0; index < lines.length; index++) {
101
121
  const rawLine = lines[index];
102
122
  const lineNumber = index + 1;
@@ -114,7 +134,7 @@ function parseAtlasSource(source, forcedOutputVersion) {
114
134
  if (!sawSchemaHeader) {
115
135
  sourceSchemaVersion = assertDraftSchemaHeader(tokens, lineNumber);
116
136
  sawSchemaHeader = true;
117
- if (prepared.comments.length > 0 && sourceSchemaVersion !== "2.1") {
137
+ if (prepared.comments.length > 0 && isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
118
138
  diagnostics.push({
119
139
  code: "parse.schema21.commentCompatibility",
120
140
  severity: "warning",
@@ -127,7 +147,7 @@ function parseAtlasSource(source, forcedOutputVersion) {
127
147
  continue;
128
148
  }
129
149
  if (indent === 0) {
130
- section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, { sawDefaults, sawAtlas });
150
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, { sawDefaults, sawAtlas });
131
151
  if (section.kind === "system") {
132
152
  system = section.system;
133
153
  }
@@ -148,6 +168,7 @@ function parseAtlasSource(source, forcedOutputVersion) {
148
168
  throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
149
169
  }
150
170
  const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
171
+ const normalizedEvents = events.map((event) => normalizeDraftEvent(event, eventPoseNodes.get(event.id) ?? []));
151
172
  const outputVersion = forcedOutputVersion ??
152
173
  (sourceSchemaVersion === "2.0-draft" ? "2.0" : sourceSchemaVersion);
153
174
  const baseDocument = {
@@ -156,6 +177,7 @@ function parseAtlasSource(source, forcedOutputVersion) {
156
177
  system,
157
178
  groups,
158
179
  relations,
180
+ events: normalizedEvents,
159
181
  objects,
160
182
  diagnostics,
161
183
  };
@@ -187,17 +209,19 @@ function parseAtlasSource(source, forcedOutputVersion) {
187
209
  function assertDraftSchemaHeader(tokens, line) {
188
210
  if (tokens.length !== 2 ||
189
211
  tokens[0].value.toLowerCase() !== "schema" ||
190
- !["2.0-draft", "2.0", "2.1"].includes(tokens[1].value.toLowerCase())) {
191
- throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
212
+ !["2.0-draft", "2.0", "2.1", "2.5"].includes(tokens[1].value.toLowerCase())) {
213
+ 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);
192
214
  }
193
215
  const version = tokens[1].value.toLowerCase();
194
- return version === "2.1"
195
- ? "2.1"
196
- : version === "2.0-draft"
197
- ? "2.0-draft"
198
- : "2.0";
216
+ return version === "2.5"
217
+ ? "2.5"
218
+ : version === "2.1"
219
+ ? "2.1"
220
+ : version === "2.0-draft"
221
+ ? "2.0-draft"
222
+ : "2.0";
199
223
  }
200
- function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, viewpointIds, annotationIds, groupIds, relationIds, flags) {
224
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, flags) {
201
225
  const keyword = tokens[0]?.value.toLowerCase();
202
226
  switch (keyword) {
203
227
  case "system":
@@ -215,6 +239,8 @@ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, sy
215
239
  return {
216
240
  kind: "defaults",
217
241
  system,
242
+ sourceSchemaVersion,
243
+ diagnostics,
218
244
  seenFields: new Set(),
219
245
  };
220
246
  case "atlas":
@@ -234,7 +260,7 @@ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, sy
234
260
  if (!system) {
235
261
  throw new WorldOrbitError('Atlas section "viewpoint" requires a preceding system declaration', line, tokens[0].column);
236
262
  }
237
- return startViewpointSection(tokens, line, system, viewpointIds);
263
+ return startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics);
238
264
  case "annotation":
239
265
  if (!system) {
240
266
  throw new WorldOrbitError('Atlas section "annotation" requires a preceding system declaration', line, tokens[0].column);
@@ -246,6 +272,9 @@ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, sy
246
272
  case "relation":
247
273
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "relation", { line, column: tokens[0].column });
248
274
  return startRelationSection(tokens, line, relations, relationIds);
275
+ case "event":
276
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "event", { line, column: tokens[0].column });
277
+ return startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics);
249
278
  case "object":
250
279
  return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
251
280
  default:
@@ -282,7 +311,7 @@ function startSystemSection(tokens, line, sourceSchemaVersion, diagnostics) {
282
311
  seenFields: new Set(),
283
312
  };
284
313
  }
285
- function startViewpointSection(tokens, line, system, viewpointIds) {
314
+ function startViewpointSection(tokens, line, system, viewpointIds, sourceSchemaVersion, diagnostics) {
286
315
  if (tokens.length !== 2) {
287
316
  throw new WorldOrbitError("Invalid viewpoint declaration", line, tokens[0]?.column ?? 1);
288
317
  }
@@ -299,10 +328,12 @@ function startViewpointSection(tokens, line, system, viewpointIds) {
299
328
  summary: "",
300
329
  focusObjectId: null,
301
330
  selectedObjectId: null,
331
+ events: [],
302
332
  projection: system.defaults.view,
303
333
  preset: system.defaults.preset,
304
334
  zoom: null,
305
335
  rotationDeg: 0,
336
+ camera: null,
306
337
  layers: {},
307
338
  filter: null,
308
339
  };
@@ -311,10 +342,15 @@ function startViewpointSection(tokens, line, system, viewpointIds) {
311
342
  return {
312
343
  kind: "viewpoint",
313
344
  viewpoint,
345
+ sourceSchemaVersion,
346
+ diagnostics,
314
347
  seenFields: new Set(),
315
348
  inFilter: false,
316
349
  filterIndent: null,
317
350
  seenFilterFields: new Set(),
351
+ inCamera: false,
352
+ cameraIndent: null,
353
+ seenCameraFields: new Set(),
318
354
  };
319
355
  }
320
356
  function startAnnotationSection(tokens, line, system, annotationIds) {
@@ -401,6 +437,51 @@ function startRelationSection(tokens, line, relations, relationIds) {
401
437
  seenFields: new Set(),
402
438
  };
403
439
  }
440
+ function startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics) {
441
+ if (tokens.length !== 2) {
442
+ throw new WorldOrbitError("Invalid event declaration", line, tokens[0]?.column ?? 1);
443
+ }
444
+ const id = normalizeIdentifier(tokens[1].value);
445
+ if (!id) {
446
+ throw new WorldOrbitError("Event id must not be empty", line, tokens[1].column);
447
+ }
448
+ if (eventIds.has(id)) {
449
+ throw new WorldOrbitError(`Duplicate event id "${id}"`, line, tokens[1].column);
450
+ }
451
+ const event = {
452
+ id,
453
+ kind: "",
454
+ label: humanizeIdentifier(id),
455
+ summary: null,
456
+ targetObjectId: null,
457
+ participantObjectIds: [],
458
+ timing: null,
459
+ visibility: null,
460
+ epoch: null,
461
+ referencePlane: null,
462
+ tags: [],
463
+ color: null,
464
+ hidden: false,
465
+ positions: [],
466
+ };
467
+ const rawPoses = [];
468
+ events.push(event);
469
+ eventPoseNodes.set(id, rawPoses);
470
+ eventIds.add(id);
471
+ return {
472
+ kind: "event",
473
+ event,
474
+ sourceSchemaVersion,
475
+ diagnostics,
476
+ seenFields: new Set(),
477
+ rawPoses,
478
+ inPositions: false,
479
+ positionsIndent: null,
480
+ activePose: null,
481
+ poseIndent: null,
482
+ activePoseSeenFields: new Set(),
483
+ };
484
+ }
404
485
  function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
405
486
  if (tokens.length < 3) {
406
487
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
@@ -457,6 +538,9 @@ function handleSectionLine(section, indent, tokens, line) {
457
538
  case "relation":
458
539
  applyRelationField(section, tokens, line);
459
540
  return;
541
+ case "event":
542
+ applyEventField(section, indent, tokens, line);
543
+ return;
460
544
  case "object":
461
545
  applyObjectField(section, indent, tokens, line);
462
546
  return;
@@ -499,6 +583,12 @@ function applyDefaultsField(section, tokens, line) {
499
583
  const value = joinFieldValue(tokens, line);
500
584
  switch (key) {
501
585
  case "view":
586
+ if (isSchema25Projection(value)) {
587
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "defaults.view", {
588
+ line,
589
+ column: tokens[0].column,
590
+ });
591
+ }
502
592
  section.system.defaults.view = parseProjectionValue(value, line, tokens[0].column);
503
593
  return;
504
594
  case "scale":
@@ -538,14 +628,36 @@ function applyAtlasField(section, indent, tokens, line) {
538
628
  throw new WorldOrbitError(`Unknown atlas field "${tokens[0].value}"`, line, tokens[0].column);
539
629
  }
540
630
  function applyViewpointField(section, indent, tokens, line) {
631
+ if (section.inCamera && indent <= (section.cameraIndent ?? 0)) {
632
+ section.inCamera = false;
633
+ section.cameraIndent = null;
634
+ }
541
635
  if (section.inFilter && indent <= (section.filterIndent ?? 0)) {
542
636
  section.inFilter = false;
543
637
  section.filterIndent = null;
544
638
  }
639
+ if (section.inCamera) {
640
+ applyViewpointCameraField(section, tokens, line);
641
+ return;
642
+ }
545
643
  if (section.inFilter) {
546
644
  applyViewpointFilterField(section, tokens, line);
547
645
  return;
548
646
  }
647
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "camera") {
648
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.camera", {
649
+ line,
650
+ column: tokens[0].column,
651
+ });
652
+ if (section.seenFields.has("camera")) {
653
+ throw new WorldOrbitError('Duplicate viewpoint field "camera"', line, tokens[0].column);
654
+ }
655
+ section.seenFields.add("camera");
656
+ section.inCamera = true;
657
+ section.cameraIndent = indent;
658
+ section.viewpoint.camera = section.viewpoint.camera ?? createEmptyViewCamera();
659
+ return;
660
+ }
549
661
  if (tokens.length === 1 && tokens[0].value.toLowerCase() === "filter") {
550
662
  if (section.seenFields.has("filter")) {
551
663
  throw new WorldOrbitError('Duplicate viewpoint field "filter"', line, tokens[0].column);
@@ -571,6 +683,12 @@ function applyViewpointField(section, indent, tokens, line) {
571
683
  section.viewpoint.selectedObjectId = value;
572
684
  return;
573
685
  case "projection":
686
+ if (isSchema25Projection(value)) {
687
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "projection", {
688
+ line,
689
+ column: tokens[0].column,
690
+ });
691
+ }
574
692
  section.viewpoint.projection = parseProjectionValue(value, line, tokens[0].column);
575
693
  return;
576
694
  case "preset":
@@ -582,13 +700,49 @@ function applyViewpointField(section, indent, tokens, line) {
582
700
  case "rotation":
583
701
  section.viewpoint.rotationDeg = parseFiniteNumber(value, line, tokens[0].column, "rotation");
584
702
  return;
703
+ case "camera":
704
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.camera", {
705
+ line,
706
+ column: tokens[0].column,
707
+ });
708
+ section.viewpoint.camera = parseInlineViewCamera(tokens.slice(1), line, section.viewpoint.camera);
709
+ return;
585
710
  case "layers":
586
- section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line);
711
+ section.viewpoint.layers = parseLayerTokens(tokens.slice(1), line, section.sourceSchemaVersion, section.diagnostics);
712
+ return;
713
+ case "events":
714
+ warnIfSchema21Feature(section.sourceSchemaVersion, section.diagnostics, "viewpoint.events", {
715
+ line,
716
+ column: tokens[0].column,
717
+ });
718
+ section.viewpoint.events = parseTokenList(tokens.slice(1), line, "events");
587
719
  return;
588
720
  default:
589
721
  throw new WorldOrbitError(`Unknown viewpoint field "${tokens[0].value}"`, line, tokens[0].column);
590
722
  }
591
723
  }
724
+ function applyViewpointCameraField(section, tokens, line) {
725
+ const key = requireUniqueField(tokens, section.seenCameraFields, line);
726
+ const value = joinFieldValue(tokens, line);
727
+ const camera = section.viewpoint.camera ?? createEmptyViewCamera();
728
+ switch (key) {
729
+ case "azimuth":
730
+ camera.azimuth = parseFiniteNumber(value, line, tokens[0].column, "camera.azimuth");
731
+ break;
732
+ case "elevation":
733
+ camera.elevation = parseFiniteNumber(value, line, tokens[0].column, "camera.elevation");
734
+ break;
735
+ case "roll":
736
+ camera.roll = parseFiniteNumber(value, line, tokens[0].column, "camera.roll");
737
+ break;
738
+ case "distance":
739
+ camera.distance = parsePositiveNumber(value, line, tokens[0].column, "camera.distance");
740
+ break;
741
+ default:
742
+ throw new WorldOrbitError(`Unknown viewpoint camera field "${tokens[0].value}"`, line, tokens[0].column);
743
+ }
744
+ section.viewpoint.camera = camera;
745
+ }
592
746
  function applyViewpointFilterField(section, tokens, line) {
593
747
  const key = requireUniqueField(tokens, section.seenFilterFields, line);
594
748
  const filter = section.viewpoint.filter ?? createEmptyViewpointFilter();
@@ -688,6 +842,127 @@ function applyRelationField(section, tokens, line) {
688
842
  throw new WorldOrbitError(`Unknown relation field "${tokens[0].value}"`, line, tokens[0].column);
689
843
  }
690
844
  }
845
+ function applyEventField(section, indent, tokens, line) {
846
+ if (section.activePose && indent <= (section.poseIndent ?? 0)) {
847
+ section.activePose = null;
848
+ section.poseIndent = null;
849
+ section.activePoseSeenFields.clear();
850
+ }
851
+ if (!section.activePose && section.inPositions && indent <= (section.positionsIndent ?? 0)) {
852
+ section.inPositions = false;
853
+ section.positionsIndent = null;
854
+ }
855
+ if (section.activePose) {
856
+ if (tokens[0]?.value === "epoch" ||
857
+ tokens[0]?.value === "referencePlane") {
858
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, `pose.${tokens[0].value}`, {
859
+ line,
860
+ column: tokens[0]?.column ?? 1,
861
+ });
862
+ }
863
+ section.activePose.fields.push(parseEventPoseField(tokens, line, section.activePoseSeenFields));
864
+ return;
865
+ }
866
+ if (section.inPositions) {
867
+ if (tokens.length !== 2 || tokens[0].value.toLowerCase() !== "pose") {
868
+ throw new WorldOrbitError(`Unknown event positions field "${tokens[0].value}"`, line, tokens[0]?.column ?? 1);
869
+ }
870
+ const objectId = tokens[1].value;
871
+ if (!objectId.trim()) {
872
+ throw new WorldOrbitError("Event pose object id must not be empty", line, tokens[1].column);
873
+ }
874
+ const rawPose = {
875
+ objectId,
876
+ fields: [],
877
+ location: { line, column: tokens[0].column },
878
+ };
879
+ section.rawPoses.push(rawPose);
880
+ section.activePose = rawPose;
881
+ section.poseIndent = indent;
882
+ section.activePoseSeenFields = new Set();
883
+ return;
884
+ }
885
+ if (tokens.length === 1 && tokens[0].value.toLowerCase() === "positions") {
886
+ if (section.seenFields.has("positions")) {
887
+ throw new WorldOrbitError('Duplicate event field "positions"', line, tokens[0].column);
888
+ }
889
+ section.seenFields.add("positions");
890
+ section.inPositions = true;
891
+ section.positionsIndent = indent;
892
+ return;
893
+ }
894
+ const key = requireUniqueField(tokens, section.seenFields, line);
895
+ switch (key) {
896
+ case "kind":
897
+ section.event.kind = joinFieldValue(tokens, line);
898
+ return;
899
+ case "label":
900
+ section.event.label = joinFieldValue(tokens, line);
901
+ return;
902
+ case "summary":
903
+ section.event.summary = joinFieldValue(tokens, line);
904
+ return;
905
+ case "target":
906
+ section.event.targetObjectId = joinFieldValue(tokens, line);
907
+ return;
908
+ case "participants":
909
+ section.event.participantObjectIds = parseTokenList(tokens.slice(1), line, "participants");
910
+ return;
911
+ case "timing":
912
+ section.event.timing = joinFieldValue(tokens, line);
913
+ return;
914
+ case "visibility":
915
+ section.event.visibility = joinFieldValue(tokens, line);
916
+ return;
917
+ case "epoch":
918
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "event.epoch", {
919
+ line,
920
+ column: tokens[0].column,
921
+ });
922
+ section.event.epoch = joinFieldValue(tokens, line);
923
+ return;
924
+ case "referenceplane":
925
+ warnIfSchema25Feature(section.sourceSchemaVersion, section.diagnostics, "event.referencePlane", {
926
+ line,
927
+ column: tokens[0].column,
928
+ });
929
+ section.event.referencePlane = joinFieldValue(tokens, line);
930
+ return;
931
+ case "tags":
932
+ section.event.tags = parseTokenList(tokens.slice(1), line, "tags");
933
+ return;
934
+ case "color":
935
+ section.event.color = joinFieldValue(tokens, line);
936
+ return;
937
+ case "hidden":
938
+ section.event.hidden = parseAtlasBoolean(joinFieldValue(tokens, line), "hidden", {
939
+ line,
940
+ column: tokens[0].column,
941
+ });
942
+ return;
943
+ default:
944
+ throw new WorldOrbitError(`Unknown event field "${tokens[0].value}"`, line, tokens[0].column);
945
+ }
946
+ }
947
+ function parseEventPoseField(tokens, line, seenFields) {
948
+ if (tokens.length < 2) {
949
+ throw new WorldOrbitError("Invalid event pose field line", line, tokens[0]?.column ?? 1);
950
+ }
951
+ const key = tokens[0].value;
952
+ if (!EVENT_POSE_FIELD_KEYS.has(key)) {
953
+ throw new WorldOrbitError(`Unknown event pose field "${key}"`, line, tokens[0].column);
954
+ }
955
+ if (seenFields.has(key)) {
956
+ throw new WorldOrbitError(`Duplicate event pose field "${key}"`, line, tokens[0].column);
957
+ }
958
+ seenFields.add(key);
959
+ return {
960
+ type: "field",
961
+ key,
962
+ values: tokens.slice(1).map((token) => token.value),
963
+ location: { line, column: tokens[0].column },
964
+ };
965
+ }
691
966
  function applyObjectField(section, indent, tokens, line) {
692
967
  if (section.activeBlock && indent <= (section.blockIndent ?? 0)) {
693
968
  section.activeBlock = null;
@@ -760,7 +1035,7 @@ function parseObjectTypeTokens(tokens, line) {
760
1035
  value === "structure" ||
761
1036
  value === "phenomenon");
762
1037
  }
763
- function parseLayerTokens(tokens, line) {
1038
+ function parseLayerTokens(tokens, line, sourceSchemaVersion, diagnostics) {
764
1039
  const layers = {};
765
1040
  for (const token of parseTokenList(tokens, line, "layers")) {
766
1041
  const enabled = !token.startsWith("-") && !token.startsWith("!");
@@ -775,9 +1050,16 @@ function parseLayerTokens(tokens, line) {
775
1050
  raw === "orbits-back" ||
776
1051
  raw === "orbits-front" ||
777
1052
  raw === "relations" ||
1053
+ raw === "events" ||
778
1054
  raw === "objects" ||
779
1055
  raw === "labels" ||
780
1056
  raw === "metadata") {
1057
+ if (raw === "events" && sourceSchemaVersion && diagnostics) {
1058
+ warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "layers.events", {
1059
+ line,
1060
+ column: tokens[0]?.column ?? 1,
1061
+ });
1062
+ }
781
1063
  layers[raw] = enabled;
782
1064
  }
783
1065
  }
@@ -795,11 +1077,18 @@ function parseTokenList(tokens, line, fieldName) {
795
1077
  }
796
1078
  function parseProjectionValue(value, line, column) {
797
1079
  const normalized = value.toLowerCase();
798
- if (normalized !== "topdown" && normalized !== "isometric") {
1080
+ if (normalized !== "topdown" &&
1081
+ normalized !== "isometric" &&
1082
+ normalized !== "orthographic" &&
1083
+ normalized !== "perspective") {
799
1084
  throw new WorldOrbitError(`Unknown projection "${value}"`, line, column);
800
1085
  }
801
1086
  return normalized;
802
1087
  }
1088
+ function isSchema25Projection(value) {
1089
+ const normalized = value.toLowerCase();
1090
+ return normalized === "orthographic" || normalized === "perspective";
1091
+ }
803
1092
  function parsePresetValue(value, line, column) {
804
1093
  const normalized = value.toLowerCase();
805
1094
  if (normalized === "diagram" ||
@@ -832,6 +1121,48 @@ function createEmptyViewpointFilter() {
832
1121
  groupIds: [],
833
1122
  };
834
1123
  }
1124
+ function createEmptyViewCamera() {
1125
+ return {
1126
+ azimuth: null,
1127
+ elevation: null,
1128
+ roll: null,
1129
+ distance: null,
1130
+ };
1131
+ }
1132
+ function parseInlineViewCamera(tokens, line, current) {
1133
+ if (tokens.length === 0 || tokens.length % 2 !== 0) {
1134
+ throw new WorldOrbitError('Field "camera" expects "<field> <value>" pairs', line, tokens[0]?.column ?? 1);
1135
+ }
1136
+ const camera = current ? { ...current } : createEmptyViewCamera();
1137
+ const seen = new Set();
1138
+ for (let index = 0; index < tokens.length; index += 2) {
1139
+ const fieldToken = tokens[index];
1140
+ const valueToken = tokens[index + 1];
1141
+ const key = fieldToken.value.toLowerCase();
1142
+ if (seen.has(key)) {
1143
+ throw new WorldOrbitError(`Duplicate viewpoint camera field "${fieldToken.value}"`, line, fieldToken.column);
1144
+ }
1145
+ seen.add(key);
1146
+ const value = valueToken.value;
1147
+ switch (key) {
1148
+ case "azimuth":
1149
+ camera.azimuth = parseFiniteNumber(value, line, fieldToken.column, "camera.azimuth");
1150
+ break;
1151
+ case "elevation":
1152
+ camera.elevation = parseFiniteNumber(value, line, fieldToken.column, "camera.elevation");
1153
+ break;
1154
+ case "roll":
1155
+ camera.roll = parseFiniteNumber(value, line, fieldToken.column, "camera.roll");
1156
+ break;
1157
+ case "distance":
1158
+ camera.distance = parsePositiveNumber(value, line, fieldToken.column, "camera.distance");
1159
+ break;
1160
+ default:
1161
+ throw new WorldOrbitError(`Unknown viewpoint camera field "${fieldToken.value}"`, line, fieldToken.column);
1162
+ }
1163
+ }
1164
+ return camera;
1165
+ }
835
1166
  function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion, diagnostics) {
836
1167
  const fields = [];
837
1168
  let index = 0;
@@ -921,7 +1252,7 @@ function parseInfoLikeEntry(tokens, line, errorMessage) {
921
1252
  }
922
1253
  function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
923
1254
  const fieldMap = collectDraftFields(node.fields);
924
- const placement = extractDraftPlacement(node.objectType, fieldMap);
1255
+ const placement = extractPlacementFromFieldMap(fieldMap);
925
1256
  const properties = normalizeDraftProperties(node.objectType, fieldMap);
926
1257
  const groups = parseOptionalTokenList(fieldMap.get("groups")?.[0]);
927
1258
  const epoch = parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]);
@@ -972,7 +1303,7 @@ function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
972
1303
  object.tolerances = tolerances;
973
1304
  if (typedBlocks && Object.keys(typedBlocks).length > 0)
974
1305
  object.typedBlocks = typedBlocks;
975
- if (sourceSchemaVersion !== "2.1") {
1306
+ if (isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
976
1307
  if (object.groups ||
977
1308
  object.epoch ||
978
1309
  object.referencePlane ||
@@ -989,14 +1320,34 @@ function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
989
1320
  }
990
1321
  return object;
991
1322
  }
992
- function collectDraftFields(fields) {
1323
+ function normalizeDraftEvent(event, rawPoses) {
1324
+ return {
1325
+ ...event,
1326
+ participantObjectIds: [...new Set(event.participantObjectIds)],
1327
+ tags: [...new Set(event.tags)],
1328
+ positions: rawPoses.map((pose) => normalizeDraftEventPose(pose)),
1329
+ };
1330
+ }
1331
+ function normalizeDraftEventPose(rawPose) {
1332
+ const fieldMap = collectDraftFields(rawPose.fields, "event-pose");
1333
+ const placement = extractPlacementFromFieldMap(fieldMap);
1334
+ return {
1335
+ objectId: rawPose.objectId,
1336
+ placement,
1337
+ inner: parseOptionalUnitField(fieldMap.get("inner")?.[0], "inner"),
1338
+ outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer"),
1339
+ epoch: parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]),
1340
+ referencePlane: parseOptionalJoinedValue(fieldMap.get("referencePlane")?.[0]),
1341
+ };
1342
+ }
1343
+ function collectDraftFields(fields, _mode = "object") {
993
1344
  const grouped = new Map();
994
1345
  for (const field of fields) {
995
1346
  const spec = getDraftObjectFieldSpec(field.key);
996
- if (!spec) {
1347
+ if (!spec && !EVENT_POSE_FIELD_KEYS.has(field.key)) {
997
1348
  throw WorldOrbitError.fromLocation(`Unknown field "${field.key}"`, field.location);
998
1349
  }
999
- if (!spec.allowRepeat && grouped.has(field.key)) {
1350
+ if (!spec?.allowRepeat && grouped.has(field.key)) {
1000
1351
  throw WorldOrbitError.fromLocation(`Duplicate field "${field.key}"`, field.location);
1001
1352
  }
1002
1353
  const existing = grouped.get(field.key) ?? [];
@@ -1005,7 +1356,7 @@ function collectDraftFields(fields) {
1005
1356
  }
1006
1357
  return grouped;
1007
1358
  }
1008
- function extractDraftPlacement(objectType, fieldMap) {
1359
+ function extractPlacementFromFieldMap(fieldMap) {
1009
1360
  const orbitField = fieldMap.get("orbit")?.[0];
1010
1361
  const atField = fieldMap.get("at")?.[0];
1011
1362
  const surfaceField = fieldMap.get("surface")?.[0];
@@ -1174,7 +1525,7 @@ function validateDraftObjectFieldCompatibility(fields, objectType) {
1174
1525
  }
1175
1526
  }
1176
1527
  function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, location) {
1177
- if (sourceSchemaVersion === "2.1") {
1528
+ if (!isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
1178
1529
  return;
1179
1530
  }
1180
1531
  diagnostics.push({
@@ -1186,6 +1537,34 @@ function warnIfSchema21Feature(sourceSchemaVersion, diagnostics, featureName, lo
1186
1537
  column: location.column,
1187
1538
  });
1188
1539
  }
1540
+ function warnIfSchema25Feature(sourceSchemaVersion, diagnostics, featureName, location) {
1541
+ if (!isSchemaOlderThan(sourceSchemaVersion, "2.5")) {
1542
+ return;
1543
+ }
1544
+ diagnostics.push({
1545
+ code: "parse.schema25.featureCompatibility",
1546
+ severity: "warning",
1547
+ source: "parse",
1548
+ message: `Feature "${featureName}" requires schema 2.5; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
1549
+ line: location.line,
1550
+ column: location.column,
1551
+ });
1552
+ }
1553
+ function isSchemaOlderThan(sourceSchemaVersion, requiredVersion) {
1554
+ return schemaVersionRank(sourceSchemaVersion) < schemaVersionRank(requiredVersion);
1555
+ }
1556
+ function schemaVersionRank(version) {
1557
+ switch (version) {
1558
+ case "2.0-draft":
1559
+ return 0;
1560
+ case "2.0":
1561
+ return 1;
1562
+ case "2.1":
1563
+ return 2;
1564
+ case "2.5":
1565
+ return 3;
1566
+ }
1567
+ }
1189
1568
  function preprocessAtlasSource(source) {
1190
1569
  const chars = [...source];
1191
1570
  const comments = [];
@@ -1,4 +1,4 @@
1
- import type { SceneRenderOptions, WorldOrbitAtlasDocument, WorldOrbitAtlasSystem, WorldOrbitDiagnostic, WorldOrbitDocument, WorldOrbitObject } from "./types.js";
1
+ import type { SceneRenderOptions, WorldOrbitAtlasDocument, WorldOrbitEvent, WorldOrbitAtlasSystem, WorldOrbitDiagnostic, WorldOrbitDocument, WorldOrbitObject } from "./types.js";
2
2
  interface UpgradeOptions extends Pick<SceneRenderOptions, "preset" | "projection"> {
3
3
  }
4
4
  export declare function upgradeDocumentToV2(document: WorldOrbitDocument, options?: UpgradeOptions): WorldOrbitAtlasDocument;
@@ -10,9 +10,12 @@ export declare function upgradeDocumentToDraftV2(document: WorldOrbitDocument, o
10
10
  system: WorldOrbitAtlasSystem | null;
11
11
  groups: import("./types.js").WorldOrbitGroup[];
12
12
  relations: import("./types.js").WorldOrbitRelation[];
13
+ events: WorldOrbitEvent[];
13
14
  objects: WorldOrbitObject[];
14
15
  diagnostics: WorldOrbitDiagnostic[];
15
16
  };
16
- export declare function materializeAtlasDocument(document: WorldOrbitAtlasDocument): WorldOrbitDocument;
17
+ export declare function materializeAtlasDocument(document: WorldOrbitAtlasDocument, options?: {
18
+ activeEventId?: string | null;
19
+ }): WorldOrbitDocument;
17
20
  export declare function materializeDraftDocument(document: WorldOrbitAtlasDocument): WorldOrbitDocument;
18
21
  export {};