- orbital_elements.py: elliptical + hyperbolic orbit support - orbit_drawer.py: orbit point generation with SOI truncation - soi_calculator.py: SOI crossing time calculator - frame_transition.py: reference frame switching - test_orbital.py: 147 assertions, all passing - visual_test.py: pygame flyby visualization
78 lines
2.3 KiB
Python
78 lines
2.3 KiB
Python
"""
|
||
Orbit trajectory point generation for rendering.
|
||
|
||
generate_orbit_points() — full orbit (elliptical) or truncated
|
||
hyperbolic path within a given cut-off radius.
|
||
"""
|
||
|
||
import math
|
||
from typing import List, Optional, Tuple
|
||
|
||
from orbital_elements import OrbitalElements, TAU
|
||
|
||
|
||
def generate_orbit_points(
|
||
orbit: OrbitalElements,
|
||
num_points: int = 200,
|
||
max_radius: Optional[float] = None,
|
||
) -> List[Tuple[float, float]]:
|
||
"""
|
||
Generate *num_points* world-space positions along the orbit.
|
||
|
||
Elliptical orbits → full closed loop, equally spaced in E.
|
||
Hyperbolic orbits → samples H ∈ [−H_max, +H_max], truncated
|
||
at *max_radius* (e.g. the planet's SOI edge).
|
||
|
||
Parameters
|
||
----------
|
||
max_radius : float or None
|
||
Only used for hyperbolic orbits. Points with r > max_radius
|
||
are omitted. If None, samples out to r ≈ 5·|a| from focus.
|
||
"""
|
||
if orbit.is_hyperbolic:
|
||
return _hyperbolic_points(orbit, num_points, max_radius)
|
||
return _elliptical_points(orbit, num_points)
|
||
|
||
|
||
def _elliptical_points(
|
||
orbit: OrbitalElements, num_points: int,
|
||
) -> List[Tuple[float, float]]:
|
||
points = []
|
||
for i in range(num_points):
|
||
E = TAU * i / num_points
|
||
points.append(orbit.position_from_E(E))
|
||
return points
|
||
|
||
|
||
def _hyperbolic_points(
|
||
orbit: OrbitalElements,
|
||
num_points: int,
|
||
max_radius: Optional[float],
|
||
) -> List[Tuple[float, float]]:
|
||
"""
|
||
Sample the hyperbolic branch that passes through periapsis.
|
||
|
||
Truncates at *max_radius* by solving for the H values where
|
||
r = max_radius, then sampling uniformly between those limits.
|
||
"""
|
||
r_limit = max_radius if max_radius is not None else 5.0 * abs(orbit.a)
|
||
|
||
# Find H where r = r_limit.
|
||
# r = |a|(e·cosh(H) − 1) → cosh(H) = (r/|a| + 1) / e
|
||
abs_a = abs(orbit.a)
|
||
cosh_Hmax = (r_limit / abs_a + 1.0) / orbit.e
|
||
if cosh_Hmax < 1.0:
|
||
return [] # entire orbit is outside the radius
|
||
H_max = math.acosh(cosh_Hmax)
|
||
|
||
points = []
|
||
for i in range(num_points):
|
||
frac = -1.0 + 2.0 * i / (num_points - 1) if num_points > 1 else 0.0
|
||
H = frac * H_max
|
||
x, y = orbit.position_from_H(H)
|
||
r = math.hypot(x, y)
|
||
if r <= r_limit + 1e-9:
|
||
points.append((x, y))
|
||
|
||
return points
|