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
@@ -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";
@@ -40,6 +40,13 @@ export function renderSceneToSvg(scene, options = {}) {
40
40
  .map((relation) => `<line class="wo-relation" x1="${relation.x1}" y1="${relation.y1}" x2="${relation.x2}" y2="${relation.y2}" data-render-id="${escapeXml(relation.renderId)}" data-relation-id="${escapeAttribute(relation.relationId)}" />`)
41
41
  .join("")
42
42
  : "";
43
+ const trajectoryMarkup = layers.trajectories
44
+ ? renderTrajectoryLayer(scene, visibleObjectIds, {
45
+ showLabels: options.showTrajectoryLabels ?? true,
46
+ showWaypoints: options.showTrajectoryWaypoints ?? true,
47
+ includeStructures: layers.structures,
48
+ })
49
+ : "";
43
50
  const eventMarkup = layers.events
44
51
  ? scene.events
45
52
  .filter((event) => !event.hidden)
@@ -78,12 +85,15 @@ export function renderSceneToSvg(scene, options = {}) {
78
85
  <stop offset="0%" stop-color="${theme.starGlow}" stop-opacity="0.95" />
79
86
  <stop offset="100%" stop-color="${theme.starCore}" stop-opacity="0" />
80
87
  </radialGradient>
81
- <radialGradient id="wo-bg-glow" cx="20%" cy="10%" r="90%">
82
- <stop offset="0%" stop-color="${theme.backgroundGlow}" />
83
- <stop offset="100%" stop-color="transparent" />
84
- </radialGradient>
85
- ${imageDefinitions}
86
- </defs>
88
+ <radialGradient id="wo-bg-glow" cx="20%" cy="10%" r="90%">
89
+ <stop offset="0%" stop-color="${theme.backgroundGlow}" />
90
+ <stop offset="100%" stop-color="transparent" />
91
+ </radialGradient>
92
+ <marker id="wo-trajectory-arrow" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto" markerUnits="strokeWidth">
93
+ <path d="M 0 0 L 12 6 L 0 12 z" fill="${theme.accent}" />
94
+ </marker>
95
+ ${imageDefinitions}
96
+ </defs>
87
97
  <style>
88
98
  .wo-bg { fill: url(#wo-bg); }
89
99
  .wo-bg-glow { fill: url(#wo-bg-glow); }
@@ -91,9 +101,13 @@ export function renderSceneToSvg(scene, options = {}) {
91
101
  .wo-orbit { fill: none; stroke: ${theme.orbit}; stroke-width: 1.5; }
92
102
  .wo-orbit-back { opacity: 0.38; stroke-dasharray: 8 6; }
93
103
  .wo-orbit-front { opacity: 0.9; }
94
- .wo-orbit-band { stroke: ${theme.orbitBand}; stroke-linecap: round; }
95
- .wo-relation { stroke: ${theme.relation}; stroke-width: 2; stroke-dasharray: 10 6; }
96
- .wo-event-line { stroke: ${theme.accent}; stroke-width: 1.6; stroke-dasharray: 5 5; opacity: 0.72; }
104
+ .wo-orbit-band { stroke: ${theme.orbitBand}; stroke-linecap: round; }
105
+ .wo-relation { stroke: ${theme.relation}; stroke-width: 2; stroke-dasharray: 10 6; }
106
+ .wo-trajectory { fill: none; stroke: ${theme.accent}; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; opacity: 0.92; }
107
+ .wo-trajectory-waypoint { fill: ${theme.spaceFog}; stroke: ${theme.selected}; stroke-width: 1.4; }
108
+ .wo-trajectory-label { fill: ${theme.accent}; font-family: ${theme.fontFamily}; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
109
+ .wo-trajectory-date { fill: ${theme.muted}; font-family: ${theme.fontFamily}; font-weight: 500; }
110
+ .wo-event-line { stroke: ${theme.accent}; stroke-width: 1.6; stroke-dasharray: 5 5; opacity: 0.72; }
97
111
  .wo-event-node { fill: ${theme.accent}; stroke: ${theme.selected}; stroke-width: 1.4; opacity: 0.92; }
98
112
  .wo-event-label { fill: ${theme.accent}; font-family: ${theme.fontFamily}; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; }
99
113
  .wo-leader { stroke: ${theme.leader}; stroke-width: 1.5; stroke-dasharray: 6 5; }
@@ -129,12 +143,13 @@ export function renderSceneToSvg(scene, options = {}) {
129
143
  <g data-worldorbit-world-content="true">
130
144
  ${layers.orbits ? `<g data-layer-id="orbits-back">${orbitMarkup.back}</g>` : ""}
131
145
  ${layers.guides ? `<g data-layer-id="guides">${leaderMarkup}</g>` : ""}
132
- ${layers.relations ? `<g data-layer-id="relations">${relationMarkup}</g>` : ""}
133
- ${layers.events ? `<g data-layer-id="events">${eventMarkup}</g>` : ""}
134
- ${layers.objects ? `<g data-layer-id="objects">${objectMarkup}</g>` : ""}
135
- ${layers.orbits ? `<g data-layer-id="orbits-front">${orbitMarkup.front}</g>` : ""}
136
- ${layers.labels ? `<g data-layer-id="labels">${labelMarkup}</g>` : ""}
137
- </g>
146
+ ${layers.relations ? `<g data-layer-id="relations">${relationMarkup}</g>` : ""}
147
+ ${layers.events ? `<g data-layer-id="events">${eventMarkup}</g>` : ""}
148
+ ${layers.objects ? `<g data-layer-id="objects">${objectMarkup}</g>` : ""}
149
+ ${layers.orbits ? `<g data-layer-id="orbits-front">${orbitMarkup.front}</g>` : ""}
150
+ ${layers.trajectories ? `<g data-layer-id="trajectories">${trajectoryMarkup}</g>` : ""}
151
+ ${layers.labels ? `<g data-layer-id="labels">${labelMarkup}</g>` : ""}
152
+ </g>
138
153
  </g>
139
154
  </g>
140
155
  </svg>`;
@@ -158,6 +173,47 @@ function renderSceneEventOverlay(scene, event, visibleObjectIds, theme) {
158
173
  <text class="wo-event-label" x="${event.x}" y="${event.y - 10}" text-anchor="middle" font-size="10">${escapeXml(label)}</text>
159
174
  </g>`;
160
175
  }
176
+ function renderTrajectoryLayer(scene, visibleObjectIds, options) {
177
+ return scene.trajectories
178
+ .filter((trajectory) => !trajectory.hidden)
179
+ .filter((trajectory) => trajectory.objectIds.length === 0 || trajectory.objectIds.some((objectId) => visibleObjectIds.has(objectId)))
180
+ .filter((trajectory) => options.includeStructures ||
181
+ !trajectory.objectIds.some((objectId) => {
182
+ const object = scene.objects.find((entry) => entry.objectId === objectId)?.object;
183
+ return object ? isStructureLike(object) : false;
184
+ }))
185
+ .map((trajectory) => {
186
+ const stroke = trajectory.stroke ?? "#f0b464";
187
+ const markerEnd = trajectory.marker === "none" ? "" : ` marker-end="url(#wo-trajectory-arrow)"`;
188
+ const waypointMarkup = trajectory.showWaypoints && options.showWaypoints
189
+ ? trajectory.waypoints
190
+ .filter((waypoint) => !waypoint.hidden)
191
+ .map((waypoint) => renderTrajectoryWaypoint(trajectory, waypoint, options.showLabels))
192
+ .join("")
193
+ : "";
194
+ return `<g class="wo-trajectory-group" data-render-id="${escapeXml(trajectory.renderId)}" data-trajectory-id="${escapeAttribute(trajectory.trajectoryId)}">
195
+ <path class="wo-trajectory wo-trajectory-${trajectory.mode}" d="${trajectory.path}" stroke="${escapeAttribute(stroke)}" stroke-width="${trajectory.strokeWidth}"${markerEnd} />
196
+ ${waypointMarkup}
197
+ </g>`;
198
+ })
199
+ .join("");
200
+ }
201
+ function renderTrajectoryWaypoint(trajectory, waypoint, showLabels) {
202
+ const labelMarkup = showLabels && trajectory.labelMode !== "hidden"
203
+ ? [
204
+ waypoint.label
205
+ ? `<text class="wo-trajectory-label" x="${waypoint.x + 10}" y="${waypoint.y - 10}" font-size="10">${escapeXml(waypoint.label)}</text>`
206
+ : "",
207
+ waypoint.dateLabel
208
+ ? `<text class="wo-trajectory-date" x="${waypoint.x + 10}" y="${waypoint.y + 4}" font-size="9">${escapeXml(waypoint.dateLabel)}</text>`
209
+ : "",
210
+ ].join("")
211
+ : "";
212
+ return `<g class="wo-trajectory-waypoint-group" data-render-id="${escapeXml(waypoint.renderId)}" data-trajectory-id="${escapeAttribute(waypoint.trajectoryId)}">
213
+ <circle class="wo-trajectory-waypoint" cx="${waypoint.x}" cy="${waypoint.y}" r="4.5" />
214
+ ${labelMarkup}
215
+ </g>`;
216
+ }
161
217
  export function renderDocumentToSvg(document, options = {}) {
162
218
  return renderSceneToSvg(renderDocumentToScene(document, options), options);
163
219
  }
@@ -4,6 +4,7 @@ const DEFAULT_LAYERS = {
4
4
  relations: true,
5
5
  events: true,
6
6
  orbits: true,
7
+ trajectories: true,
7
8
  objects: true,
8
9
  labels: true,
9
10
  structures: true,
@@ -43,6 +43,7 @@ export interface ViewerLayerOptions {
43
43
  relations?: boolean;
44
44
  events?: boolean;
45
45
  orbits?: boolean;
46
+ trajectories?: boolean;
46
47
  objects?: boolean;
47
48
  labels?: boolean;
48
49
  structures?: boolean;
@@ -77,6 +78,9 @@ export interface ViewerRenderOptions extends Omit<SvgRenderOptions, "selectedObj
77
78
  viewMode?: WorldOrbitViewMode;
78
79
  quality?: WorldOrbit3DQuality;
79
80
  style3d?: WorldOrbit3DStyle;
81
+ trajectoryMode?: "illustrative" | "solver" | "auto";
82
+ showTrajectoryWaypoints?: boolean;
83
+ showTrajectoryLabels?: boolean;
80
84
  }
81
85
  export interface ViewerState {
82
86
  scale: number;
@@ -130,6 +134,9 @@ export interface ViewerAtlasState {
130
134
  layers?: ViewerLayerOptions;
131
135
  scaleModel?: Partial<RenderScaleModel>;
132
136
  bodyScaleMode?: SceneRenderOptions["bodyScaleMode"];
137
+ trajectoryMode?: SceneRenderOptions["trajectoryMode"];
138
+ showTrajectoryWaypoints?: boolean;
139
+ showTrajectoryLabels?: boolean;
133
140
  activeEventId?: string | null;
134
141
  viewMode?: WorldOrbitViewMode;
135
142
  quality?: WorldOrbit3DQuality;
@@ -1691,6 +1691,33 @@ function fallbackSpatialSceneFromRenderScene(scene) {
1691
1691
  hidden: orbit.hidden,
1692
1692
  motion: null,
1693
1693
  })),
1694
+ trajectories: scene.trajectories.map((trajectory) => ({
1695
+ trajectoryId: trajectory.trajectoryId,
1696
+ trajectory: trajectory.trajectory,
1697
+ craftObjectId: trajectory.craftObjectId,
1698
+ mode: trajectory.mode,
1699
+ stroke: trajectory.stroke,
1700
+ strokeWidth: trajectory.strokeWidth,
1701
+ marker: trajectory.marker,
1702
+ labelMode: trajectory.labelMode,
1703
+ showWaypoints: trajectory.showWaypoints,
1704
+ samples: [],
1705
+ waypoints: trajectory.waypoints.map((waypoint) => ({
1706
+ trajectoryId: waypoint.trajectoryId,
1707
+ segmentId: waypoint.segmentId,
1708
+ maneuverId: waypoint.maneuverId,
1709
+ objectId: waypoint.objectId,
1710
+ position: {
1711
+ x: waypoint.x - scene.contentBounds.centerX,
1712
+ y: 0,
1713
+ z: waypoint.y - scene.contentBounds.centerY,
1714
+ },
1715
+ label: waypoint.label,
1716
+ dateLabel: waypoint.dateLabel,
1717
+ hidden: waypoint.hidden,
1718
+ })),
1719
+ hidden: trajectory.hidden,
1720
+ })),
1694
1721
  focusTargets: scene.objects.map((object) => ({
1695
1722
  objectId: object.objectId,
1696
1723
  center: {