worldorbit 4.0.0 → 5.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 (48) hide show
  1. package/README.md +17 -18
  2. package/dist/browser/core/dist/draft-parse.js +131 -12
  3. package/dist/browser/core/dist/draft.js +4 -4
  4. package/dist/browser/core/dist/format.js +42 -3
  5. package/dist/browser/core/dist/load.js +7 -2
  6. package/dist/browser/core/dist/scene.js +215 -3
  7. package/dist/browser/core/dist/solver.d.ts +8 -1
  8. package/dist/browser/core/dist/solver.js +72 -0
  9. package/dist/browser/core/dist/spatial-scene.js +56 -0
  10. package/dist/browser/core/dist/types.d.ts +74 -2
  11. package/dist/browser/viewer/dist/render.js +71 -15
  12. package/dist/browser/viewer/dist/theme.js +1 -0
  13. package/dist/browser/viewer/dist/types.d.ts +7 -0
  14. package/dist/browser/viewer/dist/viewer.js +27 -0
  15. package/dist/obsidian-plugin/main.js +81 -67
  16. package/dist/unpkg/core/dist/draft-parse.js +131 -12
  17. package/dist/unpkg/core/dist/draft.js +4 -4
  18. package/dist/unpkg/core/dist/format.js +42 -3
  19. package/dist/unpkg/core/dist/load.js +7 -2
  20. package/dist/unpkg/core/dist/scene.js +215 -3
  21. package/dist/unpkg/core/dist/solver.d.ts +8 -1
  22. package/dist/unpkg/core/dist/solver.js +72 -0
  23. package/dist/unpkg/core/dist/spatial-scene.js +56 -0
  24. package/dist/unpkg/core/dist/types.d.ts +74 -2
  25. package/dist/unpkg/viewer/dist/render.js +71 -15
  26. package/dist/unpkg/viewer/dist/theme.js +1 -0
  27. package/dist/unpkg/viewer/dist/types.d.ts +7 -0
  28. package/dist/unpkg/viewer/dist/viewer.js +27 -0
  29. package/dist/unpkg/worldorbit-core.min.js +12 -12
  30. package/dist/unpkg/worldorbit-editor.min.js +336 -322
  31. package/dist/unpkg/worldorbit-markdown.min.js +50 -36
  32. package/dist/unpkg/worldorbit-viewer.min.js +225 -211
  33. package/dist/unpkg/worldorbit.js +473 -19
  34. package/dist/unpkg/worldorbit.min.js +229 -215
  35. package/package.json +1 -1
  36. package/packages/core/dist/draft-parse.js +131 -12
  37. package/packages/core/dist/draft.js +4 -4
  38. package/packages/core/dist/format.js +42 -3
  39. package/packages/core/dist/load.js +7 -2
  40. package/packages/core/dist/scene.js +215 -3
  41. package/packages/core/dist/solver.d.ts +8 -1
  42. package/packages/core/dist/solver.js +72 -0
  43. package/packages/core/dist/spatial-scene.js +56 -0
  44. package/packages/core/dist/types.d.ts +74 -2
  45. package/packages/viewer/dist/render.js +71 -15
  46. package/packages/viewer/dist/theme.js +1 -0
  47. package/packages/viewer/dist/types.d.ts +7 -0
  48. package/packages/viewer/dist/viewer.js +27 -0
@@ -138,7 +138,8 @@ export function renderDocumentToScene(document, options = {}) {
138
138
  const labels = createSceneLabels(objects, width, height, scaleModel.labelMultiplier);
139
139
  const relations = createSceneRelations(document, objects);
140
140
  const events = createSceneEvents(document.events ?? [], objects, activeEventId);
141
- const layers = createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels);
141
+ const trajectories = createSceneTrajectories(document, objects, events, options);
142
+ const layers = createSceneLayers(orbitVisuals, trajectories, relations, events, leaders, objects, labels);
142
143
  const groups = createSceneGroups(objects, orbitVisuals, leaders, labels, relationships, scaleModel.labelMultiplier);
143
144
  const semanticGroups = createSceneSemanticGroups(document, objects);
144
145
  const viewpoints = createSceneViewpoints(document, schemaProjection, frame.preset, relationships, objectMap);
@@ -179,6 +180,7 @@ export function renderDocumentToScene(document, options = {}) {
179
180
  viewpoints,
180
181
  events,
181
182
  activeEventId,
183
+ trajectories,
182
184
  objects,
183
185
  orbitVisuals,
184
186
  relations,
@@ -662,7 +664,7 @@ function createLabelPlacement(object, direction, attempt, labelMultiplier) {
662
664
  }
663
665
  }
664
666
  }
665
- function createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels) {
667
+ function createSceneLayers(orbitVisuals, trajectories, relations, events, leaders, objects, labels) {
666
668
  const backOrbitIds = orbitVisuals
667
669
  .filter((visual) => !visual.hidden && Boolean(visual.backArcPath))
668
670
  .map((visual) => visual.renderId);
@@ -677,6 +679,15 @@ function createSceneLayers(orbitVisuals, relations, events, leaders, objects, la
677
679
  },
678
680
  { id: "orbits-back", renderIds: backOrbitIds },
679
681
  { id: "orbits-front", renderIds: frontOrbitIds },
682
+ {
683
+ id: "trajectories",
684
+ renderIds: trajectories
685
+ .filter((trajectory) => !trajectory.hidden)
686
+ .flatMap((trajectory) => [
687
+ trajectory.renderId,
688
+ ...trajectory.waypoints.filter((waypoint) => !waypoint.hidden).map((waypoint) => waypoint.renderId),
689
+ ]),
690
+ },
680
691
  {
681
692
  id: "relations",
682
693
  renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId),
@@ -819,6 +830,206 @@ function createSceneEvents(events, objects, activeEventId) {
819
830
  })
820
831
  .sort((left, right) => left.event.id.localeCompare(right.event.id));
821
832
  }
833
+ function createSceneTrajectories(document, objects, events, options) {
834
+ const objectMap = new Map(objects.map((object) => [object.objectId, object]));
835
+ return document.trajectories
836
+ .map((trajectory) => createSceneTrajectory(trajectory, objectMap, events, options))
837
+ .sort((left, right) => left.trajectoryId.localeCompare(right.trajectoryId));
838
+ }
839
+ function createSceneTrajectory(trajectory, objectMap, events, options) {
840
+ const craftObject = trajectory.craftObjectId ? objectMap.get(trajectory.craftObjectId) ?? null : null;
841
+ const mode = resolveTrajectoryMode(trajectory, options);
842
+ const stroke = trajectory.stroke ?? trajectory.color ?? craftObject?.fillColor ?? null;
843
+ const strokeWidth = trajectory.strokeWidth ?? 2.4;
844
+ const showWaypoints = options.showTrajectoryWaypoints ?? trajectory.showWaypoints ?? true;
845
+ const labelMode = trajectory.labelMode ?? (options.showTrajectoryLabels === false ? "hidden" : "waypoint");
846
+ const pathParts = [];
847
+ const waypoints = [];
848
+ const objectIds = new Set();
849
+ let lastAnchor = craftObject;
850
+ if (craftObject) {
851
+ objectIds.add(craftObject.objectId);
852
+ }
853
+ trajectory.segments.forEach((segment, index) => {
854
+ const geometry = buildTrajectorySegmentGeometry(trajectory, segment, index, mode, objectMap, lastAnchor, showWaypoints);
855
+ if (geometry.path) {
856
+ pathParts.push(geometry.path);
857
+ }
858
+ geometry.objectIds.forEach((objectId) => objectIds.add(objectId));
859
+ waypoints.push(...geometry.waypoints);
860
+ lastAnchor = geometry.lastAnchor ?? lastAnchor;
861
+ });
862
+ for (const event of events.filter((entry) => entry.event.trajectoryId === trajectory.id)) {
863
+ const eventObject = event.targetObjectId
864
+ ? objectMap.get(event.targetObjectId) ?? null
865
+ : event.objectIds.map((objectId) => objectMap.get(objectId) ?? null).find(Boolean) ?? null;
866
+ if (!eventObject) {
867
+ continue;
868
+ }
869
+ waypoints.push({
870
+ renderId: `${createRenderId(`${trajectory.id}-${event.eventId}`)}-waypoint`,
871
+ trajectoryId: trajectory.id,
872
+ segmentId: null,
873
+ maneuverId: null,
874
+ objectId: eventObject.objectId,
875
+ x: eventObject.x,
876
+ y: eventObject.y,
877
+ label: event.event.label ?? event.event.id,
878
+ dateLabel: event.event.epoch ?? null,
879
+ hidden: trajectory.hidden || event.hidden || !showWaypoints,
880
+ });
881
+ objectIds.add(eventObject.objectId);
882
+ }
883
+ return {
884
+ renderId: `${createRenderId(trajectory.id)}-trajectory`,
885
+ trajectoryId: trajectory.id,
886
+ trajectory,
887
+ craftObjectId: trajectory.craftObjectId,
888
+ mode,
889
+ path: pathParts.join(" "),
890
+ stroke,
891
+ strokeWidth,
892
+ marker: trajectory.marker ?? "arrow",
893
+ labelMode,
894
+ showWaypoints,
895
+ objectIds: [...objectIds],
896
+ waypoints,
897
+ hidden: trajectory.hidden || pathParts.length === 0,
898
+ };
899
+ }
900
+ function buildTrajectorySegmentGeometry(trajectory, segment, segmentIndex, mode, objectMap, fallbackStart, showWaypoints) {
901
+ const start = resolveTrajectoryObject(segment.fromObjectId, objectMap) ??
902
+ fallbackStart;
903
+ const assist = resolveTrajectoryObject(segment.assist?.objectId ?? segment.aroundObjectId ?? null, objectMap);
904
+ const end = resolveTrajectoryObject(segment.toObjectId, objectMap) ??
905
+ resolveTrajectoryObject(segment.aroundObjectId, objectMap) ??
906
+ assist ??
907
+ start;
908
+ if (!start || !end) {
909
+ return {
910
+ path: "",
911
+ objectIds: [],
912
+ waypoints: [],
913
+ lastAnchor: fallbackStart,
914
+ };
915
+ }
916
+ const objectIds = [start.objectId, end.objectId];
917
+ if (assist) {
918
+ objectIds.push(assist.objectId);
919
+ }
920
+ const hidden = trajectory.hidden || segment.renderHidden === true || start.hidden || end.hidden;
921
+ const waypoints = [];
922
+ const label = segment.waypointLabel ?? segment.label ?? humanizeIdentifier(segment.id);
923
+ const dateLabel = segment.waypointDate ?? segment.epoch ?? null;
924
+ if (showWaypoints) {
925
+ if (segmentIndex === 0) {
926
+ waypoints.push(createTrajectoryWaypoint(trajectory.id, segment.id, null, start, start.label, null, hidden));
927
+ }
928
+ if (assist && assist.objectId !== start.objectId && assist.objectId !== end.objectId) {
929
+ waypoints.push(createTrajectoryWaypoint(trajectory.id, segment.id, null, assist, label, dateLabel, hidden));
930
+ }
931
+ waypoints.push(createTrajectoryWaypoint(trajectory.id, segment.id, null, end, label, dateLabel, hidden));
932
+ }
933
+ const samples = mode === "solver"
934
+ ? sampleQuadraticPoints(start, resolveTrajectoryControlPoint(start, end, assist, segmentIndex), end, Math.max(10, Math.round((segment.sampleDensity ?? 1) * 14)))
935
+ : null;
936
+ const control = resolveTrajectoryControlPoint(start, end, assist, segmentIndex);
937
+ const path = mode === "solver"
938
+ ? pointsToPath(samples ?? [start, end])
939
+ : `M ${formatPathNumber(start.x)} ${formatPathNumber(start.y)} Q ${formatPathNumber(control.x)} ${formatPathNumber(control.y)} ${formatPathNumber(end.x)} ${formatPathNumber(end.y)}`;
940
+ segment.maneuvers.forEach((maneuver, maneuverIndex) => {
941
+ if (!showWaypoints) {
942
+ return;
943
+ }
944
+ const point = samplePointOnQuadratic(start, control, end, (maneuverIndex + 1) / (segment.maneuvers.length + 1));
945
+ waypoints.push({
946
+ renderId: `${createRenderId(`${trajectory.id}-${segment.id}-${maneuver.id}`)}-waypoint`,
947
+ trajectoryId: trajectory.id,
948
+ segmentId: segment.id,
949
+ maneuverId: maneuver.id,
950
+ objectId: null,
951
+ x: point.x,
952
+ y: point.y,
953
+ label: maneuver.label ?? maneuver.kind,
954
+ dateLabel: maneuver.epoch ?? null,
955
+ hidden,
956
+ });
957
+ });
958
+ return {
959
+ path,
960
+ objectIds: [...new Set(objectIds)],
961
+ waypoints,
962
+ lastAnchor: end,
963
+ };
964
+ }
965
+ function createTrajectoryWaypoint(trajectoryId, segmentId, maneuverId, object, label, dateLabel, hidden) {
966
+ return {
967
+ renderId: `${createRenderId(`${trajectoryId}-${segmentId ?? object.objectId}-${maneuverId ?? object.objectId}`)}-waypoint`,
968
+ trajectoryId,
969
+ segmentId,
970
+ maneuverId,
971
+ objectId: object.objectId,
972
+ x: object.x,
973
+ y: object.y,
974
+ label,
975
+ dateLabel,
976
+ hidden,
977
+ };
978
+ }
979
+ function resolveTrajectoryMode(trajectory, options) {
980
+ const requested = options.trajectoryMode ?? trajectory.renderMode ?? "auto";
981
+ if (requested !== "auto") {
982
+ return requested;
983
+ }
984
+ const segmentCount = trajectory.segments.filter((segment) => segment.fromObjectId || segment.toObjectId || segment.assist?.objectId || segment.aroundObjectId).length;
985
+ return segmentCount > 0 ? "solver" : "illustrative";
986
+ }
987
+ function resolveTrajectoryObject(objectId, objectMap) {
988
+ return objectId ? objectMap.get(objectId) ?? null : null;
989
+ }
990
+ function resolveTrajectoryControlPoint(start, end, assist, segmentIndex) {
991
+ if (assist) {
992
+ return {
993
+ x: assist.x,
994
+ y: assist.y,
995
+ };
996
+ }
997
+ const dx = end.x - start.x;
998
+ const dy = end.y - start.y;
999
+ const length = Math.max(Math.hypot(dx, dy), 1);
1000
+ const offset = Math.min(Math.max(length * 0.18, 26), 120) * (segmentIndex % 2 === 0 ? 1 : -1);
1001
+ const normalX = -dy / length;
1002
+ const normalY = dx / length;
1003
+ return {
1004
+ x: (start.x + end.x) / 2 + normalX * offset,
1005
+ y: (start.y + end.y) / 2 + normalY * offset,
1006
+ };
1007
+ }
1008
+ function sampleQuadraticPoints(start, control, end, count) {
1009
+ const samples = [];
1010
+ for (let index = 0; index <= count; index += 1) {
1011
+ samples.push(samplePointOnQuadratic(start, control, end, index / count));
1012
+ }
1013
+ return samples;
1014
+ }
1015
+ function samplePointOnQuadratic(start, control, end, t) {
1016
+ const inverse = 1 - t;
1017
+ return {
1018
+ x: inverse * inverse * start.x + 2 * inverse * t * control.x + t * t * end.x,
1019
+ y: inverse * inverse * start.y + 2 * inverse * t * control.y + t * t * end.y,
1020
+ };
1021
+ }
1022
+ function pointsToPath(points) {
1023
+ if (points.length === 0) {
1024
+ return "";
1025
+ }
1026
+ return points
1027
+ .map((point, index) => `${index === 0 ? "M" : "L"} ${formatPathNumber(point.x)} ${formatPathNumber(point.y)}`)
1028
+ .join(" ");
1029
+ }
1030
+ function formatPathNumber(value) {
1031
+ return Number.isFinite(value) ? value.toFixed(2) : "0";
1032
+ }
822
1033
  function createSceneViewpoints(document, projection, preset, relationships, objectMap) {
823
1034
  const generatedOverview = createGeneratedOverviewViewpoint(document, projection, preset);
824
1035
  const drafts = new Map();
@@ -1099,7 +1310,8 @@ function parseViewpointLayers(value) {
1099
1310
  rawLayer === "events" ||
1100
1311
  rawLayer === "objects" ||
1101
1312
  rawLayer === "labels" ||
1102
- rawLayer === "metadata") {
1313
+ rawLayer === "metadata" ||
1314
+ rawLayer === "trajectories") {
1103
1315
  next[rawLayer] = enabled;
1104
1316
  }
1105
1317
  }
@@ -1,4 +1,4 @@
1
- import type { UnitValue, WorldOrbitTrajectory, WorldOrbitTrajectorySegment } from "./types.js";
1
+ import type { SceneRenderOptions, SpatialTrajectory, UnitValue, WorldOrbitDocument, WorldOrbitTrajectory, WorldOrbitTrajectorySegment } from "./types.js";
2
2
  export interface SolverSegmentSample {
3
3
  segmentId: string;
4
4
  kind: WorldOrbitTrajectorySegment["kind"];
@@ -23,4 +23,11 @@ export interface SolverTrajectorySnapshot {
23
23
  segments: SolverSegmentSample[];
24
24
  maneuvers: SolverManeuverSample[];
25
25
  }
26
+ export interface TrajectorySamplingOptions extends Pick<SceneRenderOptions, "width" | "height" | "padding" | "preset" | "projection" | "camera" | "scaleModel" | "bodyScaleMode"> {
27
+ trajectoryMode?: "illustrative" | "solver" | "auto";
28
+ showTrajectoryWaypoints?: boolean;
29
+ showTrajectoryLabels?: boolean;
30
+ }
26
31
  export declare function createTrajectorySolverSnapshot(trajectory: WorldOrbitTrajectory): SolverTrajectorySnapshot;
32
+ export declare function sampleTrajectory(trajectory: WorldOrbitTrajectory, document: WorldOrbitDocument, options?: TrajectorySamplingOptions): SpatialTrajectory | null;
33
+ export declare function sampleDocumentTrajectories(document: WorldOrbitDocument, options?: TrajectorySamplingOptions): SpatialTrajectory[];
@@ -1,3 +1,4 @@
1
+ import { renderDocumentToScene } from "./scene.js";
1
2
  export function createTrajectorySolverSnapshot(trajectory) {
2
3
  return {
3
4
  trajectoryId: trajectory.id,
@@ -15,6 +16,58 @@ export function createTrajectorySolverSnapshot(trajectory) {
15
16
  maneuvers: trajectory.segments.flatMap((segment) => segment.maneuvers.map((maneuver) => mapManeuver(segment.id, maneuver))),
16
17
  };
17
18
  }
19
+ export function sampleTrajectory(trajectory, document, options = {}) {
20
+ const scene = renderDocumentToScene(document, {
21
+ ...options,
22
+ trajectoryMode: options.trajectoryMode ?? trajectory.renderMode ?? "auto",
23
+ showTrajectoryWaypoints: options.showTrajectoryWaypoints ?? true,
24
+ showTrajectoryLabels: options.showTrajectoryLabels ?? true,
25
+ });
26
+ const rendered = scene.trajectories.find((entry) => entry.trajectoryId === trajectory.id);
27
+ return rendered ? mapRenderTrajectoryToSpatial(rendered) : null;
28
+ }
29
+ export function sampleDocumentTrajectories(document, options = {}) {
30
+ const scene = renderDocumentToScene(document, {
31
+ ...options,
32
+ trajectoryMode: options.trajectoryMode ?? "auto",
33
+ showTrajectoryWaypoints: options.showTrajectoryWaypoints ?? true,
34
+ showTrajectoryLabels: options.showTrajectoryLabels ?? true,
35
+ });
36
+ return scene.trajectories.map(mapRenderTrajectoryToSpatial);
37
+ }
38
+ function mapRenderTrajectoryToSpatial(trajectory) {
39
+ return {
40
+ trajectoryId: trajectory.trajectoryId,
41
+ trajectory: trajectory.trajectory,
42
+ craftObjectId: trajectory.craftObjectId,
43
+ mode: trajectory.mode,
44
+ stroke: trajectory.stroke,
45
+ strokeWidth: trajectory.strokeWidth,
46
+ marker: trajectory.marker,
47
+ labelMode: trajectory.labelMode,
48
+ showWaypoints: trajectory.showWaypoints,
49
+ samples: samplePathPoints(trajectory.path).map((point) => ({
50
+ x: point.x,
51
+ y: 0,
52
+ z: point.y,
53
+ })),
54
+ waypoints: trajectory.waypoints.map((waypoint) => ({
55
+ trajectoryId: waypoint.trajectoryId,
56
+ segmentId: waypoint.segmentId,
57
+ maneuverId: waypoint.maneuverId,
58
+ objectId: waypoint.objectId,
59
+ position: {
60
+ x: waypoint.x,
61
+ y: 0,
62
+ z: waypoint.y,
63
+ },
64
+ label: waypoint.label,
65
+ dateLabel: waypoint.dateLabel,
66
+ hidden: waypoint.hidden,
67
+ })),
68
+ hidden: trajectory.hidden,
69
+ };
70
+ }
18
71
  function mapManeuver(segmentId, maneuver) {
19
72
  return {
20
73
  segmentId,
@@ -25,3 +78,22 @@ function mapManeuver(segmentId, maneuver) {
25
78
  duration: maneuver.duration ?? null,
26
79
  };
27
80
  }
81
+ function samplePathPoints(path) {
82
+ const matches = [...path.matchAll(/[MLQ]\s*(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?)(?:\s+(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?))?(?:\s+(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?))?/g)];
83
+ if (matches.length === 0) {
84
+ return [];
85
+ }
86
+ const points = [];
87
+ for (const match of matches) {
88
+ const command = match[0][0];
89
+ if (command === "M" || command === "L") {
90
+ points.push({ x: Number(match[1]), y: Number(match[2]) });
91
+ continue;
92
+ }
93
+ if (command === "Q") {
94
+ points.push({ x: Number(match[1]), y: Number(match[2]) });
95
+ points.push({ x: Number(match[5]), y: Number(match[6]) });
96
+ }
97
+ }
98
+ return points;
99
+ }
@@ -36,6 +36,7 @@ export function renderDocumentToSpatialScene(document, options = {}) {
36
36
  const spatialObjects = scene.objects.map((entry) => createSpatialObject(entry, scene, sceneCenter, objectMap, orbitMap, scaleModel, positionCache, minimumMotionMetric));
37
37
  const spatialObjectMap = new Map(spatialObjects.map((object) => [object.objectId, object]));
38
38
  const spatialOrbits = scene.orbitVisuals.map((orbit) => createSpatialOrbit(orbit, spatialObjectMap, minimumMotionMetric, scene.activeEventId !== null));
39
+ const spatialTrajectories = scene.trajectories.map((trajectory) => createSpatialTrajectory(trajectory, spatialObjectMap));
39
40
  const focusTargets = spatialObjects.map((object) => ({
40
41
  objectId: object.objectId,
41
42
  center: { ...object.position },
@@ -65,6 +66,7 @@ export function renderDocumentToSpatialScene(document, options = {}) {
65
66
  timeFrozen: scene.activeEventId !== null,
66
67
  objects: spatialObjects,
67
68
  orbits: spatialOrbits,
69
+ trajectories: spatialTrajectories,
68
70
  focusTargets,
69
71
  };
70
72
  }
@@ -117,6 +119,41 @@ function createSpatialOrbit(orbit, objectMap, minimumMotionMetric, frozen) {
117
119
  createMotionModel(orbit.object, orbit, minimumMotionMetric, frozen),
118
120
  };
119
121
  }
122
+ function createSpatialTrajectory(trajectory, objectMap) {
123
+ const samples = samplePathPoints(trajectory.path).map((point) => ({
124
+ x: point.x,
125
+ y: 0,
126
+ z: point.y,
127
+ }));
128
+ return {
129
+ trajectoryId: trajectory.trajectoryId,
130
+ trajectory: trajectory.trajectory,
131
+ craftObjectId: trajectory.craftObjectId,
132
+ mode: trajectory.mode,
133
+ stroke: trajectory.stroke,
134
+ strokeWidth: trajectory.strokeWidth,
135
+ marker: trajectory.marker,
136
+ labelMode: trajectory.labelMode,
137
+ showWaypoints: trajectory.showWaypoints,
138
+ samples,
139
+ waypoints: trajectory.waypoints.map((waypoint) => {
140
+ const object = waypoint.objectId ? objectMap.get(waypoint.objectId) ?? null : null;
141
+ return {
142
+ trajectoryId: waypoint.trajectoryId,
143
+ segmentId: waypoint.segmentId,
144
+ maneuverId: waypoint.maneuverId,
145
+ objectId: waypoint.objectId,
146
+ position: object
147
+ ? { ...object.position }
148
+ : { x: waypoint.x, y: 0, z: waypoint.y },
149
+ label: waypoint.label,
150
+ dateLabel: waypoint.dateLabel,
151
+ hidden: waypoint.hidden,
152
+ };
153
+ }),
154
+ hidden: trajectory.hidden,
155
+ };
156
+ }
120
157
  function resolveSpatialObjectPosition(entry, scene, sceneCenter, objectMap, orbitMap, cache) {
121
158
  const cached = cache.get(entry.objectId);
122
159
  if (cached) {
@@ -419,3 +456,22 @@ function clampNumber(value, min, max) {
419
456
  function degreesToRadians(value) {
420
457
  return (value * Math.PI) / 180;
421
458
  }
459
+ function samplePathPoints(path) {
460
+ const matches = [...path.matchAll(/[MLQ]\s*(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?)(?:\s+(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?))?(?:\s+(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?))?/g)];
461
+ if (matches.length === 0) {
462
+ return [];
463
+ }
464
+ const points = [];
465
+ for (const match of matches) {
466
+ const command = match[0][0];
467
+ if (command === "M" || command === "L") {
468
+ points.push({ x: Number(match[1]), y: Number(match[2]) });
469
+ continue;
470
+ }
471
+ if (command === "Q") {
472
+ points.push({ x: Number(match[1]), y: Number(match[2]) });
473
+ points.push({ x: Number(match[5]), y: Number(match[6]) });
474
+ }
475
+ }
476
+ return points;
477
+ }
@@ -2,7 +2,7 @@ export type WorldOrbitObjectType = "system" | "star" | "planet" | "moon" | "belt
2
2
  export type PlacementMode = "orbit" | "at" | "surface" | "free";
3
3
  export type Unit = "au" | "km" | "m" | "ly" | "pc" | "kpc" | "re" | "rj" | "sol" | "me" | "mj" | "s" | "min" | "h" | "d" | "y" | "ky" | "my" | "gy" | "K" | "deg";
4
4
  export type WorldOrbitDocumentVersion = "1.0";
5
- export type WorldOrbitAtlasDocumentVersion = "2.0" | "2.1" | "2.5" | "2.6" | "3.0";
5
+ export type WorldOrbitAtlasDocumentVersion = "2.0" | "2.1" | "2.5" | "2.6" | "3.0" | "3.1";
6
6
  export type WorldOrbitDraftDocumentVersion = "2.0-draft";
7
7
  export type WorldOrbitAnyDocumentVersion = WorldOrbitDocumentVersion | WorldOrbitAtlasDocumentVersion | WorldOrbitDraftDocumentVersion;
8
8
  export type ViewProjection = "topdown" | "isometric" | "orthographic" | "perspective";
@@ -201,6 +201,12 @@ export interface WorldOrbitTrajectory {
201
201
  tags: string[];
202
202
  color: string | null;
203
203
  hidden: boolean;
204
+ renderMode?: TrajectoryRenderMode | null;
205
+ stroke?: string | null;
206
+ strokeWidth?: number | null;
207
+ marker?: string | null;
208
+ labelMode?: string | null;
209
+ showWaypoints?: boolean | null;
204
210
  segments: WorldOrbitTrajectorySegment[];
205
211
  }
206
212
  export interface WorldOrbitTrajectorySegment {
@@ -221,9 +227,14 @@ export interface WorldOrbitTrajectorySegment {
221
227
  phaseAngle?: UnitValue;
222
228
  turnAngle?: UnitValue;
223
229
  energy?: UnitValue;
230
+ waypointLabel?: string | null;
231
+ waypointDate?: string | null;
232
+ renderHidden?: boolean | null;
233
+ sampleDensity?: number | null;
224
234
  notes: string[];
225
235
  maneuvers: WorldOrbitManeuver[];
226
236
  }
237
+ export type TrajectoryRenderMode = "illustrative" | "solver" | "auto";
227
238
  export type WorldOrbitTrajectorySegmentKind = "departure" | "transfer" | "flyby" | "capture" | "stationkeeping" | "escape";
228
239
  export interface WorldOrbitManeuver {
229
240
  id: string;
@@ -330,6 +341,9 @@ export interface SceneRenderOptions {
330
341
  scaleModel?: Partial<RenderScaleModel>;
331
342
  bodyScaleMode?: BodyScaleMode;
332
343
  activeEventId?: string | null;
344
+ trajectoryMode?: TrajectoryRenderMode;
345
+ showTrajectoryWaypoints?: boolean;
346
+ showTrajectoryLabels?: boolean;
333
347
  }
334
348
  export interface SpatialScaleModel {
335
349
  orbitDistanceMultiplier: number;
@@ -435,7 +449,35 @@ export interface RenderSceneEvent {
435
449
  y: number;
436
450
  hidden: boolean;
437
451
  }
438
- export type SceneLayerId = "background" | "guides" | "orbits-back" | "orbits-front" | "relations" | "events" | "objects" | "labels" | "metadata";
452
+ export interface RenderSceneTrajectoryWaypoint {
453
+ renderId: string;
454
+ trajectoryId: string;
455
+ segmentId: string | null;
456
+ maneuverId: string | null;
457
+ objectId: string | null;
458
+ x: number;
459
+ y: number;
460
+ label: string | null;
461
+ dateLabel: string | null;
462
+ hidden: boolean;
463
+ }
464
+ export interface RenderSceneTrajectory {
465
+ renderId: string;
466
+ trajectoryId: string;
467
+ trajectory: WorldOrbitTrajectory;
468
+ craftObjectId: string | null;
469
+ mode: TrajectoryRenderMode;
470
+ path: string;
471
+ stroke: string | null;
472
+ strokeWidth: number;
473
+ marker: string | null;
474
+ labelMode: string | null;
475
+ showWaypoints: boolean;
476
+ objectIds: string[];
477
+ waypoints: RenderSceneTrajectoryWaypoint[];
478
+ hidden: boolean;
479
+ }
480
+ export type SceneLayerId = "background" | "guides" | "orbits-back" | "orbits-front" | "trajectories" | "relations" | "events" | "objects" | "labels" | "metadata";
439
481
  export interface RenderSceneViewpointFilter {
440
482
  query: string | null;
441
483
  objectTypes: Array<Exclude<WorldOrbitObjectType, "system">>;
@@ -554,6 +596,34 @@ export interface SpatialOrbit {
554
596
  hidden: boolean;
555
597
  motion: OrbitalMotionModel | null;
556
598
  }
599
+ export interface SpatialTrajectorySample {
600
+ x: number;
601
+ y: number;
602
+ z: number;
603
+ }
604
+ export interface SpatialTrajectory {
605
+ trajectoryId: string;
606
+ trajectory: WorldOrbitTrajectory;
607
+ craftObjectId: string | null;
608
+ mode: TrajectoryRenderMode;
609
+ stroke: string | null;
610
+ strokeWidth: number;
611
+ marker: string | null;
612
+ labelMode: string | null;
613
+ showWaypoints: boolean;
614
+ samples: SpatialTrajectorySample[];
615
+ waypoints: Array<{
616
+ trajectoryId: string;
617
+ segmentId: string | null;
618
+ maneuverId: string | null;
619
+ objectId: string | null;
620
+ position: CoordinatePoint3D;
621
+ label: string | null;
622
+ dateLabel: string | null;
623
+ hidden: boolean;
624
+ }>;
625
+ hidden: boolean;
626
+ }
557
627
  export interface SpatialFocusTarget {
558
628
  objectId: string;
559
629
  center: CoordinatePoint3D;
@@ -581,6 +651,7 @@ export interface RenderScene {
581
651
  viewpoints: RenderSceneViewpoint[];
582
652
  events: RenderSceneEvent[];
583
653
  activeEventId: string | null;
654
+ trajectories: RenderSceneTrajectory[];
584
655
  objects: RenderSceneObject[];
585
656
  orbitVisuals: RenderOrbitVisual[];
586
657
  relations: RenderSceneRelation[];
@@ -608,6 +679,7 @@ export interface SpatialScene {
608
679
  timeFrozen: boolean;
609
680
  objects: SpatialSceneObject[];
610
681
  orbits: SpatialOrbit[];
682
+ trajectories: SpatialTrajectory[];
611
683
  focusTargets: SpatialFocusTarget[];
612
684
  }
613
685
  export type SceneLayoutPreset = "compact" | "balanced" | "presentation";