worldorbit 3.2.1 → 4.0.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 (143) hide show
  1. package/README.md +546 -545
  2. package/dist/browser/core/dist/atlas-edit.js +146 -1
  3. package/dist/browser/core/dist/atlas-validate.js +105 -10
  4. package/dist/browser/core/dist/draft-parse.js +341 -16
  5. package/dist/browser/core/dist/draft.d.ts +2 -1
  6. package/dist/browser/core/dist/draft.js +25 -3
  7. package/dist/browser/core/dist/format.js +86 -4
  8. package/dist/browser/core/dist/index.d.ts +1 -0
  9. package/dist/browser/core/dist/index.js +1 -0
  10. package/dist/browser/core/dist/load.js +7 -2
  11. package/dist/browser/core/dist/normalize.js +1 -0
  12. package/dist/browser/core/dist/scene.js +11 -2
  13. package/dist/browser/core/dist/schema.js +11 -1
  14. package/dist/browser/core/dist/solver.d.ts +26 -0
  15. package/dist/browser/core/dist/solver.js +27 -0
  16. package/dist/browser/core/dist/types.d.ts +57 -3
  17. package/dist/browser/editor/dist/editor.js +844 -719
  18. package/dist/browser/editor/dist/types.d.ts +2 -1
  19. package/dist/browser/obsidian-plugin/dist/diagnostics.d.ts +3 -0
  20. package/dist/browser/obsidian-plugin/dist/diagnostics.js +23 -0
  21. package/dist/browser/obsidian-plugin/dist/examples.d.ts +3 -0
  22. package/dist/browser/obsidian-plugin/dist/examples.js +77 -0
  23. package/dist/browser/obsidian-plugin/dist/index.d.ts +9 -0
  24. package/dist/browser/obsidian-plugin/dist/index.js +8 -0
  25. package/dist/browser/obsidian-plugin/dist/main.d.ts +2 -0
  26. package/dist/browser/obsidian-plugin/dist/main.js +2 -0
  27. package/dist/browser/obsidian-plugin/dist/navigation.d.ts +6 -0
  28. package/dist/browser/obsidian-plugin/dist/navigation.js +44 -0
  29. package/dist/browser/obsidian-plugin/dist/plugin.d.ts +8 -0
  30. package/dist/browser/obsidian-plugin/dist/plugin.js +508 -0
  31. package/dist/browser/obsidian-plugin/dist/positions.d.ts +7 -0
  32. package/dist/browser/obsidian-plugin/dist/positions.js +14 -0
  33. package/dist/browser/obsidian-plugin/dist/settings.d.ts +2 -0
  34. package/dist/browser/obsidian-plugin/dist/settings.js +5 -0
  35. package/dist/browser/obsidian-plugin/dist/theme.d.ts +2 -0
  36. package/dist/browser/obsidian-plugin/dist/theme.js +31 -0
  37. package/dist/browser/obsidian-plugin/dist/types.d.ts +42 -0
  38. package/dist/browser/obsidian-plugin/dist/types.js +1 -0
  39. package/dist/browser/obsidian-plugin/dist/viewer-host.d.ts +14 -0
  40. package/dist/browser/obsidian-plugin/dist/viewer-host.js +110 -0
  41. package/dist/browser/viewer/dist/index.d.ts +1 -0
  42. package/dist/browser/viewer/dist/index.js +1 -0
  43. package/dist/browser/viewer/dist/interactive-2d.d.ts +21 -0
  44. package/dist/browser/viewer/dist/interactive-2d.js +201 -0
  45. package/dist/browser/viewer/dist/minimap.js +9 -7
  46. package/dist/browser/viewer/dist/render.d.ts +1 -1
  47. package/dist/browser/viewer/dist/render.js +25 -20
  48. package/dist/browser/viewer/dist/runtime-3d.js +2 -0
  49. package/dist/browser/viewer/dist/viewer-state.d.ts +1 -1
  50. package/dist/browser/viewer/dist/viewer-state.js +1 -1
  51. package/dist/browser/viewer/dist/viewer.js +7 -3
  52. package/dist/obsidian-plugin/LICENSE +21 -0
  53. package/dist/obsidian-plugin/README.md +141 -0
  54. package/dist/obsidian-plugin/main.js +108 -0
  55. package/dist/obsidian-plugin/manifest.json +9 -0
  56. package/dist/obsidian-plugin/obsidian_scsh_1.png +0 -0
  57. package/dist/obsidian-plugin/obsidian_scsh_2.png +0 -0
  58. package/dist/obsidian-plugin/styles.css +164 -0
  59. package/dist/unpkg/core/dist/atlas-edit.js +146 -1
  60. package/dist/unpkg/core/dist/atlas-validate.js +105 -10
  61. package/dist/unpkg/core/dist/draft-parse.js +341 -16
  62. package/dist/unpkg/core/dist/draft.d.ts +2 -1
  63. package/dist/unpkg/core/dist/draft.js +25 -3
  64. package/dist/unpkg/core/dist/format.js +86 -4
  65. package/dist/unpkg/core/dist/index.d.ts +1 -0
  66. package/dist/unpkg/core/dist/index.js +1 -0
  67. package/dist/unpkg/core/dist/load.js +7 -2
  68. package/dist/unpkg/core/dist/normalize.js +1 -0
  69. package/dist/unpkg/core/dist/scene.js +11 -2
  70. package/dist/unpkg/core/dist/schema.js +11 -1
  71. package/dist/unpkg/core/dist/solver.d.ts +26 -0
  72. package/dist/unpkg/core/dist/solver.js +27 -0
  73. package/dist/unpkg/core/dist/types.d.ts +57 -3
  74. package/dist/unpkg/editor/dist/editor.js +844 -719
  75. package/dist/unpkg/editor/dist/types.d.ts +2 -1
  76. package/dist/unpkg/obsidian-plugin/dist/diagnostics.d.ts +3 -0
  77. package/dist/unpkg/obsidian-plugin/dist/diagnostics.js +23 -0
  78. package/dist/unpkg/obsidian-plugin/dist/examples.d.ts +3 -0
  79. package/dist/unpkg/obsidian-plugin/dist/examples.js +77 -0
  80. package/dist/unpkg/obsidian-plugin/dist/index.d.ts +9 -0
  81. package/dist/unpkg/obsidian-plugin/dist/index.js +8 -0
  82. package/dist/unpkg/obsidian-plugin/dist/main.d.ts +2 -0
  83. package/dist/unpkg/obsidian-plugin/dist/main.js +2 -0
  84. package/dist/unpkg/obsidian-plugin/dist/navigation.d.ts +6 -0
  85. package/dist/unpkg/obsidian-plugin/dist/navigation.js +44 -0
  86. package/dist/unpkg/obsidian-plugin/dist/plugin.d.ts +8 -0
  87. package/dist/unpkg/obsidian-plugin/dist/plugin.js +508 -0
  88. package/dist/unpkg/obsidian-plugin/dist/positions.d.ts +7 -0
  89. package/dist/unpkg/obsidian-plugin/dist/positions.js +14 -0
  90. package/dist/unpkg/obsidian-plugin/dist/settings.d.ts +2 -0
  91. package/dist/unpkg/obsidian-plugin/dist/settings.js +5 -0
  92. package/dist/unpkg/obsidian-plugin/dist/theme.d.ts +2 -0
  93. package/dist/unpkg/obsidian-plugin/dist/theme.js +31 -0
  94. package/dist/unpkg/obsidian-plugin/dist/types.d.ts +42 -0
  95. package/dist/unpkg/obsidian-plugin/dist/types.js +1 -0
  96. package/dist/unpkg/obsidian-plugin/dist/viewer-host.d.ts +14 -0
  97. package/dist/unpkg/obsidian-plugin/dist/viewer-host.js +110 -0
  98. package/dist/unpkg/viewer/dist/index.d.ts +1 -0
  99. package/dist/unpkg/viewer/dist/index.js +1 -0
  100. package/dist/unpkg/viewer/dist/interactive-2d.d.ts +21 -0
  101. package/dist/unpkg/viewer/dist/interactive-2d.js +201 -0
  102. package/dist/unpkg/viewer/dist/minimap.js +9 -7
  103. package/dist/unpkg/viewer/dist/render.d.ts +1 -1
  104. package/dist/unpkg/viewer/dist/render.js +25 -20
  105. package/dist/unpkg/viewer/dist/runtime-3d.js +2 -0
  106. package/dist/unpkg/viewer/dist/viewer-state.d.ts +1 -1
  107. package/dist/unpkg/viewer/dist/viewer-state.js +1 -1
  108. package/dist/unpkg/viewer/dist/viewer.js +7 -3
  109. package/dist/unpkg/worldorbit-core.min.js +10 -10
  110. package/dist/unpkg/worldorbit-editor.min.js +359 -332
  111. package/dist/unpkg/worldorbit-markdown.min.js +28 -28
  112. package/dist/unpkg/worldorbit-viewer.min.js +203 -203
  113. package/dist/unpkg/worldorbit.js +958 -40
  114. package/dist/unpkg/worldorbit.min.js +214 -214
  115. package/package.json +22 -1
  116. package/packages/core/dist/atlas-edit.js +146 -1
  117. package/packages/core/dist/atlas-validate.js +105 -10
  118. package/packages/core/dist/draft-parse.js +341 -16
  119. package/packages/core/dist/draft.d.ts +2 -1
  120. package/packages/core/dist/draft.js +25 -3
  121. package/packages/core/dist/format.js +86 -4
  122. package/packages/core/dist/index.d.ts +1 -0
  123. package/packages/core/dist/index.js +1 -0
  124. package/packages/core/dist/load.js +7 -2
  125. package/packages/core/dist/normalize.js +1 -0
  126. package/packages/core/dist/scene.js +11 -2
  127. package/packages/core/dist/schema.js +11 -1
  128. package/packages/core/dist/solver.d.ts +26 -0
  129. package/packages/core/dist/solver.js +27 -0
  130. package/packages/core/dist/types.d.ts +57 -3
  131. package/packages/editor/dist/editor.js +844 -719
  132. package/packages/editor/dist/types.d.ts +2 -1
  133. package/packages/viewer/dist/index.d.ts +1 -0
  134. package/packages/viewer/dist/index.js +1 -0
  135. package/packages/viewer/dist/interactive-2d.d.ts +21 -0
  136. package/packages/viewer/dist/interactive-2d.js +201 -0
  137. package/packages/viewer/dist/minimap.js +9 -7
  138. package/packages/viewer/dist/render.d.ts +1 -1
  139. package/packages/viewer/dist/render.js +25 -20
  140. package/packages/viewer/dist/runtime-3d.js +2 -0
  141. package/packages/viewer/dist/viewer-state.d.ts +1 -1
  142. package/packages/viewer/dist/viewer-state.js +1 -1
  143. package/packages/viewer/dist/viewer.js +7 -3
@@ -8,6 +8,14 @@ const STRUCTURED_TYPED_BLOCKS = new Set([
8
8
  "habitability",
9
9
  "settlement",
10
10
  ]);
11
+ const TRAJECTORY_SEGMENT_KINDS = new Set([
12
+ "departure",
13
+ "transfer",
14
+ "flyby",
15
+ "capture",
16
+ "stationkeeping",
17
+ "escape",
18
+ ]);
11
19
  const DRAFT_OBJECT_FIELD_SPECS = new Map();
12
20
  for (const key of [
13
21
  "orbit",
@@ -40,6 +48,7 @@ for (const key of [
40
48
  "on",
41
49
  "source",
42
50
  "cycle",
51
+ "trajectory",
43
52
  ]) {
44
53
  const schema = getFieldSchema(key);
45
54
  if (schema) {
@@ -65,10 +74,11 @@ for (const spec of [
65
74
  { key: "validate", inlineMode: "single", allowRepeat: true },
66
75
  { key: "locked", inlineMode: "multiple", allowRepeat: false },
67
76
  { key: "tolerance", inlineMode: "pair", allowRepeat: true },
77
+ { key: "trajectory", inlineMode: "single", allowRepeat: false },
68
78
  ]) {
69
79
  DRAFT_OBJECT_FIELD_SPECS.set(spec.key, {
70
80
  key: spec.key,
71
- version: "2.1",
81
+ version: spec.key === "trajectory" ? "3.0" : "2.1",
72
82
  inlineMode: spec.inlineMode,
73
83
  allowRepeat: spec.allowRepeat,
74
84
  });
@@ -90,6 +100,8 @@ const EVENT_POSE_FIELD_KEYS = new Set([
90
100
  "outer",
91
101
  "epoch",
92
102
  "referencePlane",
103
+ "segment",
104
+ "maneuver",
93
105
  ]);
94
106
  export function parseWorldOrbitAtlas(source) {
95
107
  return parseAtlasSource(source);
@@ -109,6 +121,7 @@ function parseAtlasSource(source, forcedOutputVersion) {
109
121
  const groups = [];
110
122
  const relations = [];
111
123
  const events = [];
124
+ const trajectories = [];
112
125
  const eventPoseNodes = new Map();
113
126
  let sawDefaults = false;
114
127
  let sawAtlas = false;
@@ -117,6 +130,7 @@ function parseAtlasSource(source, forcedOutputVersion) {
117
130
  const groupIds = new Set();
118
131
  const relationIds = new Set();
119
132
  const eventIds = new Set();
133
+ const trajectoryIds = new Set();
120
134
  for (let index = 0; index < lines.length; index++) {
121
135
  const rawLine = lines[index];
122
136
  const lineNumber = index + 1;
@@ -147,7 +161,7 @@ function parseAtlasSource(source, forcedOutputVersion) {
147
161
  continue;
148
162
  }
149
163
  if (indent === 0) {
150
- section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, { sawDefaults, sawAtlas });
164
+ section = startTopLevelSection(tokens, lineNumber, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, trajectories, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, trajectoryIds, { sawDefaults, sawAtlas });
151
165
  if (section.kind === "system") {
152
166
  system = section.system;
153
167
  }
@@ -165,7 +179,7 @@ function parseAtlasSource(source, forcedOutputVersion) {
165
179
  handleSectionLine(section, indent, tokens, lineNumber);
166
180
  }
167
181
  if (!sawSchemaHeader) {
168
- throw new WorldOrbitError('Missing required atlas schema header "schema 2.0"');
182
+ throw new WorldOrbitError('Missing required atlas schema header "schema 2.0" or "schema 3.0"');
169
183
  }
170
184
  const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
171
185
  const normalizedEvents = events.map((event) => normalizeDraftEvent(event, eventPoseNodes.get(event.id) ?? []));
@@ -179,6 +193,7 @@ function parseAtlasSource(source, forcedOutputVersion) {
179
193
  groups,
180
194
  relations,
181
195
  events: normalizedEvents,
196
+ trajectories,
182
197
  objects,
183
198
  diagnostics,
184
199
  };
@@ -210,21 +225,23 @@ function parseAtlasSource(source, forcedOutputVersion) {
210
225
  function assertDraftSchemaHeader(tokens, line) {
211
226
  if (tokens.length !== 2 ||
212
227
  tokens[0].value.toLowerCase() !== "schema" ||
213
- !["2.0-draft", "2.0", "2.1", "2.5", "2.6"].includes(tokens[1].value.toLowerCase())) {
214
- throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", "schema 2.5", "schema 2.6", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
228
+ !["2.0-draft", "2.0", "2.1", "2.5", "2.6", "3.0"].includes(tokens[1].value.toLowerCase())) {
229
+ throw new WorldOrbitError('Expected atlas header "schema 2.0", "schema 2.1", "schema 2.5", "schema 2.6", "schema 3.0", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
215
230
  }
216
231
  const version = tokens[1].value.toLowerCase();
217
232
  return version === "2.6"
218
233
  ? "2.6"
219
- : version === "2.5"
220
- ? "2.5"
221
- : version === "2.1"
222
- ? "2.1"
223
- : version === "2.0-draft"
224
- ? "2.0-draft"
225
- : "2.0";
226
- }
227
- function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, flags) {
234
+ : version === "3.0"
235
+ ? "3.0"
236
+ : version === "2.5"
237
+ ? "2.5"
238
+ : version === "2.1"
239
+ ? "2.1"
240
+ : version === "2.0-draft"
241
+ ? "2.0-draft"
242
+ : "2.0";
243
+ }
244
+ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, trajectories, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, trajectoryIds, flags) {
228
245
  const keyword = tokens[0]?.value.toLowerCase();
229
246
  switch (keyword) {
230
247
  case "system":
@@ -278,6 +295,9 @@ function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, sy
278
295
  case "event":
279
296
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "event", { line, column: tokens[0].column });
280
297
  return startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourceSchemaVersion, diagnostics);
298
+ case "trajectory":
299
+ warnIfSchema30Feature(sourceSchemaVersion, diagnostics, "trajectory", { line, column: tokens[0].column });
300
+ return startTrajectorySection(tokens, line, trajectories, trajectoryIds, sourceSchemaVersion, diagnostics);
281
301
  case "object":
282
302
  return startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes);
283
303
  default:
@@ -485,6 +505,45 @@ function startEventSection(tokens, line, events, eventPoseNodes, eventIds, sourc
485
505
  activePoseSeenFields: new Set(),
486
506
  };
487
507
  }
508
+ function startTrajectorySection(tokens, line, trajectories, trajectoryIds, sourceSchemaVersion, diagnostics) {
509
+ if (tokens.length !== 2) {
510
+ throw new WorldOrbitError("Invalid trajectory declaration", line, tokens[0]?.column ?? 1);
511
+ }
512
+ const id = normalizeIdentifier(tokens[1].value);
513
+ if (!id) {
514
+ throw new WorldOrbitError("Trajectory id must not be empty", line, tokens[1].column);
515
+ }
516
+ if (trajectoryIds.has(id)) {
517
+ throw new WorldOrbitError(`Duplicate trajectory id "${id}"`, line, tokens[1].column);
518
+ }
519
+ const trajectory = {
520
+ id,
521
+ label: humanizeIdentifier(id),
522
+ summary: null,
523
+ craftObjectId: null,
524
+ tags: [],
525
+ color: null,
526
+ hidden: false,
527
+ segments: [],
528
+ };
529
+ trajectories.push(trajectory);
530
+ trajectoryIds.add(id);
531
+ return {
532
+ kind: "trajectory",
533
+ trajectory,
534
+ sourceSchemaVersion,
535
+ diagnostics,
536
+ seenFields: new Set(),
537
+ inSegment: false,
538
+ segmentIndent: null,
539
+ activeSegment: null,
540
+ activeSegmentSeenFields: new Set(),
541
+ inManeuver: false,
542
+ maneuverIndent: null,
543
+ activeManeuver: null,
544
+ activeManeuverSeenFields: new Set(),
545
+ };
546
+ }
488
547
  function startObjectSection(tokens, line, sourceSchemaVersion, diagnostics, objectNodes) {
489
548
  if (tokens.length < 3) {
490
549
  throw new WorldOrbitError("Invalid atlas object declaration", line, tokens[0]?.column ?? 1);
@@ -544,6 +603,9 @@ function handleSectionLine(section, indent, tokens, line) {
544
603
  case "event":
545
604
  applyEventField(section, indent, tokens, line);
546
605
  return;
606
+ case "trajectory":
607
+ applyTrajectoryField(section, indent, tokens, line);
608
+ return;
547
609
  case "object":
548
610
  applyObjectField(section, indent, tokens, line);
549
611
  return;
@@ -863,6 +925,13 @@ function applyEventField(section, indent, tokens, line) {
863
925
  column: tokens[0]?.column ?? 1,
864
926
  });
865
927
  }
928
+ if (tokens[0]?.value === "segment" ||
929
+ tokens[0]?.value === "maneuver") {
930
+ warnIfSchema30Feature(section.sourceSchemaVersion, section.diagnostics, `pose.${tokens[0].value}`, {
931
+ line,
932
+ column: tokens[0]?.column ?? 1,
933
+ });
934
+ }
866
935
  section.activePose.fields.push(parseEventPoseField(tokens, line, section.activePoseSeenFields));
867
936
  return;
868
937
  }
@@ -905,6 +974,13 @@ function applyEventField(section, indent, tokens, line) {
905
974
  case "summary":
906
975
  section.event.summary = joinFieldValue(tokens, line);
907
976
  return;
977
+ case "trajectory":
978
+ warnIfSchema30Feature(section.sourceSchemaVersion, section.diagnostics, "event.trajectory", {
979
+ line,
980
+ column: tokens[0].column,
981
+ });
982
+ section.event.trajectoryId = joinFieldValue(tokens, line);
983
+ return;
908
984
  case "target":
909
985
  section.event.targetObjectId = joinFieldValue(tokens, line);
910
986
  return;
@@ -947,6 +1023,219 @@ function applyEventField(section, indent, tokens, line) {
947
1023
  throw new WorldOrbitError(`Unknown event field "${tokens[0].value}"`, line, tokens[0].column);
948
1024
  }
949
1025
  }
1026
+ function applyTrajectoryField(section, indent, tokens, line) {
1027
+ if (section.activeManeuver && indent <= (section.maneuverIndent ?? 0)) {
1028
+ section.activeManeuver = null;
1029
+ section.maneuverIndent = null;
1030
+ section.activeManeuverSeenFields.clear();
1031
+ section.inManeuver = false;
1032
+ }
1033
+ if (section.activeSegment && indent <= (section.segmentIndent ?? 0)) {
1034
+ section.activeSegment = null;
1035
+ section.segmentIndent = null;
1036
+ section.activeSegmentSeenFields.clear();
1037
+ section.inSegment = false;
1038
+ }
1039
+ if (section.activeManeuver) {
1040
+ applyTrajectoryManeuverField(section, tokens, line);
1041
+ return;
1042
+ }
1043
+ if (section.activeSegment) {
1044
+ if (tokens[0]?.value.toLowerCase() === "maneuver") {
1045
+ if (tokens.length !== 2) {
1046
+ throw new WorldOrbitError("Invalid trajectory maneuver declaration", line, tokens[0]?.column ?? 1);
1047
+ }
1048
+ const id = normalizeIdentifier(tokens[1].value);
1049
+ if (!id) {
1050
+ throw new WorldOrbitError("Trajectory maneuver id must not be empty", line, tokens[1].column);
1051
+ }
1052
+ if (section.activeSegment.maneuvers.some((maneuver) => maneuver.id === id)) {
1053
+ throw new WorldOrbitError(`Duplicate trajectory maneuver id "${id}"`, line, tokens[1].column);
1054
+ }
1055
+ const maneuver = {
1056
+ id,
1057
+ kind: "burn",
1058
+ label: null,
1059
+ epoch: null,
1060
+ notes: [],
1061
+ };
1062
+ section.activeSegment.maneuvers.push(maneuver);
1063
+ section.activeManeuver = maneuver;
1064
+ section.inManeuver = true;
1065
+ section.maneuverIndent = indent;
1066
+ section.activeManeuverSeenFields = new Set();
1067
+ return;
1068
+ }
1069
+ applyTrajectorySegmentField(section, tokens, line);
1070
+ return;
1071
+ }
1072
+ if (tokens[0]?.value.toLowerCase() === "segment") {
1073
+ if (tokens.length !== 2) {
1074
+ throw new WorldOrbitError("Invalid trajectory segment declaration", line, tokens[0]?.column ?? 1);
1075
+ }
1076
+ const id = normalizeIdentifier(tokens[1].value);
1077
+ if (!id) {
1078
+ throw new WorldOrbitError("Trajectory segment id must not be empty", line, tokens[1].column);
1079
+ }
1080
+ if (section.trajectory.segments.some((segment) => segment.id === id)) {
1081
+ throw new WorldOrbitError(`Duplicate trajectory segment id "${id}"`, line, tokens[1].column);
1082
+ }
1083
+ const segment = {
1084
+ id,
1085
+ kind: "transfer",
1086
+ label: null,
1087
+ summary: null,
1088
+ fromObjectId: null,
1089
+ toObjectId: null,
1090
+ aroundObjectId: null,
1091
+ assist: null,
1092
+ epoch: null,
1093
+ notes: [],
1094
+ maneuvers: [],
1095
+ };
1096
+ section.trajectory.segments.push(segment);
1097
+ section.activeSegment = {
1098
+ id,
1099
+ fields: [],
1100
+ maneuvers: segment.maneuvers,
1101
+ assist: null,
1102
+ location: { line, column: tokens[0].column },
1103
+ };
1104
+ section.inSegment = true;
1105
+ section.segmentIndent = indent;
1106
+ section.activeSegmentSeenFields = new Set();
1107
+ return;
1108
+ }
1109
+ const key = requireUniqueField(tokens, section.seenFields, line);
1110
+ const value = joinFieldValue(tokens, line);
1111
+ switch (key) {
1112
+ case "label":
1113
+ section.trajectory.label = value;
1114
+ return;
1115
+ case "summary":
1116
+ section.trajectory.summary = value;
1117
+ return;
1118
+ case "craft":
1119
+ section.trajectory.craftObjectId = value;
1120
+ return;
1121
+ case "tags":
1122
+ section.trajectory.tags = parseTokenList(tokens.slice(1), line, "tags");
1123
+ return;
1124
+ case "color":
1125
+ section.trajectory.color = value;
1126
+ return;
1127
+ case "hidden":
1128
+ section.trajectory.hidden = parseAtlasBoolean(value, "hidden", {
1129
+ line,
1130
+ column: tokens[0].column,
1131
+ });
1132
+ return;
1133
+ default:
1134
+ throw new WorldOrbitError(`Unknown trajectory field "${tokens[0].value}"`, line, tokens[0].column);
1135
+ }
1136
+ }
1137
+ function applyTrajectorySegmentField(section, tokens, line) {
1138
+ const segment = section.activeSegment;
1139
+ if (!segment) {
1140
+ return;
1141
+ }
1142
+ const key = requireUniqueField(tokens, section.activeSegmentSeenFields, line);
1143
+ const value = joinFieldValue(tokens, line);
1144
+ const target = section.trajectory.segments.find((entry) => entry.id === segment.id);
1145
+ switch (key) {
1146
+ case "kind": {
1147
+ const normalized = value.toLowerCase();
1148
+ if (!TRAJECTORY_SEGMENT_KINDS.has(normalized)) {
1149
+ throw new WorldOrbitError(`Unknown trajectory segment kind "${value}"`, line, tokens[0].column);
1150
+ }
1151
+ target.kind = normalized;
1152
+ return;
1153
+ }
1154
+ case "label":
1155
+ target.label = value;
1156
+ return;
1157
+ case "summary":
1158
+ target.summary = value;
1159
+ return;
1160
+ case "from":
1161
+ target.fromObjectId = value;
1162
+ return;
1163
+ case "to":
1164
+ target.toObjectId = value;
1165
+ return;
1166
+ case "around":
1167
+ target.aroundObjectId = value;
1168
+ return;
1169
+ case "assist":
1170
+ target.assist = {
1171
+ objectId: value,
1172
+ notes: [],
1173
+ };
1174
+ return;
1175
+ case "epoch":
1176
+ target.epoch = value;
1177
+ return;
1178
+ case "periapsis":
1179
+ target.periapsis = parseAtlasUnitValue(value, { line, column: tokens[0].column }, "periapsis");
1180
+ return;
1181
+ case "apoapsis":
1182
+ target.apoapsis = parseAtlasUnitValue(value, { line, column: tokens[0].column }, "apoapsis");
1183
+ return;
1184
+ case "inclination":
1185
+ target.inclination = parseAtlasUnitValue(value, { line, column: tokens[0].column }, "inclination");
1186
+ return;
1187
+ case "duration":
1188
+ target.duration = parseAtlasUnitValue(value, { line, column: tokens[0].column }, "duration");
1189
+ return;
1190
+ case "deltav":
1191
+ target.deltaV = parseAtlasUnitValue(value, { line, column: tokens[0].column });
1192
+ return;
1193
+ case "phaseangle":
1194
+ target.phaseAngle = parseAtlasUnitValue(value, { line, column: tokens[0].column }, "phaseAngle");
1195
+ return;
1196
+ case "turnangle":
1197
+ target.turnAngle = parseAtlasUnitValue(value, { line, column: tokens[0].column }, "turnAngle");
1198
+ return;
1199
+ case "energy":
1200
+ target.energy = parseAtlasUnitValue(value, { line, column: tokens[0].column });
1201
+ return;
1202
+ case "notes":
1203
+ target.notes = parseTokenList(tokens.slice(1), line, "notes");
1204
+ return;
1205
+ default:
1206
+ throw new WorldOrbitError(`Unknown trajectory segment field "${tokens[0].value}"`, line, tokens[0].column);
1207
+ }
1208
+ }
1209
+ function applyTrajectoryManeuverField(section, tokens, line) {
1210
+ const maneuver = section.activeManeuver;
1211
+ if (!maneuver) {
1212
+ return;
1213
+ }
1214
+ const key = requireUniqueField(tokens, section.activeManeuverSeenFields, line);
1215
+ const value = joinFieldValue(tokens, line);
1216
+ switch (key) {
1217
+ case "kind":
1218
+ maneuver.kind = value;
1219
+ return;
1220
+ case "label":
1221
+ maneuver.label = value;
1222
+ return;
1223
+ case "epoch":
1224
+ maneuver.epoch = value;
1225
+ return;
1226
+ case "deltav":
1227
+ maneuver.deltaV = parseAtlasUnitValue(value, { line, column: tokens[0].column });
1228
+ return;
1229
+ case "duration":
1230
+ maneuver.duration = parseAtlasUnitValue(value, { line, column: tokens[0].column }, "duration");
1231
+ return;
1232
+ case "notes":
1233
+ maneuver.notes = parseTokenList(tokens.slice(1), line, "notes");
1234
+ return;
1235
+ default:
1236
+ throw new WorldOrbitError(`Unknown trajectory maneuver field "${tokens[0].value}"`, line, tokens[0].column);
1237
+ }
1238
+ }
950
1239
  function parseEventPoseField(tokens, line, seenFields) {
951
1240
  if (tokens.length < 2) {
952
1241
  throw new WorldOrbitError("Invalid event pose field line", line, tokens[0]?.column ?? 1);
@@ -1181,6 +1470,12 @@ function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion,
1181
1470
  column: keyToken.column,
1182
1471
  });
1183
1472
  }
1473
+ else if (spec.version === "3.0") {
1474
+ warnIfSchema30Feature(sourceSchemaVersion, diagnostics, keyToken.value, {
1475
+ line,
1476
+ column: keyToken.column,
1477
+ });
1478
+ }
1184
1479
  index++;
1185
1480
  const valueTokens = [];
1186
1481
  if (spec.inlineMode === "single") {
@@ -1233,6 +1528,12 @@ function parseObjectField(tokens, line, objectType, sourceSchemaVersion, diagnos
1233
1528
  column: tokens[0].column,
1234
1529
  });
1235
1530
  }
1531
+ else if (spec.version === "3.0") {
1532
+ warnIfSchema30Feature(sourceSchemaVersion, diagnostics, tokens[0].value, {
1533
+ line,
1534
+ column: tokens[0].column,
1535
+ });
1536
+ }
1236
1537
  const field = {
1237
1538
  type: "field",
1238
1539
  key: tokens[0].value,
@@ -1277,6 +1578,7 @@ function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
1277
1578
  const tolerances = fieldMap.get("tolerance")?.map((field) => parseToleranceField(field));
1278
1579
  const typedBlocks = normalizeTypedBlocks(node.typedBlockEntries);
1279
1580
  const info = normalizeInfoEntries(node.infoEntries, "info");
1581
+ const trajectoryId = parseOptionalJoinedValue(fieldMap.get("trajectory")?.[0]);
1280
1582
  const object = {
1281
1583
  type: node.objectType,
1282
1584
  id: node.id,
@@ -1306,6 +1608,8 @@ function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
1306
1608
  object.tolerances = tolerances;
1307
1609
  if (typedBlocks && Object.keys(typedBlocks).length > 0)
1308
1610
  object.typedBlocks = typedBlocks;
1611
+ if (trajectoryId)
1612
+ object.trajectoryId = trajectoryId;
1309
1613
  if (isSchemaOlderThan(sourceSchemaVersion, "2.1")) {
1310
1614
  if (object.groups ||
1311
1615
  object.epoch ||
@@ -1317,10 +1621,14 @@ function normalizeDraftObject(node, sourceSchemaVersion, diagnostics) {
1317
1621
  object.validationRules?.length ||
1318
1622
  object.lockedFields?.length ||
1319
1623
  object.tolerances?.length ||
1320
- object.typedBlocks) {
1624
+ object.typedBlocks ||
1625
+ object.trajectoryId) {
1321
1626
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, node.id, node.location);
1322
1627
  }
1323
1628
  }
1629
+ if (object.trajectoryId) {
1630
+ warnIfSchema30Feature(sourceSchemaVersion, diagnostics, `${node.id}.trajectory`, node.location);
1631
+ }
1324
1632
  return object;
1325
1633
  }
1326
1634
  function normalizeDraftEvent(event, rawPoses) {
@@ -1337,6 +1645,8 @@ function normalizeDraftEventPose(rawPose) {
1337
1645
  return {
1338
1646
  objectId: rawPose.objectId,
1339
1647
  placement,
1648
+ trajectorySegmentId: parseOptionalJoinedValue(fieldMap.get("segment")?.[0]),
1649
+ trajectoryManeuverId: parseOptionalJoinedValue(fieldMap.get("maneuver")?.[0]),
1340
1650
  inner: parseOptionalUnitField(fieldMap.get("inner")?.[0], "inner"),
1341
1651
  outer: parseOptionalUnitField(fieldMap.get("outer")?.[0], "outer"),
1342
1652
  epoch: parseOptionalJoinedValue(fieldMap.get("epoch")?.[0]),
@@ -1553,6 +1863,19 @@ function warnIfSchema25Feature(sourceSchemaVersion, diagnostics, featureName, lo
1553
1863
  column: location.column,
1554
1864
  });
1555
1865
  }
1866
+ function warnIfSchema30Feature(sourceSchemaVersion, diagnostics, featureName, location) {
1867
+ if (!isSchemaOlderThan(sourceSchemaVersion, "3.0")) {
1868
+ return;
1869
+ }
1870
+ diagnostics.push({
1871
+ code: "parse.schema30.featureCompatibility",
1872
+ severity: "warning",
1873
+ source: "parse",
1874
+ message: `Feature "${featureName}" requires schema 3.0; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
1875
+ line: location.line,
1876
+ column: location.column,
1877
+ });
1878
+ }
1556
1879
  function isSchemaOlderThan(sourceSchemaVersion, requiredVersion) {
1557
1880
  return schemaVersionRank(sourceSchemaVersion) < schemaVersionRank(requiredVersion);
1558
1881
  }
@@ -1568,8 +1891,10 @@ function schemaVersionRank(version) {
1568
1891
  return 3;
1569
1892
  case "2.6":
1570
1893
  return 4;
1571
- default:
1894
+ case "3.0":
1572
1895
  return 5;
1896
+ default:
1897
+ return 6;
1573
1898
  }
1574
1899
  }
1575
1900
  function preprocessAtlasSource(source) {
@@ -1,4 +1,4 @@
1
- import type { SceneRenderOptions, WorldOrbitAtlasDocument, WorldOrbitEvent, WorldOrbitAtlasSystem, WorldOrbitDiagnostic, WorldOrbitDocument, WorldOrbitObject } from "./types.js";
1
+ import type { SceneRenderOptions, WorldOrbitAtlasDocument, WorldOrbitEvent, WorldOrbitAtlasSystem, WorldOrbitDiagnostic, WorldOrbitDocument, WorldOrbitObject, WorldOrbitTrajectory } 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;
@@ -12,6 +12,7 @@ export declare function upgradeDocumentToDraftV2(document: WorldOrbitDocument, o
12
12
  groups: import("./types.js").WorldOrbitGroup[];
13
13
  relations: import("./types.js").WorldOrbitRelation[];
14
14
  events: WorldOrbitEvent[];
15
+ trajectories: WorldOrbitTrajectory[];
15
16
  objects: WorldOrbitObject[];
16
17
  diagnostics: WorldOrbitDiagnostic[];
17
18
  };
@@ -18,15 +18,16 @@ export function upgradeDocumentToV2(document, options = {}) {
18
18
  }
19
19
  return {
20
20
  format: "worldorbit",
21
- version: "2.6",
22
- schemaVersion: "2.6",
21
+ version: "3.0",
22
+ schemaVersion: "3.0",
23
23
  sourceVersion: document.version,
24
24
  theme: document.theme ?? null,
25
25
  system,
26
26
  groups: structuredClone(document.groups ?? []),
27
27
  relations: structuredClone(document.relations ?? []),
28
28
  events: structuredClone(document.events ?? []),
29
- objects: document.objects.map(cloneWorldOrbitObject),
29
+ trajectories: [],
30
+ objects: document.objects.map(cloneWorldOrbitObject).map(normalizeLegacyCraftObject),
30
31
  diagnostics,
31
32
  };
32
33
  }
@@ -57,6 +58,7 @@ export function materializeAtlasDocument(document, options = {}) {
57
58
  groups: structuredClone(document.groups ?? []),
58
59
  relations: structuredClone(document.relations ?? []),
59
60
  events: document.events.map(cloneWorldOrbitEvent),
61
+ trajectories: document.trajectories.map(cloneWorldOrbitTrajectory),
60
62
  objects,
61
63
  };
62
64
  }
@@ -226,6 +228,7 @@ function mapSceneViewpointToDraftViewpoint(viewpoint) {
226
228
  function cloneWorldOrbitObject(object) {
227
229
  return {
228
230
  ...object,
231
+ trajectoryId: object.trajectoryId ?? null,
229
232
  groups: object.groups ? [...object.groups] : undefined,
230
233
  resonance: object.resonance ? { ...object.resonance } : object.resonance,
231
234
  renderHints: object.renderHints ? { ...object.renderHints } : object.renderHints,
@@ -255,6 +258,7 @@ function cloneWorldOrbitObject(object) {
255
258
  function cloneWorldOrbitEvent(event) {
256
259
  return {
257
260
  ...event,
261
+ trajectoryId: event.trajectoryId ?? null,
258
262
  participantObjectIds: [...event.participantObjectIds],
259
263
  tags: [...event.tags],
260
264
  positions: event.positions.map(cloneWorldOrbitEventPose),
@@ -264,12 +268,30 @@ function cloneWorldOrbitEventPose(pose) {
264
268
  return {
265
269
  objectId: pose.objectId,
266
270
  placement: clonePlacement(pose.placement),
271
+ trajectorySegmentId: pose.trajectorySegmentId ?? null,
272
+ trajectoryManeuverId: pose.trajectoryManeuverId ?? null,
267
273
  inner: pose.inner ? { ...pose.inner } : undefined,
268
274
  outer: pose.outer ? { ...pose.outer } : undefined,
269
275
  epoch: pose.epoch ?? null,
270
276
  referencePlane: pose.referencePlane ?? null,
271
277
  };
272
278
  }
279
+ function cloneWorldOrbitTrajectory(trajectory) {
280
+ return structuredClone(trajectory);
281
+ }
282
+ function normalizeLegacyCraftObject(object) {
283
+ if (object.type !== "structure") {
284
+ return object;
285
+ }
286
+ const kind = typeof object.properties.kind === "string" ? object.properties.kind.toLowerCase() : "";
287
+ if (!["ship", "probe", "station"].includes(kind)) {
288
+ return object;
289
+ }
290
+ return {
291
+ ...object,
292
+ type: "craft",
293
+ };
294
+ }
273
295
  function clonePlacement(placement) {
274
296
  return placement ? structuredClone(placement) : null;
275
297
  }