worldorbit 3.2.2 → 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 (82) hide show
  1. package/README.md +543 -543
  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 +461 -17
  5. package/dist/browser/core/dist/draft.d.ts +2 -1
  6. package/dist/browser/core/dist/draft.js +26 -4
  7. package/dist/browser/core/dist/format.js +126 -5
  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 +12 -2
  11. package/dist/browser/core/dist/normalize.js +1 -0
  12. package/dist/browser/core/dist/scene.js +226 -5
  13. package/dist/browser/core/dist/schema.js +11 -1
  14. package/dist/browser/core/dist/solver.d.ts +33 -0
  15. package/dist/browser/core/dist/solver.js +99 -0
  16. package/dist/browser/core/dist/spatial-scene.js +56 -0
  17. package/dist/browser/core/dist/types.d.ts +130 -4
  18. package/dist/browser/editor/dist/editor.js +844 -719
  19. package/dist/browser/editor/dist/types.d.ts +2 -1
  20. package/dist/browser/viewer/dist/minimap.js +9 -7
  21. package/dist/browser/viewer/dist/render.js +78 -18
  22. package/dist/browser/viewer/dist/runtime-3d.js +2 -0
  23. package/dist/browser/viewer/dist/theme.js +1 -0
  24. package/dist/browser/viewer/dist/types.d.ts +7 -0
  25. package/dist/browser/viewer/dist/viewer.js +34 -3
  26. package/dist/obsidian-plugin/README.md +141 -124
  27. package/dist/obsidian-plugin/main.js +82 -68
  28. package/dist/unpkg/core/dist/atlas-edit.js +146 -1
  29. package/dist/unpkg/core/dist/atlas-validate.js +105 -10
  30. package/dist/unpkg/core/dist/draft-parse.js +461 -17
  31. package/dist/unpkg/core/dist/draft.d.ts +2 -1
  32. package/dist/unpkg/core/dist/draft.js +26 -4
  33. package/dist/unpkg/core/dist/format.js +126 -5
  34. package/dist/unpkg/core/dist/index.d.ts +1 -0
  35. package/dist/unpkg/core/dist/index.js +1 -0
  36. package/dist/unpkg/core/dist/load.js +12 -2
  37. package/dist/unpkg/core/dist/normalize.js +1 -0
  38. package/dist/unpkg/core/dist/scene.js +226 -5
  39. package/dist/unpkg/core/dist/schema.js +11 -1
  40. package/dist/unpkg/core/dist/solver.d.ts +33 -0
  41. package/dist/unpkg/core/dist/solver.js +99 -0
  42. package/dist/unpkg/core/dist/spatial-scene.js +56 -0
  43. package/dist/unpkg/core/dist/types.d.ts +130 -4
  44. package/dist/unpkg/editor/dist/editor.js +844 -719
  45. package/dist/unpkg/editor/dist/types.d.ts +2 -1
  46. package/dist/unpkg/viewer/dist/minimap.js +9 -7
  47. package/dist/unpkg/viewer/dist/render.js +78 -18
  48. package/dist/unpkg/viewer/dist/runtime-3d.js +2 -0
  49. package/dist/unpkg/viewer/dist/theme.js +1 -0
  50. package/dist/unpkg/viewer/dist/types.d.ts +7 -0
  51. package/dist/unpkg/viewer/dist/viewer.js +34 -3
  52. package/dist/unpkg/worldorbit-core.min.js +12 -12
  53. package/dist/unpkg/worldorbit-editor.min.js +381 -340
  54. package/dist/unpkg/worldorbit-markdown.min.js +47 -33
  55. package/dist/unpkg/worldorbit-viewer.min.js +238 -224
  56. package/dist/unpkg/worldorbit.js +1218 -46
  57. package/dist/unpkg/worldorbit.min.js +242 -228
  58. package/package.json +5 -1
  59. package/packages/core/dist/atlas-edit.js +146 -1
  60. package/packages/core/dist/atlas-validate.js +105 -10
  61. package/packages/core/dist/draft-parse.js +461 -17
  62. package/packages/core/dist/draft.d.ts +2 -1
  63. package/packages/core/dist/draft.js +26 -4
  64. package/packages/core/dist/format.js +126 -5
  65. package/packages/core/dist/index.d.ts +1 -0
  66. package/packages/core/dist/index.js +1 -0
  67. package/packages/core/dist/load.js +12 -2
  68. package/packages/core/dist/normalize.js +1 -0
  69. package/packages/core/dist/scene.js +226 -5
  70. package/packages/core/dist/schema.js +11 -1
  71. package/packages/core/dist/solver.d.ts +33 -0
  72. package/packages/core/dist/solver.js +99 -0
  73. package/packages/core/dist/spatial-scene.js +56 -0
  74. package/packages/core/dist/types.d.ts +130 -4
  75. package/packages/editor/dist/editor.js +844 -719
  76. package/packages/editor/dist/types.d.ts +2 -1
  77. package/packages/viewer/dist/minimap.js +9 -7
  78. package/packages/viewer/dist/render.js +78 -18
  79. package/packages/viewer/dist/runtime-3d.js +2 -0
  80. package/packages/viewer/dist/theme.js +1 -0
  81. package/packages/viewer/dist/types.d.ts +7 -0
  82. package/packages/viewer/dist/viewer.js +34 -3
@@ -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.1",
22
+ schemaVersion: "3.1",
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: structuredClone(document.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
  }
@@ -468,7 +490,7 @@ function serializeViewpointLayers(layers) {
468
490
  if (orbitFront !== undefined || orbitBack !== undefined) {
469
491
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
470
492
  }
471
- for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
493
+ for (const key of ["background", "guides", "relations", "events", "objects", "trajectories", "labels", "metadata"]) {
472
494
  if (layers[key] !== undefined) {
473
495
  tokens.push(layers[key] ? key : `-${key}`);
474
496
  }
@@ -40,10 +40,14 @@ export function formatDocument(document, options = {}) {
40
40
  schema === "2.1" ||
41
41
  schema === "2.5" ||
42
42
  schema === "2.6" ||
43
+ schema === "3.0" ||
44
+ schema === "3.1" ||
43
45
  schema === "2.0-draft" ||
44
46
  document.version === "2.0" ||
45
47
  document.version === "2.1" ||
46
48
  document.version === "2.5" ||
49
+ document.version === "3.0" ||
50
+ document.version === "3.1" ||
47
51
  document.version === "2.6" ||
48
52
  document.version === "2.0-draft";
49
53
  if (useDraft) {
@@ -59,7 +63,12 @@ export function formatDocument(document, options = {}) {
59
63
  : upgradeDocumentToDraftV2(document);
60
64
  return formatDraftDocument(legacyDraftDocument);
61
65
  }
62
- const atlasDocument = document.version === "2.0" || document.version === "2.1" || document.version === "2.5" || document.version === "2.6"
66
+ const atlasDocument = document.version === "2.0" ||
67
+ document.version === "2.1" ||
68
+ document.version === "2.5" ||
69
+ document.version === "2.6" ||
70
+ document.version === "3.0" ||
71
+ document.version === "3.1"
63
72
  ? document
64
73
  : document.version === "2.0-draft"
65
74
  ? {
@@ -68,7 +77,13 @@ export function formatDocument(document, options = {}) {
68
77
  schemaVersion: "2.0",
69
78
  }
70
79
  : upgradeDocumentToV2(document);
71
- if ((schema === "2.0" || schema === "2.1" || schema === "2.5" || schema === "2.6") && atlasDocument.version !== schema) {
80
+ if ((schema === "2.0" ||
81
+ schema === "2.1" ||
82
+ schema === "2.5" ||
83
+ schema === "2.6" ||
84
+ schema === "3.0" ||
85
+ schema === "3.1") &&
86
+ atlasDocument.version !== schema) {
72
87
  return formatAtlasDocument({
73
88
  ...atlasDocument,
74
89
  version: schema,
@@ -108,6 +123,10 @@ export function formatAtlasDocument(document) {
108
123
  lines.push("");
109
124
  lines.push(...formatAtlasEvent(event));
110
125
  }
126
+ for (const trajectory of [...document.trajectories].sort(compareIdLike)) {
127
+ lines.push("");
128
+ lines.push(...formatAtlasTrajectory(trajectory));
129
+ }
111
130
  const sortedObjects = [...document.objects].sort(compareObjects);
112
131
  if (sortedObjects.length > 0 && lines.at(-1) !== "") {
113
132
  lines.push("");
@@ -295,6 +314,9 @@ function formatObjectMetadata(object) {
295
314
  if (object.groups?.length) {
296
315
  lines.push(`groups ${object.groups.join(" ")}`);
297
316
  }
317
+ if (object.trajectoryId) {
318
+ lines.push(`trajectory ${object.trajectoryId}`);
319
+ }
298
320
  if (object.epoch) {
299
321
  lines.push(`epoch ${quoteIfNeeded(object.epoch)}`);
300
322
  }
@@ -455,6 +477,9 @@ function formatAtlasEvent(event) {
455
477
  if (event.summary) {
456
478
  lines.push(` summary ${quoteIfNeeded(event.summary)}`);
457
479
  }
480
+ if (event.trajectoryId) {
481
+ lines.push(` trajectory ${event.trajectoryId}`);
482
+ }
458
483
  if (event.targetObjectId) {
459
484
  lines.push(` target ${event.targetObjectId}`);
460
485
  }
@@ -497,12 +522,106 @@ function formatAtlasEvent(event) {
497
522
  function formatEventPoseFields(pose) {
498
523
  return [
499
524
  ...formatPlacement(pose.placement),
525
+ ...(pose.trajectorySegmentId ? [`segment ${pose.trajectorySegmentId}`] : []),
526
+ ...(pose.trajectoryManeuverId ? [`maneuver ${pose.trajectoryManeuverId}`] : []),
500
527
  ...(pose.epoch ? [`epoch ${quoteIfNeeded(pose.epoch)}`] : []),
501
528
  ...(pose.referencePlane ? [`referencePlane ${quoteIfNeeded(pose.referencePlane)}`] : []),
502
529
  ...formatOptionalUnit("inner", pose.inner),
503
530
  ...formatOptionalUnit("outer", pose.outer),
504
531
  ];
505
532
  }
533
+ function formatAtlasTrajectory(trajectory) {
534
+ const lines = [`trajectory ${trajectory.id}`];
535
+ if (trajectory.label) {
536
+ lines.push(` label ${quoteIfNeeded(trajectory.label)}`);
537
+ }
538
+ if (trajectory.summary) {
539
+ lines.push(` summary ${quoteIfNeeded(trajectory.summary)}`);
540
+ }
541
+ if (trajectory.craftObjectId) {
542
+ lines.push(` craft ${trajectory.craftObjectId}`);
543
+ }
544
+ if (trajectory.tags.length > 0) {
545
+ lines.push(` tags ${trajectory.tags.map(quoteIfNeeded).join(" ")}`);
546
+ }
547
+ if (trajectory.color) {
548
+ lines.push(` color ${quoteIfNeeded(trajectory.color)}`);
549
+ }
550
+ if (trajectory.renderMode) {
551
+ lines.push(` renderMode ${trajectory.renderMode}`);
552
+ }
553
+ if (trajectory.stroke) {
554
+ lines.push(` stroke ${quoteIfNeeded(trajectory.stroke)}`);
555
+ }
556
+ if (trajectory.strokeWidth !== null && trajectory.strokeWidth !== undefined) {
557
+ lines.push(` strokeWidth ${trajectory.strokeWidth}`);
558
+ }
559
+ if (trajectory.marker) {
560
+ lines.push(` marker ${quoteIfNeeded(trajectory.marker)}`);
561
+ }
562
+ if (trajectory.labelMode) {
563
+ lines.push(` labelMode ${quoteIfNeeded(trajectory.labelMode)}`);
564
+ }
565
+ if (trajectory.showWaypoints !== null && trajectory.showWaypoints !== undefined) {
566
+ lines.push(` showWaypoints ${trajectory.showWaypoints ? "true" : "false"}`);
567
+ }
568
+ if (trajectory.hidden) {
569
+ lines.push(" hidden true");
570
+ }
571
+ for (const segment of [...trajectory.segments].sort(compareIdLike)) {
572
+ lines.push("");
573
+ lines.push(` segment ${segment.id}`);
574
+ for (const field of formatTrajectorySegmentFields(segment)) {
575
+ lines.push(` ${field}`);
576
+ }
577
+ for (const maneuver of [...segment.maneuvers].sort(compareIdLike)) {
578
+ lines.push(` maneuver ${maneuver.id}`);
579
+ for (const field of formatTrajectoryManeuverFields(maneuver)) {
580
+ lines.push(` ${field}`);
581
+ }
582
+ }
583
+ }
584
+ return lines;
585
+ }
586
+ function formatTrajectorySegmentFields(segment) {
587
+ return [
588
+ `kind ${segment.kind}`,
589
+ ...(segment.label ? [`label ${quoteIfNeeded(segment.label)}`] : []),
590
+ ...(segment.summary ? [`summary ${quoteIfNeeded(segment.summary)}`] : []),
591
+ ...(segment.fromObjectId ? [`from ${segment.fromObjectId}`] : []),
592
+ ...(segment.toObjectId ? [`to ${segment.toObjectId}`] : []),
593
+ ...(segment.aroundObjectId ? [`around ${segment.aroundObjectId}`] : []),
594
+ ...(segment.assist?.objectId ? [`assist ${segment.assist.objectId}`] : []),
595
+ ...(segment.epoch ? [`epoch ${quoteIfNeeded(segment.epoch)}`] : []),
596
+ ...formatOptionalUnit("periapsis", segment.periapsis),
597
+ ...formatOptionalUnit("apoapsis", segment.apoapsis),
598
+ ...formatOptionalUnit("inclination", segment.inclination),
599
+ ...formatOptionalUnit("duration", segment.duration),
600
+ ...formatOptionalUnit("deltaV", segment.deltaV),
601
+ ...formatOptionalUnit("phaseAngle", segment.phaseAngle),
602
+ ...formatOptionalUnit("turnAngle", segment.turnAngle),
603
+ ...formatOptionalUnit("energy", segment.energy),
604
+ ...(segment.waypointLabel ? [`waypointLabel ${quoteIfNeeded(segment.waypointLabel)}`] : []),
605
+ ...(segment.waypointDate ? [`waypointDate ${quoteIfNeeded(segment.waypointDate)}`] : []),
606
+ ...(segment.renderHidden !== null && segment.renderHidden !== undefined
607
+ ? [`renderHidden ${segment.renderHidden ? "true" : "false"}`]
608
+ : []),
609
+ ...(segment.sampleDensity !== null && segment.sampleDensity !== undefined
610
+ ? [`sampleDensity ${segment.sampleDensity}`]
611
+ : []),
612
+ ...(segment.notes.length > 0 ? [`notes ${segment.notes.map(quoteIfNeeded).join(" ")}`] : []),
613
+ ];
614
+ }
615
+ function formatTrajectoryManeuverFields(maneuver) {
616
+ return [
617
+ `kind ${quoteIfNeeded(maneuver.kind)}`,
618
+ ...(maneuver.label ? [`label ${quoteIfNeeded(maneuver.label)}`] : []),
619
+ ...(maneuver.epoch ? [`epoch ${quoteIfNeeded(maneuver.epoch)}`] : []),
620
+ ...formatOptionalUnit("deltaV", maneuver.deltaV),
621
+ ...formatOptionalUnit("duration", maneuver.duration),
622
+ ...(maneuver.notes.length > 0 ? [`notes ${maneuver.notes.map(quoteIfNeeded).join(" ")}`] : []),
623
+ ];
624
+ }
506
625
  function hasCameraValues(camera) {
507
626
  return (camera.azimuth !== null ||
508
627
  camera.elevation !== null ||
@@ -554,7 +673,7 @@ function formatDraftLayers(layers) {
554
673
  ? "orbits"
555
674
  : "-orbits");
556
675
  }
557
- for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
676
+ for (const key of ["background", "guides", "relations", "events", "objects", "trajectories", "labels", "metadata"]) {
558
677
  if (layers[key] !== undefined) {
559
678
  tokens.push(layers[key] ? key : `-${key}`);
560
679
  }
@@ -601,10 +720,12 @@ function objectTypeIndex(objectType) {
601
720
  return 5;
602
721
  case "ring":
603
722
  return 6;
604
- case "structure":
723
+ case "craft":
605
724
  return 7;
606
- case "phenomenon":
725
+ case "structure":
607
726
  return 8;
727
+ case "phenomenon":
728
+ return 9;
608
729
  }
609
730
  }
610
731
  function quoteIfNeeded(value) {
@@ -8,6 +8,7 @@ export { validateDocument } from "./validate.js";
8
8
  export { createDiagnostic, diagnosticFromError, normalizeWithDiagnostics, parseWithDiagnostics, validateDocumentWithDiagnostics, } from "./diagnostics.js";
9
9
  export { renderDocumentToScene, rotatePoint } from "./scene.js";
10
10
  export { evaluateSpatialSceneAtTime, renderDocumentToSpatialScene, } from "./spatial-scene.js";
11
+ export { createTrajectorySolverSnapshot } from "./solver.js";
11
12
  export { formatAtlasDocument, formatDocument, formatDraftDocument } from "./format.js";
12
13
  export { materializeAtlasDocument, materializeDraftDocument, upgradeDocumentToDraftV2, upgradeDocumentToV2, } from "./draft.js";
13
14
  export { parseWorldOrbitAtlas, parseWorldOrbitDraft } from "./draft-parse.js";
@@ -8,6 +8,7 @@ export { validateDocument } from "./validate.js";
8
8
  export { createDiagnostic, diagnosticFromError, normalizeWithDiagnostics, parseWithDiagnostics, validateDocumentWithDiagnostics, } from "./diagnostics.js";
9
9
  export { renderDocumentToScene, rotatePoint } from "./scene.js";
10
10
  export { evaluateSpatialSceneAtTime, renderDocumentToSpatialScene, } from "./spatial-scene.js";
11
+ export { createTrajectorySolverSnapshot } from "./solver.js";
11
12
  export { formatAtlasDocument, formatDocument, formatDraftDocument } from "./format.js";
12
13
  export { materializeAtlasDocument, materializeDraftDocument, upgradeDocumentToDraftV2, upgradeDocumentToV2, } from "./draft.js";
13
14
  export { parseWorldOrbitAtlas, parseWorldOrbitDraft } from "./draft-parse.js";
@@ -5,10 +5,12 @@ import { WorldOrbitError } from "./errors.js";
5
5
  import { normalizeDocument } from "./normalize.js";
6
6
  import { parseWorldOrbit } from "./parse.js";
7
7
  import { validateDocument } from "./validate.js";
8
- const ATLAS_SCHEMA_PATTERN = /^schema\s+2(?:\.0|\.1|\.5|\.6)?$/i;
8
+ const ATLAS_SCHEMA_PATTERN = /^schema\s+(?:2(?:\.0|\.1|\.5|\.6)?|3(?:\.0|\.1)?)$/i;
9
9
  const ATLAS_SCHEMA_21_PATTERN = /^schema\s+2\.1$/i;
10
10
  const ATLAS_SCHEMA_25_PATTERN = /^schema\s+2\.5$/i;
11
11
  const ATLAS_SCHEMA_26_PATTERN = /^schema\s+2\.6$/i;
12
+ const ATLAS_SCHEMA_30_PATTERN = /^schema\s+3(?:\.0)?$/i;
13
+ const ATLAS_SCHEMA_31_PATTERN = /^schema\s+3\.1$/i;
12
14
  const LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
13
15
  export function detectWorldOrbitSchemaVersion(source) {
14
16
  for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
@@ -28,6 +30,12 @@ export function detectWorldOrbitSchemaVersion(source) {
28
30
  if (ATLAS_SCHEMA_26_PATTERN.test(trimmed)) {
29
31
  return "2.6";
30
32
  }
33
+ if (ATLAS_SCHEMA_30_PATTERN.test(trimmed)) {
34
+ return "3.0";
35
+ }
36
+ if (ATLAS_SCHEMA_31_PATTERN.test(trimmed)) {
37
+ return "3.1";
38
+ }
31
39
  if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
32
40
  return "2.0";
33
41
  }
@@ -92,7 +100,9 @@ export function loadWorldOrbitSourceWithDiagnostics(source) {
92
100
  schemaVersion === "2.0-draft" ||
93
101
  schemaVersion === "2.1" ||
94
102
  schemaVersion === "2.5" ||
95
- schemaVersion === "2.6") {
103
+ schemaVersion === "2.6" ||
104
+ schemaVersion === "3.0" ||
105
+ schemaVersion === "3.1") {
96
106
  return loadAtlasSourceWithDiagnostics(source, schemaVersion);
97
107
  }
98
108
  let ast;
@@ -33,6 +33,7 @@ export function normalizeDocument(ast) {
33
33
  groups: [],
34
34
  relations: [],
35
35
  events: [],
36
+ trajectories: [],
36
37
  objects,
37
38
  };
38
39
  }
@@ -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,
@@ -467,7 +469,9 @@ function createSceneObject(position, scaleModel, relationships) {
467
469
  anchorX,
468
470
  anchorY,
469
471
  label: object.id,
470
- secondaryLabel: object.type === "structure" ? String(object.properties.kind ?? object.type) : object.type,
472
+ secondaryLabel: object.type === "structure" || object.type === "craft"
473
+ ? String(object.properties.kind ?? object.type)
474
+ : object.type,
471
475
  fillColor: customColorFor(object.properties.color),
472
476
  imageHref: typeof object.properties.image === "string" && object.properties.image.trim()
473
477
  ? object.properties.image
@@ -566,6 +570,7 @@ function labelPlacementPriority(object) {
566
570
  case "asteroid":
567
571
  case "comet":
568
572
  return 4;
573
+ case "craft":
569
574
  case "structure":
570
575
  case "phenomenon":
571
576
  return 5;
@@ -590,7 +595,8 @@ function preferredLabelDirections(object, objectMap, sceneWidth, sceneHeight) {
590
595
  const oppositeVertical = vertical === "below" ? "above" : "below";
591
596
  const horizontal = defaultHorizontalDirection(object, parent, sceneWidth);
592
597
  const oppositeHorizontal = horizontal === "right" ? "left" : "right";
593
- const preferHorizontal = object.object.type === "structure" ||
598
+ const preferHorizontal = object.object.type === "craft" ||
599
+ object.object.type === "structure" ||
594
600
  object.object.type === "phenomenon" ||
595
601
  object.object.placement?.mode === "at" ||
596
602
  object.object.placement?.mode === "surface" ||
@@ -658,7 +664,7 @@ function createLabelPlacement(object, direction, attempt, labelMultiplier) {
658
664
  }
659
665
  }
660
666
  }
661
- function createSceneLayers(orbitVisuals, relations, events, leaders, objects, labels) {
667
+ function createSceneLayers(orbitVisuals, trajectories, relations, events, leaders, objects, labels) {
662
668
  const backOrbitIds = orbitVisuals
663
669
  .filter((visual) => !visual.hidden && Boolean(visual.backArcPath))
664
670
  .map((visual) => visual.renderId);
@@ -673,6 +679,15 @@ function createSceneLayers(orbitVisuals, relations, events, leaders, objects, la
673
679
  },
674
680
  { id: "orbits-back", renderIds: backOrbitIds },
675
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
+ },
676
691
  {
677
692
  id: "relations",
678
693
  renderIds: relations.filter((relation) => !relation.hidden).map((relation) => relation.renderId),
@@ -815,6 +830,206 @@ function createSceneEvents(events, objects, activeEventId) {
815
830
  })
816
831
  .sort((left, right) => left.event.id.localeCompare(right.event.id));
817
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
+ }
818
1033
  function createSceneViewpoints(document, projection, preset, relationships, objectMap) {
819
1034
  const generatedOverview = createGeneratedOverviewViewpoint(document, projection, preset);
820
1035
  const drafts = new Map();
@@ -1095,7 +1310,8 @@ function parseViewpointLayers(value) {
1095
1310
  rawLayer === "events" ||
1096
1311
  rawLayer === "objects" ||
1097
1312
  rawLayer === "labels" ||
1098
- rawLayer === "metadata") {
1313
+ rawLayer === "metadata" ||
1314
+ rawLayer === "trajectories") {
1099
1315
  next[rawLayer] = enabled;
1100
1316
  }
1101
1317
  }
@@ -1109,6 +1325,7 @@ function parseViewpointObjectTypes(value) {
1109
1325
  entry === "asteroid" ||
1110
1326
  entry === "comet" ||
1111
1327
  entry === "ring" ||
1328
+ entry === "craft" ||
1112
1329
  entry === "structure" ||
1113
1330
  entry === "phenomenon");
1114
1331
  }
@@ -1785,6 +2002,8 @@ function visualRadiusFor(object, depth, scaleModel, sceneMetricScale) {
1785
2002
  return clampNumber(6 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1786
2003
  case "ring":
1787
2004
  return clampNumber(5 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
2005
+ case "craft":
2006
+ return clampNumber(5 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1788
2007
  case "structure":
1789
2008
  return clampNumber(6 * multiplier, scaleModel.minBodyRadius, scaleModel.maxBodyRadius);
1790
2009
  case "phenomenon":
@@ -1798,6 +2017,8 @@ function visualExtentForObject(object, radius, scaleModel) {
1798
2017
  return radius * 2.4;
1799
2018
  case "phenomenon":
1800
2019
  return radius * 1.25;
2020
+ case "craft":
2021
+ return radius + 1.5;
1801
2022
  case "structure":
1802
2023
  return radius + 2;
1803
2024
  default: