worldorbit 3.2.2 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +546 -545
  2. package/dist/browser/core/dist/atlas-edit.js +146 -1
  3. package/dist/browser/core/dist/atlas-validate.js +105 -10
  4. package/dist/browser/core/dist/draft-parse.js +341 -16
  5. package/dist/browser/core/dist/draft.d.ts +2 -1
  6. package/dist/browser/core/dist/draft.js +25 -3
  7. package/dist/browser/core/dist/format.js +86 -4
  8. package/dist/browser/core/dist/index.d.ts +1 -0
  9. package/dist/browser/core/dist/index.js +1 -0
  10. package/dist/browser/core/dist/load.js +7 -2
  11. package/dist/browser/core/dist/normalize.js +1 -0
  12. package/dist/browser/core/dist/scene.js +11 -2
  13. package/dist/browser/core/dist/schema.js +11 -1
  14. package/dist/browser/core/dist/solver.d.ts +26 -0
  15. package/dist/browser/core/dist/solver.js +27 -0
  16. package/dist/browser/core/dist/types.d.ts +57 -3
  17. package/dist/browser/editor/dist/editor.js +844 -719
  18. package/dist/browser/editor/dist/types.d.ts +2 -1
  19. package/dist/browser/viewer/dist/minimap.js +9 -7
  20. package/dist/browser/viewer/dist/render.js +23 -19
  21. package/dist/browser/viewer/dist/runtime-3d.js +2 -0
  22. package/dist/browser/viewer/dist/viewer.js +7 -3
  23. package/dist/obsidian-plugin/README.md +141 -124
  24. package/dist/obsidian-plugin/main.js +31 -31
  25. package/dist/unpkg/core/dist/atlas-edit.js +146 -1
  26. package/dist/unpkg/core/dist/atlas-validate.js +105 -10
  27. package/dist/unpkg/core/dist/draft-parse.js +341 -16
  28. package/dist/unpkg/core/dist/draft.d.ts +2 -1
  29. package/dist/unpkg/core/dist/draft.js +25 -3
  30. package/dist/unpkg/core/dist/format.js +86 -4
  31. package/dist/unpkg/core/dist/index.d.ts +1 -0
  32. package/dist/unpkg/core/dist/index.js +1 -0
  33. package/dist/unpkg/core/dist/load.js +7 -2
  34. package/dist/unpkg/core/dist/normalize.js +1 -0
  35. package/dist/unpkg/core/dist/scene.js +11 -2
  36. package/dist/unpkg/core/dist/schema.js +11 -1
  37. package/dist/unpkg/core/dist/solver.d.ts +26 -0
  38. package/dist/unpkg/core/dist/solver.js +27 -0
  39. package/dist/unpkg/core/dist/types.d.ts +57 -3
  40. package/dist/unpkg/editor/dist/editor.js +844 -719
  41. package/dist/unpkg/editor/dist/types.d.ts +2 -1
  42. package/dist/unpkg/viewer/dist/minimap.js +9 -7
  43. package/dist/unpkg/viewer/dist/render.js +23 -19
  44. package/dist/unpkg/viewer/dist/runtime-3d.js +2 -0
  45. package/dist/unpkg/viewer/dist/viewer.js +7 -3
  46. package/dist/unpkg/worldorbit-core.min.js +10 -10
  47. package/dist/unpkg/worldorbit-editor.min.js +359 -332
  48. package/dist/unpkg/worldorbit-markdown.min.js +28 -28
  49. package/dist/unpkg/worldorbit-viewer.min.js +214 -214
  50. package/dist/unpkg/worldorbit.js +758 -40
  51. package/dist/unpkg/worldorbit.min.js +223 -223
  52. package/package.json +5 -1
  53. package/packages/core/dist/atlas-edit.js +146 -1
  54. package/packages/core/dist/atlas-validate.js +105 -10
  55. package/packages/core/dist/draft-parse.js +341 -16
  56. package/packages/core/dist/draft.d.ts +2 -1
  57. package/packages/core/dist/draft.js +25 -3
  58. package/packages/core/dist/format.js +86 -4
  59. package/packages/core/dist/index.d.ts +1 -0
  60. package/packages/core/dist/index.js +1 -0
  61. package/packages/core/dist/load.js +7 -2
  62. package/packages/core/dist/normalize.js +1 -0
  63. package/packages/core/dist/scene.js +11 -2
  64. package/packages/core/dist/schema.js +11 -1
  65. package/packages/core/dist/solver.d.ts +26 -0
  66. package/packages/core/dist/solver.js +27 -0
  67. package/packages/core/dist/types.d.ts +57 -3
  68. package/packages/editor/dist/editor.js +844 -719
  69. package/packages/editor/dist/types.d.ts +2 -1
  70. package/packages/viewer/dist/minimap.js +9 -7
  71. package/packages/viewer/dist/render.js +23 -19
  72. package/packages/viewer/dist/runtime-3d.js +2 -0
  73. package/packages/viewer/dist/viewer.js +7 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worldorbit",
3
- "version": "3.2.2",
3
+ "version": "4.0.0",
4
4
  "description": "A text-based DSL and parser pipeline for orbital worldbuilding",
5
5
  "type": "module",
6
6
  "main": "./dist/unpkg/worldorbit.esm.js",
@@ -28,6 +28,10 @@
28
28
  "types": "./packages/core/dist/scene.d.ts",
29
29
  "import": "./packages/core/dist/scene.js"
30
30
  },
31
+ "./core/solver": {
32
+ "types": "./packages/core/dist/solver.d.ts",
33
+ "import": "./packages/core/dist/solver.js"
34
+ },
31
35
  "./core/types": {
32
36
  "types": "./packages/core/dist/types.d.ts",
33
37
  "import": "./packages/core/dist/types.js"
@@ -1,5 +1,5 @@
1
1
  import { collectAtlasDiagnostics } from "./atlas-validate.js";
2
- export function createEmptyAtlasDocument(systemId = "WorldOrbit", version = "2.6") {
2
+ export function createEmptyAtlasDocument(systemId = "WorldOrbit", version = "3.0") {
3
3
  return {
4
4
  format: "worldorbit",
5
5
  version,
@@ -30,6 +30,7 @@ export function createEmptyAtlasDocument(systemId = "WorldOrbit", version = "2.6
30
30
  groups: [],
31
31
  relations: [],
32
32
  events: [],
33
+ trajectories: [],
33
34
  objects: [],
34
35
  diagnostics: [],
35
36
  };
@@ -62,6 +63,19 @@ export function listAtlasDocumentPaths(document) {
62
63
  paths.push({ kind: "event-pose", id: event.id, key: pose.objectId });
63
64
  }
64
65
  }
66
+ for (const trajectory of [...document.trajectories].sort(compareIdLike)) {
67
+ paths.push({ kind: "trajectory", id: trajectory.id });
68
+ for (const segment of [...trajectory.segments].sort(compareIdLike)) {
69
+ paths.push({ kind: "trajectory-segment", id: trajectory.id, key: segment.id });
70
+ for (const maneuver of [...segment.maneuvers].sort(compareIdLike)) {
71
+ paths.push({
72
+ kind: "trajectory-maneuver",
73
+ id: trajectory.id,
74
+ key: `${segment.id}:${maneuver.id}`,
75
+ });
76
+ }
77
+ }
78
+ }
65
79
  for (const object of [...document.objects].sort(compareIdLike)) {
66
80
  paths.push({ kind: "object", id: object.id });
67
81
  }
@@ -81,6 +95,12 @@ export function getAtlasDocumentNode(document, path) {
81
95
  return path.id ? findEvent(document, path.id) : null;
82
96
  case "event-pose":
83
97
  return path.id && path.key ? findEventPose(document, path.id, path.key) : null;
98
+ case "trajectory":
99
+ return path.id ? findTrajectory(document, path.id) : null;
100
+ case "trajectory-segment":
101
+ return path.id && path.key ? findTrajectorySegment(document, path.id, path.key) : null;
102
+ case "trajectory-maneuver":
103
+ return path.id && path.key ? findTrajectoryManeuver(document, path.id, path.key) : null;
84
104
  case "object":
85
105
  return path.id ? findObject(document, path.id) : null;
86
106
  case "viewpoint":
@@ -133,6 +153,24 @@ export function upsertAtlasDocumentNode(document, path, value) {
133
153
  }
134
154
  upsertEventPose(next.events, path.id, value);
135
155
  return next;
156
+ case "trajectory":
157
+ if (!path.id) {
158
+ throw new Error('Trajectory updates require an "id" value.');
159
+ }
160
+ upsertById(next.trajectories, value);
161
+ return next;
162
+ case "trajectory-segment":
163
+ if (!path.id || !path.key) {
164
+ throw new Error('Trajectory segment updates require a trajectory "id" and segment "key" value.');
165
+ }
166
+ upsertTrajectorySegment(next.trajectories, path.id, value);
167
+ return next;
168
+ case "trajectory-maneuver":
169
+ if (!path.id || !path.key) {
170
+ throw new Error('Trajectory maneuver updates require a trajectory "id" and maneuver "key" value.');
171
+ }
172
+ upsertTrajectoryManeuver(next.trajectories, path.id, path.key, value);
173
+ return next;
136
174
  case "object":
137
175
  if (!path.id) {
138
176
  throw new Error('Object updates require an "id" value.');
@@ -194,6 +232,30 @@ export function removeAtlasDocumentNode(document, path) {
194
232
  }
195
233
  }
196
234
  return next;
235
+ case "trajectory":
236
+ if (path.id) {
237
+ next.trajectories = next.trajectories.filter((trajectory) => trajectory.id !== path.id);
238
+ }
239
+ return next;
240
+ case "trajectory-segment":
241
+ if (path.id && path.key) {
242
+ const trajectory = findTrajectory(next, path.id);
243
+ if (trajectory) {
244
+ trajectory.segments = trajectory.segments.filter((segment) => segment.id !== path.key);
245
+ }
246
+ }
247
+ return next;
248
+ case "trajectory-maneuver":
249
+ if (path.id && path.key) {
250
+ const maneuver = splitTrajectoryManeuverKey(path.key);
251
+ if (maneuver) {
252
+ const segment = findTrajectorySegment(next, path.id, maneuver.segmentId);
253
+ if (segment) {
254
+ segment.maneuvers = segment.maneuvers.filter((entry) => entry.id !== maneuver.maneuverId);
255
+ }
256
+ }
257
+ }
258
+ return next;
197
259
  case "viewpoint":
198
260
  if (path.id) {
199
261
  system.viewpoints = system.viewpoints.filter((viewpoint) => viewpoint.id !== path.id);
@@ -278,6 +340,31 @@ export function resolveAtlasDiagnosticPath(document, diagnostic) {
278
340
  };
279
341
  }
280
342
  }
343
+ if (diagnostic.field?.startsWith("trajectory.")) {
344
+ const parts = diagnostic.field.split(".");
345
+ if (parts[1] && findTrajectory(document, parts[1])) {
346
+ if (parts[2] === "segment" && parts[3] && findTrajectorySegment(document, parts[1], parts[3])) {
347
+ if (parts[4] === "maneuver" &&
348
+ parts[5] &&
349
+ findTrajectoryManeuver(document, parts[1], `${parts[3]}:${parts[5]}`)) {
350
+ return {
351
+ kind: "trajectory-maneuver",
352
+ id: parts[1],
353
+ key: `${parts[3]}:${parts[5]}`,
354
+ };
355
+ }
356
+ return {
357
+ kind: "trajectory-segment",
358
+ id: parts[1],
359
+ key: parts[3],
360
+ };
361
+ }
362
+ return {
363
+ kind: "trajectory",
364
+ id: parts[1],
365
+ };
366
+ }
367
+ }
281
368
  if (diagnostic.field && diagnostic.field in ensureSystem(document).atlasMetadata) {
282
369
  return {
283
370
  kind: "metadata",
@@ -315,6 +402,20 @@ function findEvent(document, eventId) {
315
402
  function findEventPose(document, eventId, objectId) {
316
403
  return findEvent(document, eventId)?.positions.find((pose) => pose.objectId === objectId) ?? null;
317
404
  }
405
+ function findTrajectory(document, trajectoryId) {
406
+ return document.trajectories.find((trajectory) => trajectory.id === trajectoryId) ?? null;
407
+ }
408
+ function findTrajectorySegment(document, trajectoryId, segmentId) {
409
+ return findTrajectory(document, trajectoryId)?.segments.find((segment) => segment.id === segmentId) ?? null;
410
+ }
411
+ function findTrajectoryManeuver(document, trajectoryId, combinedKey) {
412
+ const parsed = splitTrajectoryManeuverKey(combinedKey);
413
+ if (!parsed) {
414
+ return null;
415
+ }
416
+ return findTrajectorySegment(document, trajectoryId, parsed.segmentId)
417
+ ?.maneuvers.find((maneuver) => maneuver.id === parsed.maneuverId) ?? null;
418
+ }
318
419
  function findViewpoint(system, viewpointId) {
319
420
  return system?.viewpoints.find((viewpoint) => viewpoint.id === viewpointId) ?? null;
320
421
  }
@@ -343,6 +444,50 @@ function upsertEventPose(events, eventId, value) {
343
444
  }
344
445
  event.positions[index] = value;
345
446
  }
447
+ function upsertTrajectorySegment(trajectories, trajectoryId, value) {
448
+ const trajectory = trajectories.find((entry) => entry.id === trajectoryId);
449
+ if (!trajectory) {
450
+ throw new Error(`Unknown trajectory "${trajectoryId}" for segment update.`);
451
+ }
452
+ const index = trajectory.segments.findIndex((entry) => entry.id === value.id);
453
+ if (index === -1) {
454
+ trajectory.segments.push(value);
455
+ trajectory.segments.sort(compareIdLike);
456
+ return;
457
+ }
458
+ trajectory.segments[index] = value;
459
+ }
460
+ function upsertTrajectoryManeuver(trajectories, trajectoryId, combinedKey, value) {
461
+ const parsed = splitTrajectoryManeuverKey(combinedKey);
462
+ if (!parsed) {
463
+ throw new Error(`Invalid trajectory maneuver key "${combinedKey}".`);
464
+ }
465
+ const trajectory = trajectories.find((entry) => entry.id === trajectoryId);
466
+ if (!trajectory) {
467
+ throw new Error(`Unknown trajectory "${trajectoryId}" for maneuver update.`);
468
+ }
469
+ const segment = trajectory.segments.find((entry) => entry.id === parsed.segmentId);
470
+ if (!segment) {
471
+ throw new Error(`Unknown trajectory segment "${parsed.segmentId}" on "${trajectoryId}".`);
472
+ }
473
+ const index = segment.maneuvers.findIndex((entry) => entry.id === value.id);
474
+ if (index === -1) {
475
+ segment.maneuvers.push(value);
476
+ segment.maneuvers.sort(compareIdLike);
477
+ return;
478
+ }
479
+ segment.maneuvers[index] = value;
480
+ }
481
+ function splitTrajectoryManeuverKey(key) {
482
+ const separator = key.indexOf(":");
483
+ if (separator <= 0 || separator >= key.length - 1) {
484
+ return null;
485
+ }
486
+ return {
487
+ segmentId: key.slice(0, separator),
488
+ maneuverId: key.slice(separator + 1),
489
+ };
490
+ }
346
491
  function compareIdLike(left, right) {
347
492
  return left.id.localeCompare(right.id);
348
493
  }
@@ -12,6 +12,7 @@ export function collectAtlasDiagnostics(document, sourceSchemaVersion) {
12
12
  const objectMap = new Map(document.objects.map((object) => [object.id, object]));
13
13
  const groupIds = new Set(document.groups.map((group) => group.id));
14
14
  const eventIds = new Set(document.events.map((event) => event.id));
15
+ const trajectoryMap = new Map(document.trajectories.map((trajectory) => [trajectory.id, trajectory]));
15
16
  if (!document.system) {
16
17
  diagnostics.push(error("validate.system.required", "Atlas documents must declare exactly one system."));
17
18
  }
@@ -22,6 +23,7 @@ export function collectAtlasDiagnostics(document, sourceSchemaVersion) {
22
23
  ["annotation", document.system?.annotations.map((annotation) => annotation.id) ?? []],
23
24
  ["relation", document.relations.map((relation) => relation.id)],
24
25
  ["event", document.events.map((event) => event.id)],
26
+ ["trajectory", document.trajectories.map((trajectory) => trajectory.id)],
25
27
  ["object", document.objects.map((object) => object.id)],
26
28
  ]) {
27
29
  for (const id of ids) {
@@ -41,10 +43,13 @@ export function collectAtlasDiagnostics(document, sourceSchemaVersion) {
41
43
  validateViewpoint(viewpoint, groupIds, eventIds, sourceSchemaVersion, diagnostics, objectMap);
42
44
  }
43
45
  for (const object of document.objects) {
44
- validateObject(object, document.system, objectMap, groupIds, diagnostics);
46
+ validateObject(object, document.system, objectMap, groupIds, trajectoryMap, diagnostics);
45
47
  }
46
48
  for (const event of document.events) {
47
- validateEvent(event, document.system, objectMap, diagnostics);
49
+ validateEvent(event, document.system, objectMap, trajectoryMap, diagnostics);
50
+ }
51
+ for (const trajectory of document.trajectories) {
52
+ validateTrajectory(trajectory, objectMap, diagnostics);
48
53
  }
49
54
  return diagnostics;
50
55
  }
@@ -84,7 +89,7 @@ function validateViewpoint(viewpoint, groupIds, eventIds, sourceSchemaVersion, d
84
89
  validateProjection(viewpoint.projection, diagnostics, `viewpoint.${viewpoint.id}.projection`, viewpoint.id);
85
90
  validateCamera(viewpoint.camera, viewpoint.projection, viewpoint.rotationDeg, diagnostics, viewpoint.id, viewpoint.focusObjectId, viewpoint.selectedObjectId, filter, objectMap);
86
91
  }
87
- function validateObject(object, system, objectMap, groupIds, diagnostics) {
92
+ function validateObject(object, system, objectMap, groupIds, trajectoryMap, diagnostics) {
88
93
  const placement = object.placement;
89
94
  const orbitPlacement = placement?.mode === "orbit" ? placement : null;
90
95
  const parentObject = placement?.mode === "orbit" ? objectMap.get(placement.target) ?? null : null;
@@ -101,6 +106,14 @@ function validateObject(object, system, objectMap, groupIds, diagnostics) {
101
106
  if (typeof object.referencePlane === "string" && !object.referencePlane.trim()) {
102
107
  diagnostics.push(warn("validate.referencePlane.empty", `Object "${object.id}" defines an empty reference plane string.`, object.id, "referencePlane"));
103
108
  }
109
+ if (object.trajectoryId) {
110
+ if (!trajectoryMap.has(object.trajectoryId)) {
111
+ diagnostics.push(error("validate.trajectory.object.unknown", `Unknown trajectory "${object.trajectoryId}" on "${object.id}".`, object.id, "trajectory"));
112
+ }
113
+ else if (!isTrajectoryCapableObject(object)) {
114
+ diagnostics.push(error("validate.trajectory.object.invalidType", `Only craft or legacy ship-like structures may reference trajectories; found "${object.type}" on "${object.id}".`, object.id, "trajectory"));
115
+ }
116
+ }
104
117
  if (orbitPlacement) {
105
118
  if (!objectMap.has(orbitPlacement.target)) {
106
119
  diagnostics.push(error("validate.orbit.target.unknown", `Unknown placement target "${orbitPlacement.target}" on "${object.id}".`, object.id, "orbit"));
@@ -128,8 +141,8 @@ function validateObject(object, system, objectMap, groupIds, diagnostics) {
128
141
  }
129
142
  }
130
143
  if (placement?.mode === "at") {
131
- if (object.type !== "structure" && object.type !== "phenomenon") {
132
- diagnostics.push(error("validate.at.objectType", `Only structures and phenomena may use "at" placement; found "${object.type}" on "${object.id}".`, object.id, "at"));
144
+ if (object.type !== "craft" && object.type !== "structure" && object.type !== "phenomenon") {
145
+ diagnostics.push(error("validate.at.objectType", `Only craft, structures, and phenomena may use "at" placement; found "${object.type}" on "${object.id}".`, object.id, "at"));
133
146
  }
134
147
  if (!validateAtTarget(object, objectMap, diagnostics)) {
135
148
  diagnostics.push(error("validate.at.target.unknown", `Unknown at-reference target "${placement.target}" on "${object.id}".`, object.id, "at"));
@@ -176,7 +189,7 @@ function validateObject(object, system, objectMap, groupIds, diagnostics) {
176
189
  }
177
190
  }
178
191
  }
179
- function validateEvent(event, system, objectMap, diagnostics) {
192
+ function validateEvent(event, system, objectMap, trajectoryMap, diagnostics) {
180
193
  const fieldPrefix = `event.${event.id}`;
181
194
  const referencedIds = new Set();
182
195
  if (!event.kind.trim()) {
@@ -188,6 +201,9 @@ function validateEvent(event, system, objectMap, diagnostics) {
188
201
  if (typeof event.referencePlane === "string" && !event.referencePlane.trim()) {
189
202
  diagnostics.push(warn("validate.event.referencePlane.empty", `Event "${event.id}" defines an empty reference plane string.`, undefined, `${fieldPrefix}.referencePlane`));
190
203
  }
204
+ if (event.trajectoryId && !trajectoryMap.has(event.trajectoryId)) {
205
+ diagnostics.push(error("validate.event.trajectory.unknown", `Unknown trajectory "${event.trajectoryId}" on event "${event.id}".`, undefined, `${fieldPrefix}.trajectory`));
206
+ }
191
207
  if (!event.targetObjectId && event.participantObjectIds.length === 0) {
192
208
  diagnostics.push(error("validate.event.references.required", `Event "${event.id}" must define a "target" or at least one participant.`, undefined, `${fieldPrefix}.participants`));
193
209
  }
@@ -236,19 +252,25 @@ function validateEvent(event, system, objectMap, diagnostics) {
236
252
  if (!referencedIds.has(pose.objectId)) {
237
253
  diagnostics.push(warn("validate.event.pose.unreferenced", `Event pose "${pose.objectId}" on "${event.id}" is not listed in target/participants.`, undefined, poseFieldPrefix));
238
254
  }
239
- validateEventPose(pose, object, event, system, objectMap, diagnostics, poseFieldPrefix, event.id);
255
+ validateEventPose(pose, object, event, system, objectMap, trajectoryMap, diagnostics, poseFieldPrefix, event.id);
240
256
  }
241
257
  const missingPoseIds = [...referencedIds].filter((objectId) => !poseIds.has(objectId));
242
258
  if (event.positions.length > 0 && missingPoseIds.length > 0) {
243
259
  diagnostics.push(warn("validate.event.positions.partial", `Event "${event.id}" leaves ${missingPoseIds.length} referenced object(s) on their base placement.`, undefined, `${fieldPrefix}.positions`));
244
260
  }
245
261
  }
246
- function validateEventPose(pose, object, event, system, objectMap, diagnostics, fieldPrefix, eventId) {
262
+ function validateEventPose(pose, object, event, system, objectMap, trajectoryMap, diagnostics, fieldPrefix, eventId) {
247
263
  const placement = pose.placement;
248
264
  if (!placement) {
249
265
  diagnostics.push(error("validate.event.pose.placement.required", `Event "${eventId}" pose "${pose.objectId}" is missing a placement mode.`, undefined, fieldPrefix));
250
266
  return;
251
267
  }
268
+ if (pose.trajectorySegmentId && !findTrajectorySegment(trajectoryMap, pose.trajectorySegmentId)) {
269
+ diagnostics.push(error("validate.event.pose.segment.unknown", `Unknown trajectory segment "${pose.trajectorySegmentId}" on "${eventId}:${pose.objectId}".`, undefined, `${fieldPrefix}.segment`));
270
+ }
271
+ if (pose.trajectoryManeuverId && !findTrajectoryManeuver(trajectoryMap, pose.trajectoryManeuverId)) {
272
+ diagnostics.push(error("validate.event.pose.maneuver.unknown", `Unknown trajectory maneuver "${pose.trajectoryManeuverId}" on "${eventId}:${pose.objectId}".`, undefined, `${fieldPrefix}.maneuver`));
273
+ }
252
274
  if (placement.mode === "orbit") {
253
275
  if (!objectMap.has(placement.target)) {
254
276
  diagnostics.push(error("validate.event.pose.orbit.target.unknown", `Unknown event orbit target "${placement.target}" on "${eventId}:${pose.objectId}".`, undefined, `${fieldPrefix}.orbit`));
@@ -278,8 +300,8 @@ function validateEventPose(pose, object, event, system, objectMap, diagnostics,
278
300
  return;
279
301
  }
280
302
  if (placement.mode === "at") {
281
- if (object.type !== "structure" && object.type !== "phenomenon") {
282
- diagnostics.push(error("validate.event.pose.at.objectType", `Only structures and phenomena may use "at" placement in events; found "${object.type}" on "${eventId}:${pose.objectId}".`, undefined, `${fieldPrefix}.at`));
303
+ if (object.type !== "craft" && object.type !== "structure" && object.type !== "phenomenon") {
304
+ diagnostics.push(error("validate.event.pose.at.objectType", `Only craft, structures, and phenomena may use "at" placement in events; found "${object.type}" on "${eventId}:${pose.objectId}".`, undefined, `${fieldPrefix}.at`));
283
305
  }
284
306
  const reference = placement.reference;
285
307
  if (reference.kind === "named" && !objectMap.has(reference.name)) {
@@ -298,6 +320,79 @@ function validateEventPose(pose, object, event, system, objectMap, diagnostics,
298
320
  }
299
321
  }
300
322
  }
323
+ function validateTrajectory(trajectory, objectMap, diagnostics) {
324
+ if (trajectory.craftObjectId) {
325
+ const craft = objectMap.get(trajectory.craftObjectId);
326
+ if (!craft) {
327
+ diagnostics.push(error("validate.trajectory.craft.unknown", `Unknown craft "${trajectory.craftObjectId}" on trajectory "${trajectory.id}".`, undefined, `trajectory.${trajectory.id}.craft`));
328
+ }
329
+ else if (!isTrajectoryCapableObject(craft)) {
330
+ diagnostics.push(error("validate.trajectory.craft.invalidType", `Trajectory "${trajectory.id}" targets "${trajectory.craftObjectId}", which is not craft-like.`, undefined, `trajectory.${trajectory.id}.craft`));
331
+ }
332
+ }
333
+ for (const segment of trajectory.segments) {
334
+ validateTrajectorySegment(trajectory.id, segment, objectMap, diagnostics);
335
+ }
336
+ }
337
+ function validateTrajectorySegment(trajectoryId, segment, objectMap, diagnostics) {
338
+ const fieldPrefix = `trajectory.${trajectoryId}.segment.${segment.id}`;
339
+ for (const [field, objectId] of [
340
+ ["from", segment.fromObjectId],
341
+ ["to", segment.toObjectId],
342
+ ["around", segment.aroundObjectId],
343
+ ]) {
344
+ if (objectId && !objectMap.has(objectId)) {
345
+ diagnostics.push(error(`validate.trajectory.segment.${field}.unknown`, `Unknown ${field} object "${objectId}" on trajectory "${trajectoryId}" segment "${segment.id}".`, undefined, `${fieldPrefix}.${field}`));
346
+ }
347
+ }
348
+ if (segment.assist?.objectId && !objectMap.has(segment.assist.objectId)) {
349
+ diagnostics.push(error("validate.trajectory.segment.assist.unknown", `Unknown assist object "${segment.assist.objectId}" on trajectory "${trajectoryId}" segment "${segment.id}".`, undefined, `${fieldPrefix}.assist`));
350
+ }
351
+ if (segment.kind === "flyby" && !segment.assist?.objectId) {
352
+ diagnostics.push(error("validate.trajectory.segment.assist.required", `Trajectory "${trajectoryId}" segment "${segment.id}" is a flyby and requires an "assist" object.`, undefined, `${fieldPrefix}.assist`));
353
+ }
354
+ if ((segment.kind === "capture" || segment.kind === "departure") && !segment.toObjectId && !segment.aroundObjectId) {
355
+ diagnostics.push(error("validate.trajectory.segment.target.required", `Trajectory "${trajectoryId}" segment "${segment.id}" requires a target reference.`, undefined, `${fieldPrefix}.to`));
356
+ }
357
+ for (const maneuver of segment.maneuvers) {
358
+ validateTrajectoryManeuver(trajectoryId, segment.id, maneuver, diagnostics);
359
+ }
360
+ }
361
+ function validateTrajectoryManeuver(trajectoryId, segmentId, maneuver, diagnostics) {
362
+ if (!maneuver.kind.trim()) {
363
+ diagnostics.push(error("validate.trajectory.maneuver.kind.required", `Trajectory "${trajectoryId}" segment "${segmentId}" maneuver "${maneuver.id}" is missing a kind.`, undefined, `trajectory.${trajectoryId}.segment.${segmentId}.maneuver.${maneuver.id}.kind`));
364
+ }
365
+ }
366
+ function isTrajectoryCapableObject(object) {
367
+ if (object.type === "craft") {
368
+ return true;
369
+ }
370
+ if (object.type !== "structure") {
371
+ return false;
372
+ }
373
+ const kind = typeof object.properties.kind === "string" ? object.properties.kind.toLowerCase() : "";
374
+ return kind === "ship" || kind === "probe" || kind === "station";
375
+ }
376
+ function findTrajectorySegment(trajectories, segmentId) {
377
+ for (const trajectory of trajectories.values()) {
378
+ const match = trajectory.segments.find((segment) => segment.id === segmentId);
379
+ if (match) {
380
+ return match;
381
+ }
382
+ }
383
+ return null;
384
+ }
385
+ function findTrajectoryManeuver(trajectories, maneuverId) {
386
+ for (const trajectory of trajectories.values()) {
387
+ for (const segment of trajectory.segments) {
388
+ const match = segment.maneuvers.find((maneuver) => maneuver.id === maneuverId);
389
+ if (match) {
390
+ return match;
391
+ }
392
+ }
393
+ }
394
+ return null;
395
+ }
301
396
  function validateAtTarget(object, objectMap, diagnostics) {
302
397
  const reference = object.placement?.mode === "at" ? object.placement.reference : null;
303
398
  if (!reference) {