@johnmorrisdotca/uchusen-rockets 0.19.1 → 0.20.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 (128) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/LICENSES/Orbiter-MIT.txt +21 -0
  3. package/NOTICE.md +37 -1
  4. package/README.md +13 -6
  5. package/dist/assets/falcon-heavy-test-flight-apart.painted.svg +1 -0
  6. package/dist/assets/falcon-heavy-test-flight-apart.svg +1 -0
  7. package/dist/assets/falcon-heavy-test-flight-apart.thick.svg +1 -0
  8. package/dist/assets/falcon-heavy-test-flight-car.glb +0 -0
  9. package/dist/assets/falcon-heavy-test-flight-car.painted.svg +1 -0
  10. package/dist/assets/falcon-heavy-test-flight-car.svg +1 -0
  11. package/dist/assets/falcon-heavy-test-flight-car.thick.svg +1 -0
  12. package/dist/assets/falcon-heavy-test-flight-centre.glb +0 -0
  13. package/dist/assets/falcon-heavy-test-flight-centre.painted.svg +1 -0
  14. package/dist/assets/falcon-heavy-test-flight-centre.svg +1 -0
  15. package/dist/assets/falcon-heavy-test-flight-centre.thick.svg +1 -0
  16. package/dist/assets/falcon-heavy-test-flight-side-left.glb +0 -0
  17. package/dist/assets/falcon-heavy-test-flight-side-left.painted.svg +1 -0
  18. package/dist/assets/falcon-heavy-test-flight-side-left.svg +1 -0
  19. package/dist/assets/falcon-heavy-test-flight-side-left.thick.svg +1 -0
  20. package/dist/assets/falcon-heavy-test-flight-side-right.glb +0 -0
  21. package/dist/assets/falcon-heavy-test-flight-side-right.painted.svg +1 -0
  22. package/dist/assets/falcon-heavy-test-flight-side-right.svg +1 -0
  23. package/dist/assets/falcon-heavy-test-flight-side-right.thick.svg +1 -0
  24. package/dist/assets/falcon-heavy-test-flight-starman.glb +0 -0
  25. package/dist/assets/falcon-heavy-test-flight-starman.painted.svg +1 -0
  26. package/dist/assets/falcon-heavy-test-flight-starman.svg +1 -0
  27. package/dist/assets/falcon-heavy-test-flight-starman.thick.svg +1 -0
  28. package/dist/assets/falcon-heavy-test-flight.glb +0 -0
  29. package/dist/assets/falcon-heavy-test-flight.painted.svg +1 -0
  30. package/dist/assets/falcon-heavy-test-flight.svg +1 -0
  31. package/dist/assets/falcon-heavy-test-flight.thick.svg +1 -0
  32. package/dist/assets/models/roadster-first-generation-car.glb +0 -0
  33. package/dist/assets/models/roadster-second-generation-2-car.glb +0 -0
  34. package/dist/assets/models/roadster-second-generation-car.glb +0 -0
  35. package/dist/assets/roadster-first-generation-apart.painted.svg +1 -0
  36. package/dist/assets/roadster-first-generation-apart.svg +1 -0
  37. package/dist/assets/roadster-first-generation-apart.thick.svg +1 -0
  38. package/dist/assets/roadster-first-generation-car.glb +0 -0
  39. package/dist/assets/roadster-first-generation-car.painted.svg +1 -0
  40. package/dist/assets/roadster-first-generation-car.svg +1 -0
  41. package/dist/assets/roadster-first-generation-car.thick.svg +1 -0
  42. package/dist/assets/roadster-first-generation-starman.glb +0 -0
  43. package/dist/assets/roadster-first-generation-starman.painted.svg +1 -0
  44. package/dist/assets/roadster-first-generation-starman.svg +1 -0
  45. package/dist/assets/roadster-first-generation-starman.thick.svg +1 -0
  46. package/dist/assets/roadster-first-generation.glb +0 -0
  47. package/dist/assets/roadster-first-generation.painted.svg +1 -0
  48. package/dist/assets/roadster-first-generation.svg +1 -0
  49. package/dist/assets/roadster-first-generation.thick.svg +1 -0
  50. package/dist/assets/roadster-second-generation-2-apart.painted.svg +1 -0
  51. package/dist/assets/roadster-second-generation-2-apart.svg +1 -0
  52. package/dist/assets/roadster-second-generation-2-apart.thick.svg +1 -0
  53. package/dist/assets/roadster-second-generation-2-car.glb +0 -0
  54. package/dist/assets/roadster-second-generation-2-car.painted.svg +1 -0
  55. package/dist/assets/roadster-second-generation-2-car.svg +1 -0
  56. package/dist/assets/roadster-second-generation-2-car.thick.svg +1 -0
  57. package/dist/assets/roadster-second-generation-2-starman.glb +0 -0
  58. package/dist/assets/roadster-second-generation-2-starman.painted.svg +1 -0
  59. package/dist/assets/roadster-second-generation-2-starman.svg +1 -0
  60. package/dist/assets/roadster-second-generation-2-starman.thick.svg +1 -0
  61. package/dist/assets/roadster-second-generation-2.glb +0 -0
  62. package/dist/assets/roadster-second-generation-2.painted.svg +1 -0
  63. package/dist/assets/roadster-second-generation-2.svg +1 -0
  64. package/dist/assets/roadster-second-generation-2.thick.svg +1 -0
  65. package/dist/assets/roadster-second-generation-apart.painted.svg +1 -0
  66. package/dist/assets/roadster-second-generation-apart.svg +1 -0
  67. package/dist/assets/roadster-second-generation-apart.thick.svg +1 -0
  68. package/dist/assets/roadster-second-generation-car.glb +0 -0
  69. package/dist/assets/roadster-second-generation-car.painted.svg +1 -0
  70. package/dist/assets/roadster-second-generation-car.svg +1 -0
  71. package/dist/assets/roadster-second-generation-car.thick.svg +1 -0
  72. package/dist/assets/roadster-second-generation-starman.glb +0 -0
  73. package/dist/assets/roadster-second-generation-starman.painted.svg +1 -0
  74. package/dist/assets/roadster-second-generation-starman.svg +1 -0
  75. package/dist/assets/roadster-second-generation-starman.thick.svg +1 -0
  76. package/dist/assets/roadster-second-generation.glb +0 -0
  77. package/dist/assets/roadster-second-generation.painted.svg +1 -0
  78. package/dist/assets/roadster-second-generation.svg +1 -0
  79. package/dist/assets/roadster-second-generation.thick.svg +1 -0
  80. package/dist/data/countries.d.ts +51 -0
  81. package/dist/data/countries.js +91 -0
  82. package/dist/data/credits.d.ts +36 -0
  83. package/dist/data/credits.js +36 -0
  84. package/dist/data/falcon.js +26 -14
  85. package/dist/data/makers.js +1 -0
  86. package/dist/data/roadster.d.ts +13 -0
  87. package/dist/data/roadster.js +73 -0
  88. package/dist/data/sources.d.ts +56 -0
  89. package/dist/data/sources.js +7 -0
  90. package/dist/element/flag.d.ts +30 -0
  91. package/dist/element/flag.js +23 -0
  92. package/dist/element/scene.d.ts +7 -0
  93. package/dist/element/scene.js +35 -1
  94. package/dist/element.d.ts +5 -0
  95. package/dist/element.js +58 -2
  96. package/dist/index.d.ts +3 -0
  97. package/dist/index.js +2 -0
  98. package/dist/inspace.js +1 -0
  99. package/dist/model/registry.js +6 -4
  100. package/dist/model/roadster.d.ts +31 -0
  101. package/dist/model/roadster.js +112 -0
  102. package/dist/parts.d.ts +1 -1
  103. package/dist/physics/atmosphere.d.ts +182 -0
  104. package/dist/physics/atmosphere.js +246 -0
  105. package/dist/physics/attitude.d.ts +342 -0
  106. package/dist/physics/attitude.js +351 -0
  107. package/dist/physics/burns.d.ts +194 -0
  108. package/dist/physics/burns.js +157 -0
  109. package/dist/physics/elements.d.ts +133 -0
  110. package/dist/physics/elements.js +239 -0
  111. package/dist/physics/gravity.d.ts +53 -0
  112. package/dist/physics/gravity.js +78 -0
  113. package/dist/physics/index.d.ts +17 -0
  114. package/dist/physics/index.js +8 -0
  115. package/dist/physics/propagate.d.ts +110 -0
  116. package/dist/physics/propagate.js +122 -0
  117. package/dist/physics/rocket.d.ts +105 -0
  118. package/dist/physics/rocket.js +102 -0
  119. package/dist/physics/transfers.d.ts +128 -0
  120. package/dist/physics/transfers.js +155 -0
  121. package/dist/physics/vector.d.ts +20 -0
  122. package/dist/physics/vector.js +9 -0
  123. package/dist/react.d.ts +1 -1
  124. package/dist/rockets.d.ts +5 -0
  125. package/dist/rockets.js +9 -4
  126. package/dist/sun.d.ts +13 -0
  127. package/dist/sun.js +25 -0
  128. package/package.json +11 -1
@@ -0,0 +1,157 @@
1
+ import { propagate } from "./propagate.js";
2
+ import { burnSeconds, deltaV, massFlow } from "./rocket.js";
3
+ import { addScaled, cross, length, scale, sub } from "./vector.js";
4
+ const unit = (v) => {
5
+ const l = length(v);
6
+ if (!(l > 0))
7
+ throw new RangeError("a direction needs a length");
8
+ return scale(v, 1 / l);
9
+ };
10
+ /**
11
+ * The unit vector a direction points along, for a craft at a position and speed relative to the body it orbits.
12
+ * @param direction The direction.
13
+ * @param position The craft's position from the body's centre, metres.
14
+ * @param velocity The craft's speed relative to the body, metres a second.
15
+ * @returns A unit vector in the frame.
16
+ * @example
17
+ * ```ts
18
+ * import { directionOf } from "@johnmorrisdotca/uchusen-rockets/physics";
19
+ *
20
+ * console.log(directionOf("normal", [7_000_000, 0, 0], [0, 7_500, 0]));
21
+ * ```
22
+ */
23
+ export function directionOf(direction, position, velocity) {
24
+ switch (direction) {
25
+ case "prograde":
26
+ return unit(velocity);
27
+ case "retrograde":
28
+ return scale(unit(velocity), -1);
29
+ case "normal":
30
+ return unit(cross(position, velocity));
31
+ case "antinormal":
32
+ return scale(unit(cross(position, velocity)), -1);
33
+ case "radial-out":
34
+ return unit(position);
35
+ case "radial-in":
36
+ return scale(unit(position), -1);
37
+ default:
38
+ return unit(direction);
39
+ }
40
+ }
41
+ const ORIGIN = () => ({ position: [0, 0, 0], velocity: [0, 0, 0] });
42
+ /**
43
+ * A change of speed all at once (an impulsive burn), as a planning tool treats a short burn.
44
+ * @param state The craft before the burn.
45
+ * @param dv The change of speed, metres a second.
46
+ * @param direction Which way.
47
+ * @param centre Where the body the craft orbits is; left out, at the origin and still.
48
+ * @returns The craft just after: the same place and time, the new speed.
49
+ * @example
50
+ * ```ts
51
+ * import { elementsOf, impulse } from "@johnmorrisdotca/uchusen-rockets/physics";
52
+ *
53
+ * const after = impulse({ t: 0, position: [6_778_000, 0, 0], velocity: [0, 7_669, 0] }, 2_400, "prograde");
54
+ * console.log(((elementsOf(3.986004418e14, after.position, after.velocity).apoapsis - 6_378_137) / 1000).toFixed(0), "km apogee");
55
+ * ```
56
+ */
57
+ export function impulse(state, dv, direction, centre = ORIGIN) {
58
+ const c = centre(state.t);
59
+ const along = directionOf(direction, sub(state.position, c.position), sub(state.velocity, c.velocity));
60
+ return { ...state, velocity: addScaled(state.velocity, along, dv) };
61
+ }
62
+ /**
63
+ * When to start a burn so that it is centred on a planned point, as Orbiter's plane-alignment display times one: the point's time less half the burn.
64
+ * @param centreTime When the planned point is reached, seconds.
65
+ * @param seconds How long the burn lasts, seconds.
66
+ * @returns When to start, seconds.
67
+ * @example
68
+ * ```ts
69
+ * import { burnStart } from "@johnmorrisdotca/uchusen-rockets/physics";
70
+ *
71
+ * console.log(burnStart(1_000, 120));
72
+ * ```
73
+ */
74
+ export const burnStart = (centreTime, seconds) => centreTime - seconds / 2;
75
+ /**
76
+ * Flies one burn from a state: gravity and the engine's thrust together, the mass falling at the engine's flow, the thrust kept along its direction as the craft moves.
77
+ * @param start The craft when the engine lights.
78
+ * @param mass Its mass then, kilograms.
79
+ * @param burn The engine, the direction, and how long or how much.
80
+ * @param options The step, the gravity, the centre and the dry mass.
81
+ * @returns Where the craft is when the engine stops, and what the burn did.
82
+ * @throws {RangeError} When the burn has no length, or would use all the mass with no `dryMass` above zero.
83
+ * @example
84
+ * ```ts
85
+ * import { exhaustSpeedOf, flyBurn, gravityAt } from "@johnmorrisdotca/uchusen-rockets/physics";
86
+ *
87
+ * const earth = [{ gm: 3.986004418e14, position: [0, 0, 0] as const }];
88
+ * const raise = flyBurn({ t: 0, position: [6_778_000, 0, 0], velocity: [0, 7_669, 0] }, 20_000, { engine: { thrust: 91_189, exhaustSpeed: exhaustSpeedOf(314) }, direction: "prograde", dv: 100 }, { step: 1, gravity: (_t, p) => gravityAt(p, earth) });
89
+ * console.log(raise.seconds.toFixed(1), "s,", raise.mass.toFixed(0), "kg left");
90
+ * ```
91
+ */
92
+ export function flyBurn(start, mass, burn, options) {
93
+ const throttle = burn.throttle ?? 1;
94
+ const flow = massFlow(burn.engine, throttle);
95
+ const thrust = flow * burn.engine.exhaustSpeed;
96
+ let seconds = burn.seconds ?? (burn.dv !== undefined ? burnSeconds(burn.engine, mass, burn.dv, throttle) : NaN);
97
+ if (!(seconds >= 0) || !Number.isFinite(seconds))
98
+ throw new RangeError("a burn needs seconds, or a dv and an engine that is lit");
99
+ const reserve = options.dryMass ?? 0;
100
+ const longest = flow > 0 ? (mass - reserve) / flow : Infinity;
101
+ // With nothing held back, a burn that empties the craft has no finite answer: the thrust over the mass grows without limit, and so does the change of speed.
102
+ if (!(reserve > 0) && seconds >= longest)
103
+ throw new RangeError("a burn that uses all the mass has no finite change of speed: give a dryMass above zero, and it stops there");
104
+ const short = seconds > longest;
105
+ if (short)
106
+ seconds = Math.max(0, longest);
107
+ const centre = options.centre ?? ORIGIN;
108
+ const t0 = start.t;
109
+ const acceleration = (t, p, v) => {
110
+ const c = centre(t);
111
+ const push = directionOf(burn.direction, sub(p, c.position), sub(v, c.velocity));
112
+ return addScaled(options.gravity(t, p, v), push, thrust / (mass - flow * (t - t0)));
113
+ };
114
+ const state = seconds > 0 ? propagate(start, seconds, { step: options.step, method: options.burnMethod ?? "rk4", acceleration }) : start;
115
+ const end = mass - flow * seconds;
116
+ return { state, mass: end, seconds, dv: deltaV(burn.engine.exhaustSpeed, mass, end), propellant: mass - end, short };
117
+ }
118
+ /**
119
+ * Flies a plan: coasts to each burn, starting it so that its middle falls at its planned time, flies it, and coasts on to the end.
120
+ * @param start The craft at the start.
121
+ * @param mass Its mass then, kilograms.
122
+ * @param burns The planned burns, in any order; they must not overlap.
123
+ * @param end The time to stop, seconds (after the last burn).
124
+ * @param options The step, the gravity, the centre, the dry mass and the methods.
125
+ * @returns The craft at `end`, its mass, and what each burn did, in time order.
126
+ * @example
127
+ * ```ts
128
+ * import { elementsOf, exhaustSpeedOf, flyPlan, gravityAt } from "@johnmorrisdotca/uchusen-rockets/physics";
129
+ *
130
+ * const earth = [{ gm: 3.986004418e14, position: [0, 0, 0] as const }];
131
+ * const engine = { thrust: 91_189, exhaustSpeed: exhaustSpeedOf(314) };
132
+ * const flown = flyPlan({ t: 0, position: [6_778_000, 0, 0], velocity: [0, 7_669, 0] }, 20_000, [{ at: 600, engine, direction: "prograde", dv: 100 }], 3_600, { step: 5, gravity: (_t, p) => gravityAt(p, earth) });
133
+ * console.log(((elementsOf(3.986004418e14, flown.state.position, flown.state.velocity).apoapsis - 6_378_137) / 1000).toFixed(0), "km apogee");
134
+ * ```
135
+ */
136
+ export function flyPlan(start, mass, burns, end, options) {
137
+ let state = start;
138
+ let left = mass;
139
+ const done = [];
140
+ const coast = (to) => {
141
+ if (to < state.t - 1e-9)
142
+ throw new RangeError(`the plan goes back in time at ${to} s: burns overlap, or one starts before the craft is there`);
143
+ if (to > state.t)
144
+ state = propagate(state, to - state.t, { step: options.step, method: options.coastMethod ?? "sy4", acceleration: options.gravity });
145
+ };
146
+ for (const burn of [...burns].sort((a, b) => a.at - b.at)) {
147
+ const lit = burn.engine;
148
+ const seconds = burn.seconds ?? burnSeconds(lit, left, burn.dv ?? NaN, burn.throttle ?? 1);
149
+ coast(burnStart(burn.at, seconds));
150
+ const result = flyBurn(state, left, { ...burn, seconds }, options);
151
+ done.push(result);
152
+ state = result.state;
153
+ left = result.mass;
154
+ }
155
+ coast(end);
156
+ return { state, mass: left, burns: done };
157
+ }
@@ -0,0 +1,133 @@
1
+ import { type Vec3 } from "./vector.ts";
2
+ /**
3
+ * A position and a speed, in metres and metres a second, in one frame.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * import type { StateVector } from "@johnmorrisdotca/uchusen-rockets/physics";
8
+ *
9
+ * const leo: StateVector = { position: [6_778_000, 0, 0], velocity: [0, 7_669, 0] };
10
+ * console.log(leo.velocity[1]);
11
+ * ```
12
+ */
13
+ export interface StateVector {
14
+ position: Vec3;
15
+ velocity: Vec3;
16
+ }
17
+ /**
18
+ * The shape of an orbit and where on it a craft is, worked out from one position and speed (the osculating elements). Angles in radians, lengths in metres, times in seconds.
19
+ *
20
+ * @example
21
+ * ```ts
22
+ * import { elementsOf, type Elements } from "@johnmorrisdotca/uchusen-rockets/physics";
23
+ *
24
+ * const orbit: Elements = elementsOf(3.986004418e14, [7_000_000, 0, 0], [0, 7_546, 0]);
25
+ * console.log(orbit.e.toFixed(4), (orbit.period / 60).toFixed(1));
26
+ * ```
27
+ */
28
+ export interface Elements {
29
+ /** The gravitational parameter the elements were worked with, m^3/s^2. */
30
+ gm: number;
31
+ /** Semi-major axis, metres: negative for an open (hyperbolic) path. */
32
+ a: number;
33
+ /** Eccentricity: 0 a circle, below 1 an ellipse, above 1 a hyperbola. */
34
+ e: number;
35
+ /** Inclination to the frame's x-y plane, 0 to pi. */
36
+ i: number;
37
+ /** Longitude of the ascending node, 0 to 2 pi (0 for an orbit in the plane). */
38
+ node: number;
39
+ /** Argument of periapsis, from the node, 0 to 2 pi (0 for a circle). */
40
+ argPeriapsis: number;
41
+ /** Longitude of periapsis: the node plus the argument of periapsis. */
42
+ longitudePeriapsis: number;
43
+ /** True anomaly now: the angle from periapsis to the craft. */
44
+ trueAnomaly: number;
45
+ /** Eccentric anomaly now (the hyperbolic anomaly on an open path). */
46
+ eccentricAnomaly: number;
47
+ /** Mean anomaly now. */
48
+ meanAnomaly: number;
49
+ /** Mean motion, radians a second. */
50
+ meanMotion: number;
51
+ /** Time once round, seconds; Infinity for an open path. */
52
+ period: number;
53
+ /** Distance of periapsis from the centre, metres. */
54
+ periapsis: number;
55
+ /** Distance of apoapsis from the centre, metres; Infinity for an open path. */
56
+ apoapsis: number;
57
+ /** Semi-latus rectum `a (1 - e^2)`, metres. */
58
+ semiLatus: number;
59
+ /** Seconds to the next periapsis (for an open path, to the one periapsis; negative once it is passed). */
60
+ timeToPeriapsis: number;
61
+ }
62
+ /**
63
+ * The eccentric anomaly from the mean anomaly: Kepler's equation `M = E - e sin E` for an ellipse, `M = e sinh E - E` for a hyperbola, solved by Newton's method with each step held to one radian, as Orbiter does.
64
+ * @param meanAnomaly The mean anomaly, radians.
65
+ * @param e The eccentricity, 0 or more (not exactly 1).
66
+ * @returns The eccentric anomaly (the hyperbolic anomaly when `e` is above 1), radians.
67
+ * @example
68
+ * ```ts
69
+ * import { eccentricAnomaly } from "@johnmorrisdotca/uchusen-rockets/physics";
70
+ *
71
+ * console.log(eccentricAnomaly(1, 0.2).toFixed(6), eccentricAnomaly(2, 1.5).toFixed(6));
72
+ * ```
73
+ */
74
+ export declare function eccentricAnomaly(meanAnomaly: number, e: number): number;
75
+ /**
76
+ * The orbital elements of a craft about one body, from its position and speed relative to that body (Orbiter's `Elements::Calculate`).
77
+ * @param gm The gravitational parameter of the body (plus the craft's, which is nothing for a craft), m^3/s^2.
78
+ * @param position The craft's position from the body's centre, metres.
79
+ * @param velocity The craft's speed relative to the body, metres a second.
80
+ * @returns The elements: shape, plane, where the craft is on it, its period and the time to periapsis.
81
+ * @example
82
+ * ```ts
83
+ * import { elementsOf } from "@johnmorrisdotca/uchusen-rockets/physics";
84
+ *
85
+ * const gto = elementsOf(3.986004418e14, [6_563_000, 0, 0], [0, 10_240, 0]);
86
+ * console.log(((gto.apoapsis - 6_378_137) / 1000).toFixed(0), "km apogee");
87
+ * ```
88
+ */
89
+ export declare function elementsOf(gm: number, position: Vec3, velocity: Vec3): Elements;
90
+ /**
91
+ * Where a craft on a two-body orbit is a time after its elements were taken, and its speed (Orbiter's `Elements::PosVel`): Kepler's equation, with no other pull.
92
+ * @param elements The elements, from {@link elementsOf}.
93
+ * @param seconds The time since the elements were taken, seconds (negative for before).
94
+ * @returns The position from the body's centre (metres) and the speed (metres a second), in the frame of the elements.
95
+ * @example
96
+ * ```ts
97
+ * import { elementsOf, stateOf } from "@johnmorrisdotca/uchusen-rockets/physics";
98
+ *
99
+ * const orbit = elementsOf(3.986004418e14, [7_000_000, 0, 0], [0, 7_546, 0]);
100
+ * const later = stateOf(orbit, orbit.period / 2);
101
+ * console.log((later.position[0] / 1000).toFixed(0), "km");
102
+ * ```
103
+ */
104
+ export declare function stateOf(elements: Elements, seconds: number): StateVector;
105
+ /**
106
+ * Where a craft is, and its speed, when it is at a given true anomaly on its orbit (Orbiter's `Elements::PosVel_TA`).
107
+ * @param elements The elements, from {@link elementsOf}.
108
+ * @param trueAnomaly The angle from periapsis, radians (on an open path, only angles the path reaches).
109
+ * @returns The position from the body's centre (metres) and the speed (metres a second).
110
+ * @example
111
+ * ```ts
112
+ * import { elementsOf, stateAtTrueAnomaly } from "@johnmorrisdotca/uchusen-rockets/physics";
113
+ *
114
+ * const orbit = elementsOf(3.986004418e14, [6_563_000, 0, 0], [0, 10_240, 0]);
115
+ * const apogee = stateAtTrueAnomaly(orbit, Math.PI);
116
+ * console.log((apogee.position[0] / 1000).toFixed(0), "km,", Math.hypot(...apogee.velocity).toFixed(0), "m/s");
117
+ * ```
118
+ */
119
+ export declare function stateAtTrueAnomaly(elements: Elements, trueAnomaly: number): StateVector;
120
+ /**
121
+ * How long until a craft reaches a true anomaly on its orbit: Kepler's equation run backwards, as Orbiter's plane-alignment display times the nodes.
122
+ * @param elements The elements, from {@link elementsOf}.
123
+ * @param trueAnomaly The angle from periapsis to reach, radians.
124
+ * @returns Seconds from the time the elements were taken: 0 to one period on a closed orbit (the next time it is there); on an open path, negative if that point is already behind.
125
+ * @example
126
+ * ```ts
127
+ * import { elementsOf, timeToTrueAnomaly } from "@johnmorrisdotca/uchusen-rockets/physics";
128
+ *
129
+ * const orbit = elementsOf(3.986004418e14, [6_563_000, 0, 0], [0, 10_240, 0]);
130
+ * console.log((timeToTrueAnomaly(orbit, Math.PI) / 3600).toFixed(2), "hours to apogee");
131
+ * ```
132
+ */
133
+ export declare function timeToTrueAnomaly(elements: Elements, trueAnomaly: number): number;
@@ -0,0 +1,239 @@
1
+ // Orbital elements from a position and a speed, and back again after a time: the two-body orbit, closed or open. Pure.
2
+ // Ported from Orbiter (https://github.com/orbitersim/orbiter), Src/Orbiter/Element.cpp, class Elements: Calculate, EccAnomaly,
3
+ // TrueAnomaly_from_EccAnomaly, EccAnomaly_from_TrueAnomaly, MeanAnomaly_from_EccAnomaly, RelPos, Pol2Crt, PosVel and PosVel_TA. Copyright (c) 2000-2026 Martin Schweiger, MIT licence (LICENSES/Orbiter-MIT.txt).
4
+ // Changed in the port: a right-handed frame with z as the pole (Orbiter's is left-handed with y as the pole, so its y and z swap
5
+ // and its H = V x R becomes R x V); no cached state; the Kepler solver may take 64 rounds where Orbiter takes 16, so a long
6
+ // hyperbolic path still converges; an orbit in the equator's plane run backwards (i = 180 degrees) takes the equatorial branch,
7
+ // where Orbiter divides by zero; the inclination, the node, the argument of periapsis and the true anomaly are taken with atan2 in place of
8
+ // Orbiter's acos and sign tests (the same angles, without acos's loss of half the digits next to 0 and pi, worth a metre in low orbit
9
+ // and a kilometre round the Sun).
10
+ import { cross, dot, length } from "./vector.js";
11
+ const TAU = 2 * Math.PI;
12
+ const E_CIRCLE_LIMIT = 1e-8;
13
+ const I_NOINC_LIMIT = 1e-8;
14
+ const ROUNDS = 64;
15
+ const TOLERANCE = 1e-14;
16
+ const posAngle = (angle) => {
17
+ const a = angle % TAU;
18
+ return a >= 0 ? a : a + TAU;
19
+ };
20
+ const clampedAcos = (x) => Math.acos(Math.max(-1, Math.min(1, x)));
21
+ const clampStep = (x) => Math.max(-1, Math.min(1, x));
22
+ /**
23
+ * The eccentric anomaly from the mean anomaly: Kepler's equation `M = E - e sin E` for an ellipse, `M = e sinh E - E` for a hyperbola, solved by Newton's method with each step held to one radian, as Orbiter does.
24
+ * @param meanAnomaly The mean anomaly, radians.
25
+ * @param e The eccentricity, 0 or more (not exactly 1).
26
+ * @returns The eccentric anomaly (the hyperbolic anomaly when `e` is above 1), radians.
27
+ * @example
28
+ * ```ts
29
+ * import { eccentricAnomaly } from "@johnmorrisdotca/uchusen-rockets/physics";
30
+ *
31
+ * console.log(eccentricAnomaly(1, 0.2).toFixed(6), eccentricAnomaly(2, 1.5).toFixed(6));
32
+ * ```
33
+ */
34
+ export function eccentricAnomaly(meanAnomaly, e) {
35
+ const ma = meanAnomaly;
36
+ let big = ma;
37
+ if (e < 1) {
38
+ let res = ma - big + e * Math.sin(big);
39
+ if (Math.abs(res) > Math.abs(ma)) {
40
+ big = 0;
41
+ res = ma;
42
+ }
43
+ for (let i = 0; Math.abs(res) > TOLERANCE && i < ROUNDS; i++) {
44
+ big += clampStep(res / (1 - e * Math.cos(big)));
45
+ res = ma - big + e * Math.sin(big);
46
+ }
47
+ }
48
+ else {
49
+ let res = ma - e * Math.sinh(big) + big;
50
+ if (Math.abs(res) > Math.abs(ma)) {
51
+ big = 0;
52
+ res = ma;
53
+ }
54
+ for (let i = 0; Math.abs(res) > TOLERANCE && i < ROUNDS; i++) {
55
+ big += clampStep(res / (e * Math.cosh(big) - 1));
56
+ res = ma - e * Math.sinh(big) + big;
57
+ }
58
+ }
59
+ return big;
60
+ }
61
+ const trueFromEccentric = (ea, e) => {
62
+ if (e < 1)
63
+ return 2 * Math.atan(Math.sqrt((1 + e) / (1 - e)) * Math.tan(0.5 * ea));
64
+ const chea = Math.cosh(ea);
65
+ const tra = clampedAcos((e - chea) / (e * chea - 1));
66
+ return ea >= 0 ? tra : -tra;
67
+ };
68
+ /**
69
+ * The orbital elements of a craft about one body, from its position and speed relative to that body (Orbiter's `Elements::Calculate`).
70
+ * @param gm The gravitational parameter of the body (plus the craft's, which is nothing for a craft), m^3/s^2.
71
+ * @param position The craft's position from the body's centre, metres.
72
+ * @param velocity The craft's speed relative to the body, metres a second.
73
+ * @returns The elements: shape, plane, where the craft is on it, its period and the time to periapsis.
74
+ * @example
75
+ * ```ts
76
+ * import { elementsOf } from "@johnmorrisdotca/uchusen-rockets/physics";
77
+ *
78
+ * const gto = elementsOf(3.986004418e14, [6_563_000, 0, 0], [0, 10_240, 0]);
79
+ * console.log(((gto.apoapsis - 6_378_137) / 1000).toFixed(0), "km apogee");
80
+ * ```
81
+ */
82
+ export function elementsOf(gm, position, velocity) {
83
+ const v2 = dot(velocity, velocity);
84
+ const r = length(position);
85
+ const h = cross(position, velocity);
86
+ const hl = length(h);
87
+ const rv = dot(position, velocity);
88
+ const a = (r * gm) / (2 * gm - r * v2);
89
+ const k = 1 / r - 1 / a;
90
+ const q = rv / gm;
91
+ const ev = [position[0] * k - velocity[0] * q, position[1] * k - velocity[1] * q, position[2] * k - velocity[2] * q];
92
+ const e = length(ev);
93
+ const closed = e < 1;
94
+ const hxy = Math.hypot(h[0], h[1]);
95
+ const i = Math.atan2(hxy, h[2]);
96
+ const hu = [h[0] / hl, h[1] / hl, h[2] / hl];
97
+ /** The angle from `from` to `to` in the orbit's plane, in the direction of motion, 0 to 2 pi. */
98
+ const along = (from, to) => posAngle(Math.atan2(dot(cross(from, to), hu), dot(from, to)));
99
+ const semiLatus = Math.max(0, a * (1 - e * e));
100
+ const meanMotion = closed ? Math.sqrt(gm / (a * a * a)) : Math.sqrt(gm / (-a * a * a));
101
+ const period = closed ? TAU / meanMotion : Infinity;
102
+ const inclined = i > I_NOINC_LIMIT && hxy > I_NOINC_LIMIT * hl;
103
+ const n = inclined ? [-h[1] / hxy, h[0] / hxy, 0] : [1, 0, 0];
104
+ const node = inclined ? posAngle(Math.atan2(n[1], n[0])) : 0;
105
+ let argPeriapsis = 0;
106
+ if (e > E_CIRCLE_LIMIT) {
107
+ if (inclined) {
108
+ argPeriapsis = along(n, ev);
109
+ }
110
+ else {
111
+ argPeriapsis = Math.atan2(ev[1], ev[0]);
112
+ if (argPeriapsis < 0)
113
+ argPeriapsis += TAU;
114
+ }
115
+ }
116
+ let longitudePeriapsis = node + argPeriapsis;
117
+ if (longitudePeriapsis >= TAU)
118
+ longitudePeriapsis -= TAU;
119
+ const trueAnomaly = along(e > E_CIRCLE_LIMIT ? ev : n, position);
120
+ let ea;
121
+ let ma;
122
+ let timeToPeriapsis;
123
+ if (closed) {
124
+ ea = e > E_CIRCLE_LIMIT ? Math.atan2(rv * Math.sqrt(a / gm), a - r) : trueAnomaly;
125
+ ma = ea - e * Math.sin(ea);
126
+ timeToPeriapsis = -ma / meanMotion;
127
+ if (timeToPeriapsis < 0)
128
+ timeToPeriapsis += period;
129
+ }
130
+ else {
131
+ const cosTrue = Math.cos(trueAnomaly);
132
+ ea = Math.acosh((e + cosTrue) / (1 + e * cosTrue));
133
+ if (trueAnomaly >= Math.PI)
134
+ ea = -ea;
135
+ ma = e * Math.sinh(ea) - ea;
136
+ timeToPeriapsis = -ma / meanMotion;
137
+ }
138
+ return {
139
+ gm,
140
+ a,
141
+ e,
142
+ i,
143
+ node,
144
+ argPeriapsis,
145
+ longitudePeriapsis,
146
+ trueAnomaly,
147
+ eccentricAnomaly: ea,
148
+ meanAnomaly: ma,
149
+ meanMotion,
150
+ period,
151
+ periapsis: a - a * e,
152
+ apoapsis: closed ? a + a * e : Infinity,
153
+ semiLatus,
154
+ timeToPeriapsis,
155
+ };
156
+ }
157
+ /** A point at distance `r` and angle `u` from the node, turned into the frame (Orbiter's `Pol2Crt`). */
158
+ function toFrame(el, r, u) {
159
+ const so = Math.sin(u);
160
+ const co = Math.cos(u);
161
+ const st = Math.sin(el.node);
162
+ const ct = Math.cos(el.node);
163
+ const ci = Math.cos(el.i);
164
+ return [r * (ct * co - st * so * ci), r * (st * co + ct * so * ci), r * so * Math.sin(el.i)];
165
+ }
166
+ /**
167
+ * Where a craft on a two-body orbit is a time after its elements were taken, and its speed (Orbiter's `Elements::PosVel`): Kepler's equation, with no other pull.
168
+ * @param elements The elements, from {@link elementsOf}.
169
+ * @param seconds The time since the elements were taken, seconds (negative for before).
170
+ * @returns The position from the body's centre (metres) and the speed (metres a second), in the frame of the elements.
171
+ * @example
172
+ * ```ts
173
+ * import { elementsOf, stateOf } from "@johnmorrisdotca/uchusen-rockets/physics";
174
+ *
175
+ * const orbit = elementsOf(3.986004418e14, [7_000_000, 0, 0], [0, 7_546, 0]);
176
+ * const later = stateOf(orbit, orbit.period / 2);
177
+ * console.log((later.position[0] / 1000).toFixed(0), "km");
178
+ * ```
179
+ */
180
+ export function stateOf(elements, seconds) {
181
+ const { e } = elements;
182
+ const ma = elements.meanAnomaly + elements.meanMotion * seconds;
183
+ const ta = e < E_CIRCLE_LIMIT ? ma : trueFromEccentric(eccentricAnomaly(e < 1 ? posAngle(ma) : ma, e), e);
184
+ return stateAtTrueAnomaly(elements, ta);
185
+ }
186
+ /**
187
+ * Where a craft is, and its speed, when it is at a given true anomaly on its orbit (Orbiter's `Elements::PosVel_TA`).
188
+ * @param elements The elements, from {@link elementsOf}.
189
+ * @param trueAnomaly The angle from periapsis, radians (on an open path, only angles the path reaches).
190
+ * @returns The position from the body's centre (metres) and the speed (metres a second).
191
+ * @example
192
+ * ```ts
193
+ * import { elementsOf, stateAtTrueAnomaly } from "@johnmorrisdotca/uchusen-rockets/physics";
194
+ *
195
+ * const orbit = elementsOf(3.986004418e14, [6_563_000, 0, 0], [0, 10_240, 0]);
196
+ * const apogee = stateAtTrueAnomaly(orbit, Math.PI);
197
+ * console.log((apogee.position[0] / 1000).toFixed(0), "km,", Math.hypot(...apogee.velocity).toFixed(0), "m/s");
198
+ * ```
199
+ */
200
+ export function stateAtTrueAnomaly(elements, trueAnomaly) {
201
+ const { e, gm, semiLatus } = elements;
202
+ const ta = trueAnomaly;
203
+ const r = semiLatus / (1 + e * Math.cos(ta));
204
+ const muh = Math.sqrt(gm / semiLatus);
205
+ const vx = -muh * Math.sin(ta);
206
+ const vz = muh * (e + Math.cos(ta));
207
+ return { position: toFrame(elements, r, ta + elements.argPeriapsis), velocity: toFrame(elements, Math.hypot(vx, vz), Math.atan2(vz, vx) + elements.argPeriapsis) };
208
+ }
209
+ /** The mean anomaly at a true anomaly (Orbiter's `MeanAnomaly_from_TrueAnomaly`, through `EccAnomaly_from_TrueAnomaly` and `MeanAnomaly_from_EccAnomaly`). */
210
+ function meanFromTrue(ta, e) {
211
+ if (e < 1) {
212
+ const ea = 2 * Math.atan(Math.tan(0.5 * ta) / Math.sqrt((1 + e) / (1 - e)));
213
+ return ea - e * Math.sin(ea);
214
+ }
215
+ const t = ta - TAU * Math.round(ta / TAU);
216
+ const cosTa = Math.cos(t);
217
+ const ea = Math.acosh((e + cosTa) / (e * cosTa + 1)) * (t >= 0 ? 1 : -1);
218
+ return e * Math.sinh(ea) - ea;
219
+ }
220
+ /**
221
+ * How long until a craft reaches a true anomaly on its orbit: Kepler's equation run backwards, as Orbiter's plane-alignment display times the nodes.
222
+ * @param elements The elements, from {@link elementsOf}.
223
+ * @param trueAnomaly The angle from periapsis to reach, radians.
224
+ * @returns Seconds from the time the elements were taken: 0 to one period on a closed orbit (the next time it is there); on an open path, negative if that point is already behind.
225
+ * @example
226
+ * ```ts
227
+ * import { elementsOf, timeToTrueAnomaly } from "@johnmorrisdotca/uchusen-rockets/physics";
228
+ *
229
+ * const orbit = elementsOf(3.986004418e14, [6_563_000, 0, 0], [0, 10_240, 0]);
230
+ * console.log((timeToTrueAnomaly(orbit, Math.PI) / 3600).toFixed(2), "hours to apogee");
231
+ * ```
232
+ */
233
+ export function timeToTrueAnomaly(elements, trueAnomaly) {
234
+ const { e, meanMotion } = elements;
235
+ if (e < E_CIRCLE_LIMIT)
236
+ return posAngle(trueAnomaly - elements.meanAnomaly) / meanMotion;
237
+ const dm = meanFromTrue(trueAnomaly, e) - elements.meanAnomaly;
238
+ return e < 1 ? posAngle(dm) / meanMotion : dm / meanMotion;
239
+ }
@@ -0,0 +1,53 @@
1
+ import { type Vec3 } from "./vector.ts";
2
+ /**
3
+ * A body's flattening, as the zonal terms of its gravity field: `J2`, and if given `J3` and `J4`, which is as far as Orbiter reads them.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * import type { Zonal } from "@johnmorrisdotca/uchusen-rockets/physics";
8
+ *
9
+ * const earth: Zonal = { radius: 6_378_137, pole: [0, 0, 1], j: [0.00108262545] };
10
+ * console.log(earth.j[0]);
11
+ * ```
12
+ */
13
+ export interface Zonal {
14
+ /** The reference radius the coefficients are given for (usually the equatorial radius), metres. */
15
+ radius: number;
16
+ /** The body's north pole, a unit vector in the frame. */
17
+ pole: Vec3;
18
+ /** `[J2]`, `[J2, J3]` or `[J2, J3, J4]`, unnormalised. Further terms are ignored. */
19
+ j: readonly number[];
20
+ }
21
+ /**
22
+ * One body that pulls: its gravitational parameter, where it is at the time asked, and its flattening if it matters.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * import type { GravityBody } from "@johnmorrisdotca/uchusen-rockets/physics";
27
+ *
28
+ * const sun: GravityBody = { gm: 1.32712440041279419e20, position: [0, 0, 0] };
29
+ * console.log(sun.gm);
30
+ * ```
31
+ */
32
+ export interface GravityBody {
33
+ /** Gravitational parameter, m^3/s^2. */
34
+ gm: number;
35
+ /** Where the body's centre is, metres, in the craft's frame. */
36
+ position: Vec3;
37
+ zonal?: Zonal;
38
+ }
39
+ /**
40
+ * The acceleration gravity gives a craft at a place: the sum of `gm (body - craft) / d^3` over the bodies, plus each flattened body's zonal terms (Orbiter's `SingleGacc` summed as in `PlanetarySystem::Gacc`).
41
+ * For the best answer give the bodies in a frame that is not itself accelerating, such as the solar system's barycentre; a frame centred on a body that the others pull leaves out that pull on the frame.
42
+ * @param position The craft's position, metres.
43
+ * @param bodies The bodies that pull, at the same time and in the same frame.
44
+ * @returns The acceleration, metres a second squared.
45
+ * @example
46
+ * ```ts
47
+ * import { gravityAt } from "@johnmorrisdotca/uchusen-rockets/physics";
48
+ *
49
+ * const g = gravityAt([6_378_137, 0, 0], [{ gm: 3.986004418e14, position: [0, 0, 0] }]);
50
+ * console.log(g[0].toFixed(3), "m/s^2");
51
+ * ```
52
+ */
53
+ export declare function gravityAt(position: Vec3, bodies: readonly GravityBody[]): Vec3;
@@ -0,0 +1,78 @@
1
+ // The pull on a craft from the bodies of a scene: each a point mass, and a body's flattening (its J2, J3 and J4 terms) where the caller gives it. Pure.
2
+ // Ported from Orbiter (https://github.com/orbitersim/orbiter), Src/Orbiter/Psys.cpp: SingleGacc, the J-coefficient branch of
3
+ // SingleGacc_perturbation, and PlanetarySystem::Gacc. Copyright (c) 2000-2026 Martin Schweiger, MIT licence (LICENSES/Orbiter-MIT.txt).
4
+ // Changed in the port: a right-handed frame; Orbiter's polar unit vector (er x (er x pole)) is written out as -(pole - s r) / cos(latitude),
5
+ // and the cosine of the latitude in each polar term cancels it, so nothing is divided by zero over a pole; the bodies are a list the
6
+ // caller hands in, at the time asked, in place of Orbiter's planetary system; Orbiter's spherical-harmonic (Pines) model is not ported.
7
+ import {} from "./vector.js";
8
+ const EPS = 1e-10;
9
+ /** The pull of a flattened body beyond its point mass, at `offset` (the craft from the body's centre, metres), at distance `d`. */
10
+ function zonalPull(gm, zonal, offset, d) {
11
+ const [j2 = 0, j3 = 0, j4 = 0] = zonal.j;
12
+ const rr = zonal.radius / d;
13
+ let rrn = rr * rr;
14
+ let jn = j2 * rrn;
15
+ if (Math.abs(jn) <= EPS)
16
+ return [0, 0, 0];
17
+ const r = [offset[0] / d, offset[1] / d, offset[2] / d];
18
+ const p = zonal.pole;
19
+ const s = r[0] * p[0] + r[1] * p[1] + r[2] * p[2];
20
+ const s2 = s * s;
21
+ let radial = 1.5 * jn * (1 - 3 * s2);
22
+ let polar = 3 * jn * s;
23
+ if (zonal.j.length > 1) {
24
+ rrn *= rr;
25
+ jn = j3 * rrn;
26
+ if (Math.abs(jn) > EPS) {
27
+ radial += 2 * jn * s * (3 - 5 * s2);
28
+ polar += 1.5 * jn * (-1 + 5 * s2);
29
+ if (zonal.j.length > 2) {
30
+ rrn *= rr;
31
+ jn = j4 * rrn;
32
+ if (Math.abs(jn) > EPS) {
33
+ radial += -0.625 * jn * (3 + s2 * (-30 + 35 * s2));
34
+ polar += 2.5 * jn * s * (-3 + 7 * s2);
35
+ }
36
+ }
37
+ }
38
+ }
39
+ const t0 = gm / (d * d);
40
+ const along = radial - polar * s;
41
+ return [-t0 * (along * r[0] + polar * p[0]), -t0 * (along * r[1] + polar * p[1]), -t0 * (along * r[2] + polar * p[2])];
42
+ }
43
+ /**
44
+ * The acceleration gravity gives a craft at a place: the sum of `gm (body - craft) / d^3` over the bodies, plus each flattened body's zonal terms (Orbiter's `SingleGacc` summed as in `PlanetarySystem::Gacc`).
45
+ * For the best answer give the bodies in a frame that is not itself accelerating, such as the solar system's barycentre; a frame centred on a body that the others pull leaves out that pull on the frame.
46
+ * @param position The craft's position, metres.
47
+ * @param bodies The bodies that pull, at the same time and in the same frame.
48
+ * @returns The acceleration, metres a second squared.
49
+ * @example
50
+ * ```ts
51
+ * import { gravityAt } from "@johnmorrisdotca/uchusen-rockets/physics";
52
+ *
53
+ * const g = gravityAt([6_378_137, 0, 0], [{ gm: 3.986004418e14, position: [0, 0, 0] }]);
54
+ * console.log(g[0].toFixed(3), "m/s^2");
55
+ * ```
56
+ */
57
+ export function gravityAt(position, bodies) {
58
+ let ax = 0;
59
+ let ay = 0;
60
+ let az = 0;
61
+ for (const body of bodies) {
62
+ const dx = body.position[0] - position[0];
63
+ const dy = body.position[1] - position[1];
64
+ const dz = body.position[2] - position[2];
65
+ const d = Math.hypot(dx, dy, dz);
66
+ const k = body.gm / (d * d * d);
67
+ ax += dx * k;
68
+ ay += dy * k;
69
+ az += dz * k;
70
+ if (body.zonal) {
71
+ const extra = zonalPull(body.gm, body.zonal, [-dx, -dy, -dz], d);
72
+ ax += extra[0];
73
+ ay += extra[1];
74
+ az += extra[2];
75
+ }
76
+ }
77
+ return [ax, ay, az];
78
+ }