use astrodyn::Planet;
use bevy::prelude::*;
#[allow(unused_imports)] use super::super::integration::{frame_switch_system, integration_system};
use crate::components::*;
#[derive(Resource, Debug, Default)]
pub struct FrameUidIndexR(pub(crate) std::collections::HashMap<astrodyn::FrameUid, Entity>);
impl FrameUidIndexR {
pub fn get(&self, uid: &astrodyn::FrameUid) -> Option<Entity> {
self.0.get(uid).copied()
}
pub fn iter(&self) -> impl Iterator<Item = (&astrodyn::FrameUid, Entity)> {
self.0.iter().map(|(uid, &e)| (uid, e))
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
#[allow(clippy::type_complexity)]
pub fn index_frame_uids_system(
mut index: ResMut<FrameUidIndexR>,
added: Query<(Entity, &FrameUidC), (Added<FrameUidC>, With<FrameTransC>)>,
) {
for (entity, uid) in &added {
if let Some(&existing) = index.0.get(&uid.0) {
if existing == entity {
continue; }
panic!(
"FrameUidIndexR: duplicate frame identity `{}` — already registered \
at entity {existing:?}, cannot register it again at {entity:?}. Each \
FrameUid maps to exactly one frame entity. If these are genuinely \
distinct frames, mint distinct identities (different tag, role, or \
namespace).",
uid.0
);
}
index.0.insert(uid.0.clone(), entity);
}
}
pub fn deindex_frame_uids_system(
mut index: ResMut<FrameUidIndexR>,
mut removed: RemovedComponents<FrameUidC>,
) {
for entity in removed.read() {
index.0.retain(|_, e| *e != entity);
}
}
#[allow(clippy::type_complexity)]
pub fn register_source_frames_system(
mut commands: Commands,
root_frame_entity: Res<crate::RootFrameEntityR>,
sim_time: Res<crate::SimulationTimeR>,
sources: Query<
(
Entity,
Option<&Name>,
Option<&FrameUidC>,
&SourceInertialPositionC,
Option<&SourceInertialVelocityC>,
),
(With<GravitySourceC>, Without<FrameEntityC>),
>,
) {
let frame_epoch = FrameEpochC(sim_time.tdb());
for (entity, name, uid, pos, vel) in &sources {
let label = name
.map(|n| n.as_str().to_string())
.unwrap_or_else(|| format!("source{:?}", entity));
let uid = uid.unwrap_or_else(|| {
panic!(
"register_source_frames_system: gravity source {entity:?} ({label}) \
has no FrameUidC — every gravity source must carry its \
inertial-frame identity, minted from the source's own planet \
marker. Spawn it via PlanetBundle or populate_app (both stamp \
it), or insert it explicitly before registration, e.g. \
FrameUidC(astrodyn::FrameUid::of::<astrodyn::PlanetInertial<astrodyn::Earth>>())."
)
});
let init_pos = pos.0.raw_si();
let init_vel = vel.map_or(glam::DVec3::ZERO, |v| v.0.raw_si());
let source_frame_entity = commands
.spawn((
Name::new(format!("{label}.frame.inertial")),
InertialFrameMarker,
uid.clone(),
frame_epoch,
FrameTransC {
position: init_pos,
velocity: init_vel,
},
FrameRotC::default(),
FrameAngVelC::default(),
ChildOf(root_frame_entity.0),
))
.id();
commands
.entity(entity)
.insert(FrameEntityC(source_frame_entity));
}
}
#[allow(clippy::type_complexity)]
pub fn register_pfix_frames_system<P: Planet>(
mut commands: Commands,
sim_time: Res<crate::SimulationTimeR>,
sources: Query<
(
Entity,
Option<&Name>,
// The source's carried inertial-frame identity (issue #664);
// required — `register_source_frames_system` panics before
// this system can see a source without one.
&FrameUidC,
// The source's own frame entity: the spawned pfix frame
// entity ChildOf-links under it. Required for registration
// — `register_source_frames_system` always inserts it.
&FrameEntityC,
Option<&RotationModelC>,
// ECS-entity retirement marker so we reuse instead of leak
// on toggle cycles.
Option<&RetiredPfixFrameEntityC>,
),
(
With<GravitySourceC>,
With<PlanetFixedRotationC<P>>,
Without<PfixFrameEntityC>,
),
>,
mut frame_trans: Query<&mut FrameTransC>,
mut frame_rots: Query<&mut FrameRotC>,
mut frame_ang_vels: Query<&mut FrameAngVelC>,
) {
let frame_epoch = FrameEpochC(sim_time.tdb());
for (entity, name, uid, source_frame_entity, rotation_model, retired_entity) in &sources {
let default_model = astrodyn::RotationModel::EarthRNP;
let model_value = rotation_model.map_or(default_model, |m| m.0);
if matches!(model_value, astrodyn::RotationModel::None) {
continue;
}
let label = name
.map(|n| n.as_str().to_string())
.unwrap_or_else(|| format!("source{:?}", entity));
let pfix_uid = FrameUidC(astrodyn::pfix_sibling_uid(&uid.0));
let pfix_frame_entity = if let Some(retired_e) = retired_entity {
commands.entity(retired_e.0).insert((
Name::new(format!("{label}.frame.pfix")),
pfix_uid.clone(),
frame_epoch,
));
let mut t = frame_trans.get_mut(retired_e.0).unwrap_or_else(|err| {
panic!(
"register_pfix_frames_system: source {entity:?} \
carries RetiredPfixFrameEntityC({:?}) but that \
entity has no FrameTransC ({err:?}). The retired \
pfix frame entity must be alive with FrameTransC / \
FrameRotC / FrameAngVelC intact (set up by \
planet_fixed_rotation_system on retirement). Do not \
despawn or strip components from a retired pfix \
frame entity while its source still carries the \
marker.",
retired_e.0
)
});
*t = FrameTransC::default();
let mut r = frame_rots.get_mut(retired_e.0).unwrap_or_else(|err| {
panic!(
"register_pfix_frames_system: source {entity:?} \
carries RetiredPfixFrameEntityC({:?}) but that \
entity has no FrameRotC ({err:?}). The retired \
pfix frame entity must be alive with FrameTransC / \
FrameRotC / FrameAngVelC intact (set up by \
planet_fixed_rotation_system on retirement). Do not \
despawn or strip components from a retired pfix \
frame entity while its source still carries the \
marker.",
retired_e.0
)
});
*r = FrameRotC::default();
let mut av = frame_ang_vels.get_mut(retired_e.0).unwrap_or_else(|err| {
panic!(
"register_pfix_frames_system: source {entity:?} \
carries RetiredPfixFrameEntityC({:?}) but that \
entity has no FrameAngVelC ({err:?}). The retired \
pfix frame entity must be alive with FrameTransC / \
FrameRotC / FrameAngVelC intact (set up by \
planet_fixed_rotation_system on retirement). Do not \
despawn or strip components from a retired pfix \
frame entity while its source still carries the \
marker.",
retired_e.0
)
});
*av = FrameAngVelC::default();
commands.entity(entity).remove::<RetiredPfixFrameEntityC>();
retired_e.0
} else {
commands
.spawn((
Name::new(format!("{label}.frame.pfix")),
PlanetFixedFrameMarker,
pfix_uid.clone(),
frame_epoch,
FrameTransC::default(),
FrameRotC::default(),
FrameAngVelC::default(),
ChildOf(source_frame_entity.0),
))
.id()
};
commands
.entity(entity)
.insert(PfixFrameEntityC(pfix_frame_entity));
}
}
#[allow(clippy::type_complexity)]
pub fn sync_source_to_frame_system<P: Planet>(
sim_time: Res<crate::SimulationTimeR>,
sources: Query<(
&FrameEntityC,
&SourceInertialPositionC,
Option<&SourceInertialVelocityC>,
Option<&TranslationalStateC<P>>,
Has<EphemerisBodyC>,
)>,
mut frame_states: Query<&mut FrameTransC>,
mut frame_epochs: Query<&mut FrameEpochC>,
) {
let frame_epoch = FrameEpochC(sim_time.tdb());
for (fe, pos, vel, trans, ephemeris_driven) in &sources {
let position = pos.0.raw_si();
let velocity = vel
.map(|v| v.0.raw_si())
.or_else(|| trans.map(|t| t.0.velocity.raw_si()));
let mut frame_trans = frame_states.get_mut(fe.0).unwrap_or_else(|err| {
panic!(
"sync_source_to_frame_system: source has \
FrameEntityC({:?}) but that entity has no FrameTransC \
({err:?}). The source's frame entity must be alive \
with FrameTransC attached (spawned by PlanetBundle / \
register_*_frames_system). Either remove the stale \
FrameEntityC marker before despawning the frame \
entity, or ensure the frame entity stays alive for \
as long as the source carries the handle.",
fe.0
)
});
frame_trans.position = position;
if let Some(v) = velocity {
frame_trans.velocity = v;
}
if ephemeris_driven {
let mut epoch = frame_epochs.get_mut(fe.0).unwrap_or_else(|err| {
panic!(
"sync_source_to_frame_system: source has FrameEntityC({:?}) \
but that entity has no FrameEpochC ({err:?}). Frame \
entities are stamped with FrameEpochC at registration \
(issue #664); do not strip it.",
fe.0
)
});
*epoch = frame_epoch;
}
}
}
#[allow(clippy::type_complexity)]
pub fn register_body_frames_system<P: Planet>(
mut commands: Commands,
root_frame_entity: Res<crate::RootFrameEntityR>,
sim_time: Res<crate::SimulationTimeR>,
sources: Query<(&FrameUidC, &FrameEntityC), With<GravitySourceC>>,
bodies: Query<
(
Entity,
Option<&Name>,
Option<&FrameUidC>,
&TranslationalStateC<P>,
Option<&IntegSourceC>,
// Wire the frame-side `MassPointRef` back-pointer at
// body-frame registration time for any entity that also
// carries `MassPropertiesC` (i.e. participates in the
// mass tree). In the current Bevy adapter the body /
// mass / frame ECS entity is one and the same, so the
// back-pointer resolves to `MassPointRef(self)`. The
// component is skipped for kinematic-only bodies (no
// `MassPropertiesC`), matching the "absent for
// kinematic-only attaches" contract on the type.
Has<MassPropertiesC>,
),
(
With<TranslationalStateC<P>>,
With<DynamicsConfigC>,
Without<FrameEntityC>,
),
>,
) {
let frame_epoch = FrameEpochC(sim_time.tdb());
for (entity, name, uid, trans, integ_source, has_mass) in &bodies {
let label = name
.map(|n| n.as_str().to_string())
.unwrap_or_else(|| format!("body{:?}", entity));
let body_uid = uid.unwrap_or_else(|| {
panic!(
"register_body_frames_system: body {entity:?} ({label}) has no \
FrameUidC — every dynamic body must carry a stable frame \
identity. Spawn it via VehicleConfig::spawn_bevy (which stamps \
config.frame_uid), or insert \
FrameUidC(astrodyn::named_body_frame_uid(\"<label>\")) on the \
body entity before registration."
)
});
let integ_frame_entity = match integ_source.and_then(|c| c.0.as_ref()) {
Some(integ_uid) => sources
.iter()
.find(|(uid, _)| &uid.0 == integ_uid)
.map(|(_, fe)| fe.0)
.unwrap_or_else(|| {
panic!(
"register_body_frames_system: body {entity:?} has \
IntegSourceC(`{integ_uid}`), but no registered gravity \
source carries that identity (it is missing, or its \
frame was never registered). Spawn the source via \
PlanetBundle before the body, fix the identity, or \
remove IntegSourceC."
)
}),
None => root_frame_entity.0,
};
let init_pos = trans.0.position.raw_si();
let init_vel = trans.0.velocity.raw_si();
commands
.entity(integ_frame_entity)
.insert(IntegrationFrameMarker);
let body_frame_entity = commands
.spawn((
Name::new(format!("{label}.frame.body")),
BodyFrameMarker,
body_uid.clone(),
frame_epoch,
FrameTransC {
position: init_pos,
velocity: init_vel,
},
FrameRotC::default(),
FrameAngVelC::default(),
ChildOf(integ_frame_entity),
))
.id();
let mut entity_cmds = commands.entity(entity);
entity_cmds.insert(FrameEntityC(body_frame_entity));
if has_mass {
entity_cmds.insert(MassPointRef(entity));
}
}
}
#[allow(clippy::type_complexity)]
pub fn sync_body_mass_point_ref_system(
mut commands: Commands,
acquired: Query<
Entity,
(
With<FrameEntityC>,
With<DynamicsConfigC>,
With<MassPropertiesC>,
Without<MassPointRef>,
),
>,
lost: Query<
Entity,
(
With<FrameEntityC>,
With<DynamicsConfigC>,
With<MassPointRef>,
Without<MassPropertiesC>,
),
>,
) {
for entity in &acquired {
commands.entity(entity).insert(MassPointRef(entity));
}
for entity in &lost {
commands.entity(entity).remove::<MassPointRef>();
}
}
pub fn sync_body_to_frame_system<P: Planet>(
sim_time: Res<crate::SimulationTimeR>,
bodies: Query<(&TranslationalStateC<P>, &FrameEntityC), With<DynamicsConfigC>>,
mut frame_states: Query<&mut FrameTransC>,
mut frame_epochs: Query<&mut FrameEpochC>,
) {
let frame_epoch = FrameEpochC(sim_time.tdb());
for (trans, frame_entity) in &bodies {
let position = trans.0.position.raw_si();
let velocity = trans.0.velocity.raw_si();
let mut frame_trans = frame_states.get_mut(frame_entity.0).unwrap_or_else(|err| {
panic!(
"sync_body_to_frame_system: body has FrameEntityC({:?}) \
but that entity has no FrameTransC ({err:?}). The \
body's frame entity must be alive with FrameTransC \
attached (spawned by register_body_frames_system). \
Either remove the stale FrameEntityC marker before \
despawning the frame entity, or ensure the frame \
entity stays alive for as long as the body carries \
the handle.",
frame_entity.0
)
});
frame_trans.position = position;
frame_trans.velocity = velocity;
let mut epoch = frame_epochs.get_mut(frame_entity.0).unwrap_or_else(|err| {
panic!(
"sync_body_to_frame_system: body has FrameEntityC({:?}) but \
that entity has no FrameEpochC ({err:?}). Frame entities are \
stamped with FrameEpochC at registration (issue #664); do \
not strip it.",
frame_entity.0
)
});
*epoch = frame_epoch;
}
}
pub fn resolve_shadow_body_ref_system(
mut commands: Commands,
refs: Query<(Entity, &ShadowBodyRefC)>,
sources: Query<(Entity, &FrameUidC, Option<&ShadowBodyC>), With<GravitySourceC>>,
) {
let mut declared: std::collections::HashMap<&astrodyn::FrameUid, f64> =
std::collections::HashMap::new();
for (body_entity, sb_ref) in &refs {
let (source_entity, _, existing) = sources
.iter()
.find(|(_, uid, _)| uid.0 == sb_ref.source)
.unwrap_or_else(|| {
panic!(
"resolve_shadow_body_ref_system: body {body_entity:?} declares \
shadow caster `{}`, but no registered gravity source carries \
that identity. Spawn the source (PlanetBundle / populate_app / \
explicit FrameUidC) before the vehicle, or fix the identity.",
sb_ref.source
)
});
let prior = declared
.get(&sb_ref.source)
.copied()
.or(existing.map(|e| e.radius));
match prior {
Some(prev) => {
assert!(
prev.to_bits() == sb_ref.radius.to_bits(),
"resolve_shadow_body_ref_system: shadow caster `{}` already has \
radius {prev} m (from an earlier declaration or its existing \
ShadowBodyC), but body {body_entity:?} declares radius {} m. \
All bodies sharing a shadow caster must agree on its radius.",
sb_ref.source,
sb_ref.radius,
);
}
None => {
commands.entity(source_entity).insert(ShadowBodyC {
radius: sb_ref.radius,
});
}
}
declared.insert(&sb_ref.source, sb_ref.radius);
commands.entity(body_entity).remove::<ShadowBodyRefC>();
}
}