use astrodyn::Planet;
use bevy::prelude::*;
use crate::components::{
CannonballSrpC, DragConfigC, DynamicsConfigC, EarthLightingConfigC, EulerAnglesConfigC,
FlatPlateConfigC, FrameAngVelC, FrameEntityC, FrameRotC, FrameSwitchesC, FrameTransC,
GeodeticConfigC, GravityAccelerationC, GravityControlsC, GravitySourceC, LvlhFrameC,
MassPropertiesC, MoonMarker, OrbitalElementsConfigC, PlanetFixedRotationC, RotationalStateC,
SolarBetaC, SunMarker, TidalConfigC, TidalDeltaC20C, TranslationalStateC,
};
use crate::RootFrameEntityR;
pub(crate) fn is_root_equivalent_entity(
frame_entity: Entity,
root_entity: Entity,
parents: &Query<&ChildOf>,
frame_states: &Query<(&FrameTransC, &FrameRotC, &FrameAngVelC)>,
) -> bool {
if frame_entity == root_entity {
return true;
}
let Ok(child_of) = parents.get(frame_entity) else {
return false;
};
if child_of.parent() != root_entity {
return false;
}
let Ok((trans, rot, ang_vel)) = frame_states.get(frame_entity) else {
return false;
};
trans.position == glam::DVec3::ZERO
&& trans.velocity == glam::DVec3::ZERO
&& rot.t_parent_this == glam::DMat3::IDENTITY
&& ang_vel.0 == glam::DVec3::ZERO
}
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
pub fn validate_jeod_invariants<P: Planet>(
mut bodies: Query<
(
Entity,
&DynamicsConfigC,
&mut GravityControlsC,
Option<&GravityAccelerationC>,
Option<&MassPropertiesC>,
Option<&RotationalStateC>,
Option<&TranslationalStateC<P>>,
Option<&FlatPlateConfigC>,
),
Added<GravityControlsC>,
>,
sources: Query<(Entity, &GravitySourceC)>,
tidal_sources: Query<(
Entity,
&TidalConfigC,
Option<&TidalDeltaC20C>,
Option<&PlanetFixedRotationC<P>>,
)>,
srp_exclusion: Query<Entity, With<CannonballSrpC>>,
derived_state_markers: Query<(
Entity,
Option<&SolarBetaC>,
Option<&EarthLightingConfigC>,
Option<&SunMarker>,
Option<&MoonMarker>,
Option<&TranslationalStateC<P>>,
)>,
body_frame_state: Query<(Option<&FrameEntityC>, Option<&FrameSwitchesC>)>,
source_frames: Query<&FrameEntityC, With<GravitySourceC>>,
parents: Query<&ChildOf>,
frame_states: Query<(&FrameTransC, &FrameRotC, &FrameAngVelC)>,
root_frame_entity: Option<Res<RootFrameEntityR>>,
#[allow(clippy::type_complexity)] root_dependent_features: Query<(
Has<DragConfigC>,
Has<FlatPlateConfigC>,
Has<CannonballSrpC>,
Has<OrbitalElementsConfigC>,
Has<EulerAnglesConfigC>,
Has<GeodeticConfigC>,
Has<LvlhFrameC>,
Has<SolarBetaC>,
Has<EarthLightingConfigC>,
)>,
) {
if bodies.is_empty() {
return;
}
let mut sun_count = 0;
let mut moon_count = 0;
for (entity, _, _, sun, moon, trans) in &derived_state_markers {
if sun.is_some() {
sun_count += 1;
assert!(
trans.is_some(),
"Entity {entity:?}: SunMarker present but TranslationalStateC is missing. \
Sun entity requires TranslationalStateC for position queries."
);
}
if moon.is_some() {
moon_count += 1;
assert!(
trans.is_some(),
"Entity {entity:?}: MoonMarker present but TranslationalStateC is missing. \
Moon entity requires TranslationalStateC for position queries."
);
}
}
assert!(
sun_count <= 1,
"Multiple SunMarker entities found. JEOD assumes exactly one Sun body."
);
assert!(
moon_count <= 1,
"Multiple MoonMarker entities found. JEOD assumes exactly one Moon body."
);
for (entity, solar_beta, earth_lighting, _, _, _) in &derived_state_markers {
if solar_beta.is_some() && sun_count == 0 {
panic!(
"Entity {entity:?}: SolarBetaC present but no SunMarker entity exists. \
Solar beta computation requires exactly one SunMarker entity."
);
}
if earth_lighting.is_some() {
if sun_count == 0 {
panic!(
"Entity {entity:?}: EarthLightingConfigC present but no SunMarker entity. \
Earth lighting requires both SunMarker and MoonMarker entities."
);
}
if moon_count == 0 {
panic!(
"Entity {entity:?}: EarthLightingConfigC present but no MoonMarker entity. \
Earth lighting requires both SunMarker and MoonMarker entities."
);
}
}
}
for (entity, _config, delta, rotation) in &tidal_sources {
assert!(
delta.is_some(),
"Entity {entity:?}: TidalConfigC is present but TidalDeltaC20C is missing. \
Add TidalDeltaC20C::default() to the entity so tidal_update_system can write ΔC20."
);
assert!(
rotation.is_some(),
"Entity {entity:?}: TidalConfigC is present but PlanetFixedRotationC is missing. \
tidal_update_system requires PlanetFixedRotationC to transform tidal body \
positions into the planet-fixed frame."
);
}
for (entity, _, _, _, _, _, _, flat_plates) in &bodies {
if flat_plates.is_some() && srp_exclusion.get(entity).is_ok() {
panic!(
"Entity {entity:?}: both FlatPlateConfigC and CannonballSrpC are present. \
These are mutually exclusive — use one SRP model per entity."
);
}
}
for (entity, config, mut controls, grav_accel, mass, rot_state, trans_state, flat_plates) in
&mut bodies
{
let plate_counts = flat_plates.map(|fp| {
(
fp.plates.len(),
fp.temperatures.len(),
fp.t_pow4_cached.len(),
)
});
let mass_untyped = mass.map(|m| astrodyn::typed_bridge::mass_typed_to_raw(&m.0));
let trans_untyped = trans_state.map(|t| astrodyn::typed_bridge::trans_typed_to_raw(&t.0));
let errors = astrodyn::validate_body(
config,
&controls.0,
grav_accel.is_some(),
mass_untyped.as_ref(),
rot_state.is_some(),
trans_untyped.as_ref(),
|source_entity| sources.get(source_entity).ok().map(|(_, source)| &source.0),
plate_counts,
);
for error in &errors {
if error.is_warning() {
bevy::log::warn!("Entity {entity:?}: {error}");
} else {
panic!("Entity {entity:?}: {error}");
}
}
let (body_frame_handle, switches) = body_frame_state.get(entity).unwrap_or((None, None));
let root_entity_value = root_frame_entity.as_ref().map(|r| r.0);
let body_integ_frame_entity = body_frame_handle
.and_then(|fe| parents.get(fe.0).ok().map(|child_of| child_of.parent()));
let non_root_integ = match (body_integ_frame_entity, root_entity_value) {
(Some(integ_e), Some(root_e)) => {
!is_root_equivalent_entity(integ_e, root_e, &parents, &frame_states)
}
_ => false,
};
let mut non_root_switch = false;
if let Some(switches) = switches {
for sw in &switches.0 {
if !sw.active {
continue;
}
let target_frame_entity = match source_frames.get(sw.target_source) {
Ok(fe) => Some(fe.0),
Err(_) => {
panic!(
"Entity {entity:?}: FrameSwitchConfig.target_source = {target:?} \
is not a registered gravity source — it is missing \
GravitySourceC and/or FrameEntityC. Spawn it with PlanetBundle \
(which inserts both) before adding the body.",
target = sw.target_source,
);
}
};
if !controls
.0
.controls
.iter()
.any(|c| c.source_name == sw.target_source)
{
panic!(
"Entity {entity:?}: FrameSwitchConfig.target_source = {target:?} \
is not in the body's GravityControlsC. The post-switch gravity \
reclassification needs the target source to have a GravityControl \
entry (it becomes the non-differential central body). Add \
GravityControl::new_spherical({target:?}, ...) to the body's \
controls before configuring the switch.",
target = sw.target_source,
);
}
if let (Some(tfe), Some(root_e)) = (target_frame_entity, root_entity_value) {
if !is_root_equivalent_entity(tfe, root_e, &parents, &frame_states) {
non_root_switch = true;
}
}
}
}
if non_root_integ || non_root_switch {
let (
has_drag,
has_flat,
has_cannonball,
has_orbital,
has_euler,
has_geodetic,
has_lvlh,
has_solar_beta,
has_earth_lighting,
) = root_dependent_features.get(entity).unwrap_or_default();
let has_root_dependent = has_drag
|| has_flat
|| has_cannonball
|| has_orbital
|| has_euler
|| has_geodetic
|| has_lvlh
|| has_solar_beta
|| has_earth_lighting;
if has_root_dependent {
bevy::log::warn!(
"Entity {entity:?}: non-root integration frame (or active \
frame switch into a non-root frame) with features that \
assume root-inertial coordinates (drag={has_drag}, \
flat_plate_srp={has_flat}, cannonball_srp={has_cannonball}, \
orbital_elements={has_orbital}, euler={has_euler}, \
geodetic={has_geodetic}, lvlh={has_lvlh}, \
solar_beta={has_solar_beta}, earth_lighting={has_earth_lighting}). \
These derived states assume the simulation's central-body inertial \
frame and will produce incorrect results in other frames.",
);
}
}
for ctrl in &mut controls.0.controls {
if let Ok((_source_entity, source)) = sources.get(ctrl.source_name) {
ctrl.check_validity(&source.0);
}
}
}
}