#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
#![warn(missing_docs)]
pub mod app_ext;
pub mod body_action;
pub mod bundles;
pub mod components;
pub mod frame_attach_system;
pub mod frame_param;
pub mod kinematic_propagation;
pub mod mass_tree;
pub mod prelude;
pub mod recipes;
pub mod scenario;
pub mod sets;
pub mod source_mutator;
pub mod systems;
pub mod validation;
pub mod wrench;
pub use app_ext::AstrodynAppExt;
pub use body_action::{
add_body_action_via, body_action_intake_system, body_action_system,
body_action_unregistered_planet_fence_system, BodyActionCommandsExt, BodyActionEvent,
BodyActionsR, RegisteredPlanetsR,
};
pub use bundles::*;
pub use components::*;
pub use frame_attach_system::{
frame_attach_system, propagate_frame_attached_state_post_integration_system,
propagate_frame_attached_state_system,
};
pub use kinematic_propagation::{
propagate_state_from_root_post_integration_system, propagate_state_from_root_system,
};
pub use mass_tree::{composite_mass_system, MassTreeQueries, MassTreeView};
pub use scenario::{ScenarioHandles, SimulationBuilderBevyExt};
pub use sets::*;
pub use source_mutator::{SourceMutator, SourceReader};
pub use systems::*;
pub use wrench::wrench_aggregation_system;
use bevy::prelude::*;
pub use astrodyn::atmosphere::{AtmosphereConfig, AtmosphereModel};
#[derive(Resource, Debug, Deref, DerefMut)]
pub struct SimulationTimeR(pub astrodyn::SimulationTime);
impl Default for SimulationTimeR {
fn default() -> Self {
Self(astrodyn::SimulationTime::at_j2000(
astrodyn::default_leap_second_table(),
))
}
}
#[derive(Resource, Debug, Clone, Copy, Deref, DerefMut)]
pub struct IntegrationDtR(pub f64);
#[derive(Resource, Debug, Clone, Copy)]
pub struct PolarMotionR {
pub xp: f64,
pub yp: f64,
}
#[derive(Resource, Debug, Clone)]
#[non_exhaustive]
pub struct AtmosphereModelR {
pub config: AtmosphereConfig,
pub planet_entity: Entity,
}
impl AtmosphereModelR {
pub fn new(config: AtmosphereConfig, planet_entity: Entity) -> Self {
Self {
config,
planet_entity,
}
}
}
#[derive(Resource, Deref, DerefMut)]
pub struct EphemerisR(pub astrodyn::Ephemeris);
#[derive(Resource, Deref, DerefMut)]
pub struct MassTreeR(pub astrodyn::MassTree);
#[derive(Resource, Debug, Clone, Copy, Deref, DerefMut)]
pub struct RootFrameEntityR(pub Entity);
pub struct AstrodynPlugin;
impl Plugin for AstrodynPlugin {
fn build(&self, app: &mut App) {
app.configure_sets(
FixedUpdate,
(
AstrodynSet::TimeUpdate,
AstrodynSet::EphemerisUpdate.after(AstrodynSet::TimeUpdate),
AstrodynSet::Environment.after(AstrodynSet::EphemerisUpdate),
AstrodynSet::Interaction.after(AstrodynSet::Environment),
AstrodynSet::ForceCollection.after(AstrodynSet::Interaction),
AstrodynSet::Integration.after(AstrodynSet::ForceCollection),
AstrodynSet::DerivedState.after(AstrodynSet::Integration),
),
);
app.init_resource::<SimulationTimeR>();
if !app.world().contains_resource::<RootFrameEntityR>() {
let root_frame_entity = app
.world_mut()
.spawn((
Name::new("root.frame"),
components::InertialFrameMarker,
components::FrameTransC::default(),
components::FrameRotC::default(),
components::FrameAngVelC::default(),
))
.id();
app.insert_resource(RootFrameEntityR(root_frame_entity));
} else {
let root_frame_entity = app.world().resource::<RootFrameEntityR>().0;
assert!(
app.world().get_entity(root_frame_entity).is_ok(),
"AstrodynPlugin: pre-installed RootFrameEntityR ({root_frame_entity:?}) \
references an entity that no longer exists in the world. Source / \
body registration will `ChildOf`-link new frame entities under \
this dangling reference and panic later. Insert the resource only \
after spawning the root frame entity in the same `App`, or remove \
the pre-installation and let AstrodynPlugin own root-frame creation.",
);
assert!(
app.world()
.entity(root_frame_entity)
.contains::<components::InertialFrameMarker>(),
"AstrodynPlugin: pre-installed RootFrameEntityR ({root_frame_entity:?}) \
is missing `InertialFrameMarker`. The plugin assumes the root \
frame is inertial — source / body registration tags new children \
with `InertialFrameMarker` and the typed Bevy components \
(`Position<RootInertial>`, \
`TranslationalStateC<P>` storing `<PlanetInertial<P>>`) \
are all phantom-tagged for an inertial root. Add \
`InertialFrameMarker` to the entity, or let AstrodynPlugin spawn the \
root frame.",
);
assert!(
app.world()
.entity(root_frame_entity)
.contains::<components::FrameTransC>()
&& app
.world()
.entity(root_frame_entity)
.contains::<components::FrameRotC>()
&& app
.world()
.entity(root_frame_entity)
.contains::<components::FrameAngVelC>(),
"AstrodynPlugin: pre-installed RootFrameEntityR ({root_frame_entity:?}) \
is missing one or more of the required frame components \
(`FrameTransC`, `FrameRotC`, `FrameAngVelC`). Frame-tree \
consumers read these directly from the root entity. Insert all \
three (each with `Default::default()` for an inertial root), or \
let AstrodynPlugin spawn the root frame.",
);
}
app.add_message::<AttachEvent<astrodyn::SelfRef, astrodyn::SelfRef>>();
app.add_message::<DetachEvent>();
app.add_message::<FrameAttachEvent>();
app.add_message::<FrameDetachEvent>();
app.add_message::<body_action::BodyActionEvent>();
app.init_resource::<body_action::BodyActionsR<astrodyn::Earth>>();
app.init_resource::<body_action::RegisteredPlanetsR>();
app.world_mut()
.resource_mut::<body_action::RegisteredPlanetsR>()
.register::<astrodyn::Earth>();
app.add_systems(
Startup,
(
systems::register_source_frames_system::<astrodyn::Earth>,
systems::register_pfix_frames_system::<astrodyn::Earth>
.after(systems::register_source_frames_system::<astrodyn::Earth>),
systems::register_body_frames_system::<astrodyn::Earth>
.after(systems::register_pfix_frames_system::<astrodyn::Earth>),
systems::sync_body_mass_point_ref_system
.after(systems::register_body_frames_system::<astrodyn::Earth>),
),
);
systems::register_joint_kinematics_exclusivity_hooks(app);
app.add_systems(PostStartup, systems::validate_joint_kinematics_exclusivity);
app.add_systems(
PreUpdate,
(
systems::register_source_frames_system::<astrodyn::Earth>,
systems::register_pfix_frames_system::<astrodyn::Earth>
.after(systems::register_source_frames_system::<astrodyn::Earth>),
systems::register_body_frames_system::<astrodyn::Earth>
.after(systems::register_pfix_frames_system::<astrodyn::Earth>),
systems::sync_body_mass_point_ref_system
.after(systems::register_body_frames_system::<astrodyn::Earth>),
),
);
app.add_observer(systems::on_retired_pfix_frame_entity_despawn);
app.add_observer(systems::on_frame_entity_despawn);
app.add_observer(systems::on_source_pfix_frame_entity_despawn);
app.add_systems(
FixedUpdate,
(
systems::time_advance_system.in_set(AstrodynSet::TimeUpdate),
systems::register_source_frames_system::<astrodyn::Earth>
.before(AstrodynSet::EphemerisUpdate),
systems::register_pfix_frames_system::<astrodyn::Earth>
.after(systems::register_source_frames_system::<astrodyn::Earth>)
.before(AstrodynSet::EphemerisUpdate),
systems::register_body_frames_system::<astrodyn::Earth>
.after(systems::register_pfix_frames_system::<astrodyn::Earth>)
.before(AstrodynSet::EphemerisUpdate),
systems::sync_body_mass_point_ref_system
.after(systems::register_body_frames_system::<astrodyn::Earth>)
.before(AstrodynSet::EphemerisUpdate),
validation::validate_jeod_invariants::<astrodyn::Earth>
.after(systems::register_body_frames_system::<astrodyn::Earth>)
.before(AstrodynSet::EphemerisUpdate),
systems::sync_source_to_frame_system::<astrodyn::Earth>
.in_set(AstrodynSet::EphemerisUpdate)
.after(systems::ephemeris_update_system::<astrodyn::Earth>)
.after(systems::planet_fixed_rotation_system::<astrodyn::Earth>),
systems::planet_fixed_rotation_system::<astrodyn::Earth>
.in_set(AstrodynSet::EphemerisUpdate),
systems::ephemeris_update_system::<astrodyn::Earth>
.in_set(AstrodynSet::EphemerisUpdate),
systems::tidal_update_system::<astrodyn::Earth>
.in_set(AstrodynSet::EphemerisUpdate)
.after(systems::planet_fixed_rotation_system::<astrodyn::Earth>),
systems::mass_update_system
.after(AstrodynSet::TimeUpdate)
.before(AstrodynSet::EphemerisUpdate),
mass_tree::composite_mass_system
.after(systems::mass_update_system)
.before(AstrodynSet::EphemerisUpdate),
systems::gravity_computation_system::<astrodyn::Earth>
.in_set(AstrodynSet::Environment),
systems::atmosphere_update_system::<astrodyn::Earth>
.in_set(AstrodynSet::Environment),
systems::staging_system::<astrodyn::Earth>
.after(AstrodynSet::Environment)
.before(AstrodynSet::Interaction),
systems::step_detached_system::<astrodyn::Earth>
.in_set(AstrodynSet::Integration)
.before(systems::sync_body_to_frame_system::<astrodyn::Earth>)
.before(systems::frame_switch_system::<astrodyn::Earth>),
systems::aero_drag_system::<astrodyn::Earth>.in_set(AstrodynSet::Interaction),
systems::gravity_torque_system.in_set(AstrodynSet::Interaction),
systems::flat_plate_srp_system::<astrodyn::Earth>.in_set(AstrodynSet::Interaction),
systems::cannonball_srp_system::<astrodyn::Earth>.in_set(AstrodynSet::Interaction),
),
);
app.add_systems(
FixedUpdate,
(
systems::joint_kinematics_system.in_set(AstrodynSet::EphemerisUpdate),
systems::sinusoidal_joint_kinematics_system.in_set(AstrodynSet::EphemerisUpdate),
systems::closure_joint_kinematics_system.in_set(AstrodynSet::EphemerisUpdate),
systems::multi_dof_joint_kinematics_system.in_set(AstrodynSet::EphemerisUpdate),
frame_attach_system::frame_attach_system::<astrodyn::Earth>
.after(AstrodynSet::EphemerisUpdate)
.before(AstrodynSet::Environment),
frame_attach_system::propagate_frame_attached_state_system::<astrodyn::Earth>
.after(frame_attach_system::frame_attach_system::<astrodyn::Earth>)
.before(AstrodynSet::Environment),
body_action::body_action_intake_system::<astrodyn::Earth>
.after(AstrodynSet::TimeUpdate)
.after(systems::sync_body_mass_point_ref_system)
.before(systems::mass_update_system)
.before(AstrodynSet::EphemerisUpdate),
body_action::body_action_unregistered_planet_fence_system
.after(AstrodynSet::TimeUpdate)
.after(body_action::body_action_intake_system::<astrodyn::Earth>)
.before(body_action::body_action_system::<astrodyn::Earth>)
.before(systems::mass_update_system)
.before(AstrodynSet::EphemerisUpdate),
body_action::body_action_system::<astrodyn::Earth>
.after(AstrodynSet::TimeUpdate)
.after(body_action::body_action_intake_system::<astrodyn::Earth>)
.after(body_action::body_action_unregistered_planet_fence_system)
.before(systems::mass_update_system)
.before(AstrodynSet::EphemerisUpdate),
kinematic_propagation::propagate_state_from_root_system::<astrodyn::Earth>
.after(
frame_attach_system::propagate_frame_attached_state_system::<astrodyn::Earth>,
)
.before(AstrodynSet::Environment),
systems::force_collection_system.in_set(AstrodynSet::ForceCollection),
wrench::wrench_aggregation_system
.in_set(AstrodynSet::ForceCollection)
.after(systems::force_collection_system),
systems::integration_system::<astrodyn::Earth>.in_set(AstrodynSet::Integration),
systems::sync_body_to_frame_system::<astrodyn::Earth>
.in_set(AstrodynSet::Integration)
.after(systems::integration_system::<astrodyn::Earth>),
systems::frame_switch_system::<astrodyn::Earth>
.in_set(AstrodynSet::Integration)
.after(systems::sync_body_to_frame_system::<astrodyn::Earth>),
frame_attach_system::propagate_frame_attached_state_post_integration_system::<
astrodyn::Earth,
>
.in_set(AstrodynSet::Integration)
.after(systems::frame_switch_system::<astrodyn::Earth>),
kinematic_propagation::propagate_state_from_root_post_integration_system::<
astrodyn::Earth,
>
.in_set(AstrodynSet::Integration)
.after(
frame_attach_system::propagate_frame_attached_state_post_integration_system::<astrodyn::Earth>,
),
),
);
app.add_systems(
FixedUpdate,
(
systems::orbital_elements_system::<astrodyn::Earth>
.in_set(AstrodynSet::DerivedState),
systems::euler_angles_system.in_set(AstrodynSet::DerivedState),
systems::lvlh_system::<astrodyn::Earth>.in_set(AstrodynSet::DerivedState),
systems::geodetic_system::<astrodyn::Earth>.in_set(AstrodynSet::DerivedState),
systems::solar_beta_system::<astrodyn::Earth>.in_set(AstrodynSet::DerivedState),
systems::earth_lighting_system::<astrodyn::Earth>.in_set(AstrodynSet::DerivedState),
),
);
}
}
pub fn register_planet_systems<P: astrodyn::Planet>(app: &mut App) {
app.init_resource::<body_action::BodyActionsR<P>>();
app.world_mut()
.resource_mut::<body_action::RegisteredPlanetsR>()
.register::<P>();
app.add_systems(
Startup,
(
systems::register_source_frames_system::<P>,
systems::register_pfix_frames_system::<P>
.after(systems::register_source_frames_system::<P>),
systems::register_body_frames_system::<P>
.after(systems::register_pfix_frames_system::<P>),
),
);
app.add_systems(
PreUpdate,
(
systems::register_source_frames_system::<P>,
systems::register_pfix_frames_system::<P>
.after(systems::register_source_frames_system::<P>),
systems::register_body_frames_system::<P>
.after(systems::register_pfix_frames_system::<P>),
),
);
app.add_systems(
FixedUpdate,
(
systems::register_source_frames_system::<P>.before(AstrodynSet::EphemerisUpdate),
systems::register_pfix_frames_system::<P>
.after(systems::register_source_frames_system::<P>)
.before(AstrodynSet::EphemerisUpdate),
systems::register_body_frames_system::<P>
.after(systems::register_pfix_frames_system::<P>)
.before(AstrodynSet::EphemerisUpdate),
validation::validate_jeod_invariants::<P>
.after(systems::register_body_frames_system::<P>)
.before(AstrodynSet::EphemerisUpdate),
systems::sync_source_to_frame_system::<P>
.in_set(AstrodynSet::EphemerisUpdate)
.after(systems::ephemeris_update_system::<P>)
.after(systems::planet_fixed_rotation_system::<P>),
systems::planet_fixed_rotation_system::<P>.in_set(AstrodynSet::EphemerisUpdate),
systems::ephemeris_update_system::<P>.in_set(AstrodynSet::EphemerisUpdate),
systems::tidal_update_system::<P>
.in_set(AstrodynSet::EphemerisUpdate)
.after(systems::planet_fixed_rotation_system::<P>),
systems::gravity_computation_system::<P>.in_set(AstrodynSet::Environment),
systems::atmosphere_update_system::<P>.in_set(AstrodynSet::Environment),
systems::staging_system::<P>
.after(AstrodynSet::Environment)
.before(AstrodynSet::Interaction),
systems::step_detached_system::<P>
.in_set(AstrodynSet::Integration)
.before(systems::sync_body_to_frame_system::<P>)
.before(systems::frame_switch_system::<P>),
systems::aero_drag_system::<P>.in_set(AstrodynSet::Interaction),
systems::flat_plate_srp_system::<P>.in_set(AstrodynSet::Interaction),
systems::cannonball_srp_system::<P>.in_set(AstrodynSet::Interaction),
),
);
app.add_systems(
FixedUpdate,
(
body_action::body_action_intake_system::<P>
.after(AstrodynSet::TimeUpdate)
.after(systems::sync_body_mass_point_ref_system)
.before(body_action::body_action_unregistered_planet_fence_system)
.before(systems::mass_update_system)
.before(AstrodynSet::EphemerisUpdate),
body_action::body_action_system::<P>
.after(AstrodynSet::TimeUpdate)
.after(body_action::body_action_intake_system::<P>)
.after(body_action::body_action_unregistered_planet_fence_system)
.before(systems::mass_update_system)
.before(AstrodynSet::EphemerisUpdate),
frame_attach_system::frame_attach_system::<P>
.after(AstrodynSet::EphemerisUpdate)
.before(AstrodynSet::Environment),
frame_attach_system::propagate_frame_attached_state_system::<P>
.after(frame_attach_system::frame_attach_system::<P>)
.before(AstrodynSet::Environment),
kinematic_propagation::propagate_state_from_root_system::<P>
.after(frame_attach_system::propagate_frame_attached_state_system::<P>)
.before(AstrodynSet::Environment),
systems::integration_system::<P>.in_set(AstrodynSet::Integration),
systems::sync_body_to_frame_system::<P>
.in_set(AstrodynSet::Integration)
.after(systems::integration_system::<P>),
systems::frame_switch_system::<P>
.in_set(AstrodynSet::Integration)
.after(systems::sync_body_to_frame_system::<P>),
frame_attach_system::propagate_frame_attached_state_post_integration_system::<P>
.in_set(AstrodynSet::Integration)
.after(systems::frame_switch_system::<P>),
kinematic_propagation::propagate_state_from_root_post_integration_system::<P>
.in_set(AstrodynSet::Integration)
.after(
frame_attach_system::propagate_frame_attached_state_post_integration_system::<P>,
),
systems::orbital_elements_system::<P>.in_set(AstrodynSet::DerivedState),
systems::lvlh_system::<P>.in_set(AstrodynSet::DerivedState),
systems::geodetic_system::<P>.in_set(AstrodynSet::DerivedState),
systems::solar_beta_system::<P>.in_set(AstrodynSet::DerivedState),
systems::earth_lighting_system::<P>.in_set(AstrodynSet::DerivedState),
),
);
}
pub trait VehicleConfigBevyExt {
fn spawn_bevy<P: astrodyn::Planet>(
self,
commands: &mut Commands,
source_entities: &[Entity],
) -> Entity;
}
fn resolve_source_entity(source_entities: &[Entity], idx: usize, what: &str) -> Entity {
*source_entities.get(idx).unwrap_or_else(|| {
panic!(
"spawn_bevy: {what} references source index {idx} but only {len} source \
entities were provided. Spawn all gravity sources before calling spawn_bevy.",
what = what,
idx = idx,
len = source_entities.len()
)
})
}
impl VehicleConfigBevyExt for astrodyn::VehicleConfig {
fn spawn_bevy<P: astrodyn::Planet>(
self,
commands: &mut Commands,
source_entities: &[Entity],
) -> Entity {
let entity_controls = astrodyn::GravityControls::<Entity> {
controls: self
.gravity_controls
.controls
.into_iter()
.map(|c| {
c.retag_source(|idx| {
resolve_source_entity(source_entities, idx, "GravityControl")
})
})
.collect(),
};
let dynamics_config = astrodyn::DynamicsConfig {
translational_dynamics: true,
rotational_dynamics: self.rot.is_some(),
three_dof: self.rot.is_none(),
};
let mut entity = commands.spawn((
components::TranslationalStateC::<P>::from(self.trans),
components::DynamicsConfigC(dynamics_config),
components::GravityControlsC(entity_controls),
components::IntegratorTypeC(self.integrator),
components::StructuralTransformC(astrodyn::FrameTransform::from_matrix(
self.t_struct_body,
)),
));
if let Some(rot) = self.rot {
entity.insert(components::RotationalStateC::from(rot));
}
if let Some(mass) = self.mass {
entity.insert(components::MassPropertiesC::from(mass));
}
if self.external_force.raw_si() != glam::DVec3::ZERO {
entity.insert(components::ExternalForceC(self.external_force));
}
if self.external_torque.raw_si() != glam::DVec3::ZERO {
entity.insert(components::ExternalTorqueC(self.external_torque));
}
if self.compute_gravity_gradient {
entity.insert(components::GravityTorqueC::default());
}
if let Some(drag) = self.drag {
entity.insert(components::DragConfigC::from_untyped(&drag));
}
match self.srp {
None => {}
Some(astrodyn::SrpModel::FlatPlate(state)) => {
entity.insert(components::FlatPlateConfigC(state));
}
Some(astrodyn::SrpModel::Cannonball {
cx_area,
albedo,
diffuse,
}) => {
entity.insert(components::CannonballSrpC {
cx_area,
albedo,
diffuse,
});
}
}
if let Some(sb) = self.shadow_body {
let src = resolve_source_entity(source_entities, sb.source_idx, "shadow_body");
let body_id = entity.id();
commands
.entity(src)
.insert(components::ShadowBodyC { radius: sb.radius });
entity = commands.entity(body_id);
}
if let Some(idx) = self.integ_source {
let src = resolve_source_entity(source_entities, idx, "integ_source");
entity.insert(components::IntegSourceC(Some(src)));
}
if !self.frame_switches.is_empty() {
let entity_switches: Vec<astrodyn::FrameSwitchConfig<Entity>> = self
.frame_switches
.into_iter()
.map(|sw| astrodyn::FrameSwitchConfig::<Entity> {
target_source: resolve_source_entity(
source_entities,
sw.target_source,
"FrameSwitchConfig::target_source",
),
switch_sense: sw.switch_sense,
switch_distance: sw.switch_distance,
active: sw.active,
})
.collect();
entity.insert(components::FrameSwitchesC(entity_switches));
}
let astrodyn::DerivedStateConfig {
orbital_elements_source,
euler_sequence,
lvlh,
geodetic,
solar_beta,
earth_lighting,
} = self.derived;
if let Some(idx) = orbital_elements_source {
let src =
resolve_source_entity(source_entities, idx, "derived.orbital_elements_source");
entity.insert((
components::OrbitalElementsC::<P>::default(),
components::OrbitalElementsConfigC {
gravity_source: src,
},
));
}
if let Some(sequence) = euler_sequence {
entity.insert((
components::EulerAnglesC::default(),
components::EulerAnglesConfigC { sequence },
));
}
if lvlh {
entity.insert(components::LvlhFrameC::default());
}
if let Some(geo) = geodetic {
let src = resolve_source_entity(
source_entities,
geo.source_idx,
"derived.geodetic.source_idx",
);
entity.insert((
components::GeodeticStateC::default(),
components::GeodeticConfigC { planet: src },
));
}
if solar_beta {
entity.insert(components::SolarBetaC::default());
}
if let Some(el) = earth_lighting {
entity.insert((
components::EarthLightingStateC::default(),
components::EarthLightingConfigC {
earth_radius: el.earth_radius,
moon_radius: el.moon_radius,
sun_radius: el.sun_radius,
},
));
}
entity.id()
}
}