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
@@ -179,7 +179,7 @@ function parseAtlasSource(source, forcedOutputVersion) {
179
179
  handleSectionLine(section, indent, tokens, lineNumber);
180
180
  }
181
181
  if (!sawSchemaHeader) {
182
- throw new WorldOrbitError('Missing required atlas schema header "schema 2.0" or "schema 3.0"');
182
+ throw new WorldOrbitError('Missing required atlas schema header "schema 2.0", "schema 3.0", or "schema 3.1"');
183
183
  }
184
184
  const objects = objectNodes.map((node) => normalizeDraftObject(node, sourceSchemaVersion, diagnostics));
185
185
  const normalizedEvents = events.map((event) => normalizeDraftEvent(event, eventPoseNodes.get(event.id) ?? []));
@@ -225,21 +225,23 @@ function parseAtlasSource(source, forcedOutputVersion) {
225
225
  function assertDraftSchemaHeader(tokens, line) {
226
226
  if (tokens.length !== 2 ||
227
227
  tokens[0].value.toLowerCase() !== "schema" ||
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);
228
+ !["2.0-draft", "2.0", "2.1", "2.5", "2.6", "3.0", "3.1"].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", "schema 3.1", or legacy "schema 2.0-draft"', line, tokens[0]?.column ?? 1);
230
230
  }
231
231
  const version = tokens[1].value.toLowerCase();
232
232
  return version === "2.6"
233
233
  ? "2.6"
234
234
  : version === "3.0"
235
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";
236
+ : version === "3.1"
237
+ ? "3.1"
238
+ : version === "2.5"
239
+ ? "2.5"
240
+ : version === "2.1"
241
+ ? "2.1"
242
+ : version === "2.0-draft"
243
+ ? "2.0-draft"
244
+ : "2.0";
243
245
  }
244
246
  function startTopLevelSection(tokens, line, sourceSchemaVersion, diagnostics, system, objectNodes, groups, relations, events, trajectories, eventPoseNodes, viewpointIds, annotationIds, groupIds, relationIds, eventIds, trajectoryIds, flags) {
245
247
  const keyword = tokens[0]?.value.toLowerCase();
@@ -523,6 +525,12 @@ function startTrajectorySection(tokens, line, trajectories, trajectoryIds, sourc
523
525
  craftObjectId: null,
524
526
  tags: [],
525
527
  color: null,
528
+ renderMode: null,
529
+ stroke: null,
530
+ strokeWidth: null,
531
+ marker: null,
532
+ labelMode: null,
533
+ showWaypoints: null,
526
534
  hidden: false,
527
535
  segments: [],
528
536
  };
@@ -1090,6 +1098,10 @@ function applyTrajectoryField(section, indent, tokens, line) {
1090
1098
  aroundObjectId: null,
1091
1099
  assist: null,
1092
1100
  epoch: null,
1101
+ waypointLabel: null,
1102
+ waypointDate: null,
1103
+ renderHidden: null,
1104
+ sampleDensity: null,
1093
1105
  notes: [],
1094
1106
  maneuvers: [],
1095
1107
  };
@@ -1124,6 +1136,54 @@ function applyTrajectoryField(section, indent, tokens, line) {
1124
1136
  case "color":
1125
1137
  section.trajectory.color = value;
1126
1138
  return;
1139
+ case "rendermode":
1140
+ warnIfSchema31Feature(section.sourceSchemaVersion, section.diagnostics, "trajectory.renderMode", {
1141
+ line,
1142
+ column: tokens[0].column,
1143
+ });
1144
+ if (value !== "illustrative" && value !== "solver" && value !== "auto") {
1145
+ throw new WorldOrbitError(`Unknown trajectory render mode "${value}"`, line, tokens[0].column);
1146
+ }
1147
+ section.trajectory.renderMode = value;
1148
+ return;
1149
+ case "stroke":
1150
+ warnIfSchema31Feature(section.sourceSchemaVersion, section.diagnostics, "trajectory.stroke", {
1151
+ line,
1152
+ column: tokens[0].column,
1153
+ });
1154
+ section.trajectory.stroke = value;
1155
+ return;
1156
+ case "strokewidth":
1157
+ warnIfSchema31Feature(section.sourceSchemaVersion, section.diagnostics, "trajectory.strokeWidth", {
1158
+ line,
1159
+ column: tokens[0].column,
1160
+ });
1161
+ section.trajectory.strokeWidth = parsePositiveNumber(value, line, tokens[0].column, "strokeWidth");
1162
+ return;
1163
+ case "marker":
1164
+ warnIfSchema31Feature(section.sourceSchemaVersion, section.diagnostics, "trajectory.marker", {
1165
+ line,
1166
+ column: tokens[0].column,
1167
+ });
1168
+ section.trajectory.marker = value;
1169
+ return;
1170
+ case "labelmode":
1171
+ warnIfSchema31Feature(section.sourceSchemaVersion, section.diagnostics, "trajectory.labelMode", {
1172
+ line,
1173
+ column: tokens[0].column,
1174
+ });
1175
+ section.trajectory.labelMode = value;
1176
+ return;
1177
+ case "showwaypoints":
1178
+ warnIfSchema31Feature(section.sourceSchemaVersion, section.diagnostics, "trajectory.showWaypoints", {
1179
+ line,
1180
+ column: tokens[0].column,
1181
+ });
1182
+ section.trajectory.showWaypoints = parseAtlasBoolean(value, "showWaypoints", {
1183
+ line,
1184
+ column: tokens[0].column,
1185
+ });
1186
+ return;
1127
1187
  case "hidden":
1128
1188
  section.trajectory.hidden = parseAtlasBoolean(value, "hidden", {
1129
1189
  line,
@@ -1199,6 +1259,37 @@ function applyTrajectorySegmentField(section, tokens, line) {
1199
1259
  case "energy":
1200
1260
  target.energy = parseAtlasUnitValue(value, { line, column: tokens[0].column });
1201
1261
  return;
1262
+ case "waypointlabel":
1263
+ warnIfSchema31Feature(section.sourceSchemaVersion, section.diagnostics, "segment.waypointLabel", {
1264
+ line,
1265
+ column: tokens[0].column,
1266
+ });
1267
+ target.waypointLabel = value;
1268
+ return;
1269
+ case "waypointdate":
1270
+ warnIfSchema31Feature(section.sourceSchemaVersion, section.diagnostics, "segment.waypointDate", {
1271
+ line,
1272
+ column: tokens[0].column,
1273
+ });
1274
+ target.waypointDate = value;
1275
+ return;
1276
+ case "renderhidden":
1277
+ warnIfSchema31Feature(section.sourceSchemaVersion, section.diagnostics, "segment.renderHidden", {
1278
+ line,
1279
+ column: tokens[0].column,
1280
+ });
1281
+ target.renderHidden = parseAtlasBoolean(value, "renderHidden", {
1282
+ line,
1283
+ column: tokens[0].column,
1284
+ });
1285
+ return;
1286
+ case "sampledensity":
1287
+ warnIfSchema31Feature(section.sourceSchemaVersion, section.diagnostics, "segment.sampleDensity", {
1288
+ line,
1289
+ column: tokens[0].column,
1290
+ });
1291
+ target.sampleDensity = parsePositiveNumber(value, line, tokens[0].column, "sampleDensity");
1292
+ return;
1202
1293
  case "notes":
1203
1294
  target.notes = parseTokenList(tokens.slice(1), line, "notes");
1204
1295
  return;
@@ -1345,7 +1436,8 @@ function parseLayerTokens(tokens, line, sourceSchemaVersion, diagnostics) {
1345
1436
  raw === "events" ||
1346
1437
  raw === "objects" ||
1347
1438
  raw === "labels" ||
1348
- raw === "metadata") {
1439
+ raw === "metadata" ||
1440
+ raw === "trajectories") {
1349
1441
  if (raw === "events" && sourceSchemaVersion && diagnostics) {
1350
1442
  warnIfSchema21Feature(sourceSchemaVersion, diagnostics, "layers.events", {
1351
1443
  line,
@@ -1476,6 +1568,12 @@ function parseInlineObjectFields(tokens, line, objectType, sourceSchemaVersion,
1476
1568
  column: keyToken.column,
1477
1569
  });
1478
1570
  }
1571
+ else if (spec.version === "3.1") {
1572
+ warnIfSchema31Feature(sourceSchemaVersion, diagnostics, keyToken.value, {
1573
+ line,
1574
+ column: keyToken.column,
1575
+ });
1576
+ }
1479
1577
  index++;
1480
1578
  const valueTokens = [];
1481
1579
  if (spec.inlineMode === "single") {
@@ -1534,6 +1632,12 @@ function parseObjectField(tokens, line, objectType, sourceSchemaVersion, diagnos
1534
1632
  column: tokens[0].column,
1535
1633
  });
1536
1634
  }
1635
+ else if (spec.version === "3.1") {
1636
+ warnIfSchema31Feature(sourceSchemaVersion, diagnostics, tokens[0].value, {
1637
+ line,
1638
+ column: tokens[0].column,
1639
+ });
1640
+ }
1537
1641
  const field = {
1538
1642
  type: "field",
1539
1643
  key: tokens[0].value,
@@ -1876,6 +1980,19 @@ function warnIfSchema30Feature(sourceSchemaVersion, diagnostics, featureName, lo
1876
1980
  column: location.column,
1877
1981
  });
1878
1982
  }
1983
+ function warnIfSchema31Feature(sourceSchemaVersion, diagnostics, featureName, location) {
1984
+ if (!isSchemaOlderThan(sourceSchemaVersion, "3.1")) {
1985
+ return;
1986
+ }
1987
+ diagnostics.push({
1988
+ code: "parse.schema31.featureCompatibility",
1989
+ severity: "warning",
1990
+ source: "parse",
1991
+ message: `Feature "${featureName}" requires schema 3.1; parsed in compatibility mode because the document header is "schema ${sourceSchemaVersion}".`,
1992
+ line: location.line,
1993
+ column: location.column,
1994
+ });
1995
+ }
1879
1996
  function isSchemaOlderThan(sourceSchemaVersion, requiredVersion) {
1880
1997
  return schemaVersionRank(sourceSchemaVersion) < schemaVersionRank(requiredVersion);
1881
1998
  }
@@ -1893,8 +2010,10 @@ function schemaVersionRank(version) {
1893
2010
  return 4;
1894
2011
  case "3.0":
1895
2012
  return 5;
1896
- default:
2013
+ case "3.1":
1897
2014
  return 6;
2015
+ default:
2016
+ return 7;
1898
2017
  }
1899
2018
  }
1900
2019
  function preprocessAtlasSource(source) {
@@ -18,15 +18,15 @@ export function upgradeDocumentToV2(document, options = {}) {
18
18
  }
19
19
  return {
20
20
  format: "worldorbit",
21
- version: "3.0",
22
- schemaVersion: "3.0",
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
- trajectories: [],
29
+ trajectories: structuredClone(document.trajectories ?? []),
30
30
  objects: document.objects.map(cloneWorldOrbitObject).map(normalizeLegacyCraftObject),
31
31
  diagnostics,
32
32
  };
@@ -490,7 +490,7 @@ function serializeViewpointLayers(layers) {
490
490
  if (orbitFront !== undefined || orbitBack !== undefined) {
491
491
  tokens.push(orbitFront !== false || orbitBack !== false ? "orbits" : "-orbits");
492
492
  }
493
- for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
493
+ for (const key of ["background", "guides", "relations", "events", "objects", "trajectories", "labels", "metadata"]) {
494
494
  if (layers[key] !== undefined) {
495
495
  tokens.push(layers[key] ? key : `-${key}`);
496
496
  }
@@ -41,11 +41,13 @@ export function formatDocument(document, options = {}) {
41
41
  schema === "2.5" ||
42
42
  schema === "2.6" ||
43
43
  schema === "3.0" ||
44
+ schema === "3.1" ||
44
45
  schema === "2.0-draft" ||
45
46
  document.version === "2.0" ||
46
47
  document.version === "2.1" ||
47
48
  document.version === "2.5" ||
48
49
  document.version === "3.0" ||
50
+ document.version === "3.1" ||
49
51
  document.version === "2.6" ||
50
52
  document.version === "2.0-draft";
51
53
  if (useDraft) {
@@ -61,7 +63,12 @@ export function formatDocument(document, options = {}) {
61
63
  : upgradeDocumentToDraftV2(document);
62
64
  return formatDraftDocument(legacyDraftDocument);
63
65
  }
64
- const atlasDocument = document.version === "2.0" || document.version === "2.1" || document.version === "2.5" || document.version === "2.6" || document.version === "3.0"
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"
65
72
  ? document
66
73
  : document.version === "2.0-draft"
67
74
  ? {
@@ -70,7 +77,13 @@ export function formatDocument(document, options = {}) {
70
77
  schemaVersion: "2.0",
71
78
  }
72
79
  : upgradeDocumentToV2(document);
73
- if ((schema === "2.0" || schema === "2.1" || schema === "2.5" || schema === "2.6" || schema === "3.0") && 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) {
74
87
  return formatAtlasDocument({
75
88
  ...atlasDocument,
76
89
  version: schema,
@@ -534,6 +547,24 @@ function formatAtlasTrajectory(trajectory) {
534
547
  if (trajectory.color) {
535
548
  lines.push(` color ${quoteIfNeeded(trajectory.color)}`);
536
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
+ }
537
568
  if (trajectory.hidden) {
538
569
  lines.push(" hidden true");
539
570
  }
@@ -570,6 +601,14 @@ function formatTrajectorySegmentFields(segment) {
570
601
  ...formatOptionalUnit("phaseAngle", segment.phaseAngle),
571
602
  ...formatOptionalUnit("turnAngle", segment.turnAngle),
572
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
+ : []),
573
612
  ...(segment.notes.length > 0 ? [`notes ${segment.notes.map(quoteIfNeeded).join(" ")}`] : []),
574
613
  ];
575
614
  }
@@ -634,7 +673,7 @@ function formatDraftLayers(layers) {
634
673
  ? "orbits"
635
674
  : "-orbits");
636
675
  }
637
- for (const key of ["background", "guides", "relations", "events", "objects", "labels", "metadata"]) {
676
+ for (const key of ["background", "guides", "relations", "events", "objects", "trajectories", "labels", "metadata"]) {
638
677
  if (layers[key] !== undefined) {
639
678
  tokens.push(layers[key] ? key : `-${key}`);
640
679
  }
@@ -5,11 +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)?|3(?:\.0)?)$/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
12
  const ATLAS_SCHEMA_30_PATTERN = /^schema\s+3(?:\.0)?$/i;
13
+ const ATLAS_SCHEMA_31_PATTERN = /^schema\s+3\.1$/i;
13
14
  const LEGACY_DRAFT_SCHEMA_PATTERN = /^schema\s+2\.0-draft$/i;
14
15
  export function detectWorldOrbitSchemaVersion(source) {
15
16
  for (const line of stripCommentsForSchemaDetection(source).split(/\r?\n/)) {
@@ -32,6 +33,9 @@ export function detectWorldOrbitSchemaVersion(source) {
32
33
  if (ATLAS_SCHEMA_30_PATTERN.test(trimmed)) {
33
34
  return "3.0";
34
35
  }
36
+ if (ATLAS_SCHEMA_31_PATTERN.test(trimmed)) {
37
+ return "3.1";
38
+ }
35
39
  if (ATLAS_SCHEMA_PATTERN.test(trimmed)) {
36
40
  return "2.0";
37
41
  }
@@ -97,7 +101,8 @@ export function loadWorldOrbitSourceWithDiagnostics(source) {
97
101
  schemaVersion === "2.1" ||
98
102
  schemaVersion === "2.5" ||
99
103
  schemaVersion === "2.6" ||
100
- schemaVersion === "3.0") {
104
+ schemaVersion === "3.0" ||
105
+ schemaVersion === "3.1") {
101
106
  return loadAtlasSourceWithDiagnostics(source, schemaVersion);
102
107
  }
103
108
  let ast;
@@ -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[];