use astrodyn::frame_doc::{
CanonicalRotation, Conventions, DocHeader, FrameDocument, FrameRecord, Origin, TransRecord,
SCHEMA_VERSION,
};
use astrodyn::frame_doc_io::{record_epoch, record_state};
use astrodyn::Planet;
use bevy::prelude::*;
use std::collections::HashMap;
use crate::components::*;
use crate::systems::FrameUidIndexR;
use crate::{RootFrameEntityR, SimulationTimeR};
pub fn export_frame_document(world: &mut World) -> FrameDocument {
let time = world.resource::<SimulationTimeR>();
let header = DocHeader {
schema_version: SCHEMA_VERSION,
conventions: Conventions::current(),
simtime: time.simtime,
tai_tjt_at_epoch: time.tai_tjt_at_epoch,
};
let root = world.resource::<RootFrameEntityR>().0;
let mut origins: HashMap<Entity, Origin> = HashMap::new();
origins.insert(root, Origin::Injected);
let mut sources = world.query_filtered::<(
&FrameEntityC,
Option<&PfixFrameEntityC>,
Option<&EphemerisBodyC>,
Option<&RotationModelC>,
), With<GravitySourceC>>();
for (fe, pfix_fe, ephem, model) in sources.iter(world) {
let inertial_origin = match ephem {
Some(e) => Origin::Derived {
model: format!("DE4xx:{:?}/{:?}", e.target, e.observer),
},
None => Origin::Injected,
};
origins.insert(fe.0, inertial_origin);
if let Some(pfix) = pfix_fe {
let effective = model.map_or(astrodyn::RotationModel::EarthRNP, |m| m.0);
let pfix_origin = match effective {
astrodyn::RotationModel::None => Origin::Injected,
m => Origin::Derived {
model: format!("{m:?}"),
},
};
origins.insert(pfix.0, pfix_origin);
}
}
let mut bodies =
world.query_filtered::<(&FrameEntityC, Option<&RotationalStateC>), With<DynamicsConfigC>>();
for (fe, rot) in bodies.iter(world) {
let (attitude_quat, ang_vel_body) = match rot {
Some(r) => {
let raw = astrodyn::typed_bridge::rot_typed_to_raw(&r.0);
(Some(raw.quaternion.data), Some(raw.ang_vel_body.to_array()))
}
None => (None, None),
};
origins.insert(
fe.0,
Origin::Integrated {
attitude_quat,
ang_vel_body,
},
);
}
let retired: std::collections::HashSet<Entity> = {
let mut handles = world.query::<&RetiredPfixFrameEntityC>();
handles.iter(world).map(|h| h.0).collect()
};
let mut identityless =
world.query_filtered::<(Entity, Option<&Name>), (With<FrameTransC>, Without<FrameUidC>)>();
for (entity, name) in identityless.iter(world) {
assert!(
retired.contains(&entity),
"export_frame_document: entity {entity:?} (`{}`) carries frame state \
(FrameTransC) but no FrameUidC and is not a retired pfix frame — it \
bypassed identity stamping and would be silently omitted from the \
snapshot. Spawn frame state only through registration (which stamps \
FrameUidC), or despawn the entity before export.",
name.map(|n| n.as_str()).unwrap_or("<unnamed>")
);
}
let mut frames = world.query::<(
Entity,
&FrameUidC,
&FrameTransC,
&FrameRotC,
&FrameAngVelC,
Option<&FrameEpochC>,
Option<&Name>,
Option<&ChildOf>,
Has<PlanetFixedFrameMarker>,
)>();
let mut ordered: Vec<Entity> = vec![root];
for (entity, ..) in frames.iter(world) {
if entity != root {
ordered.push(entity);
}
}
let mut uids = Vec::with_capacity(ordered.len());
let mut records = Vec::with_capacity(ordered.len());
for (idx, &entity) in ordered.iter().enumerate() {
let (_, uid, trans, rot, ang_vel, epoch, name, child_of, is_pfix) = frames
.get(world, entity)
.expect("ordered entities carry frame components");
let epoch = epoch.unwrap_or_else(|| {
panic!(
"export_frame_document: frame entity {entity:?} has no FrameEpochC — \
registration stamps every frame entity (issue #664); do not strip it."
)
});
let parent = child_of.map(|c| {
let parent_entity = c.parent();
let pos = ordered
.iter()
.position(|&e| e == parent_entity)
.unwrap_or_else(|| {
panic!(
"export_frame_document: frame entity {entity:?}'s parent \
{parent_entity:?} carries no FrameUidC / frame state — the \
ChildOf hierarchy must contain only stamped frame entities."
)
});
u32::try_from(pos).expect("frame population exceeds u32 — unsupported document size")
});
let rotation = if is_pfix {
CanonicalRotation::Matrix(rot.t_parent_this.to_cols_array_2d())
} else {
CanonicalRotation::Quat(rot.q_parent_this.data)
};
let origin = origins.remove(&entity).unwrap_or_else(|| {
panic!(
"export_frame_document: frame entity {entity:?} (`{}`) is not the \
root, a registered source's inertial/pfix frame, or a registered \
body's frame — cannot classify its origin for export.",
name.map(|n| n.as_str()).unwrap_or("<unnamed>")
)
});
uids.push(uid.0.clone());
records.push(FrameRecord {
name: name
.map(|n| n.as_str().to_string())
.unwrap_or_else(|| format!("{entity:?}")),
uid_index: u32::try_from(idx)
.expect("frame population exceeds u32 — unsupported document size"),
parent,
epoch: Some(epoch.0.as_seconds()),
trans: TransRecord {
position: trans.position.to_array(),
velocity: trans.velocity.to_array(),
},
rotation,
ang_vel_this: ang_vel.0.to_array(),
origin,
});
}
let doc = FrameDocument {
header,
uids,
records,
};
doc.validate()
.unwrap_or_else(|err| panic!("export_frame_document: produced an invalid document: {err}"));
doc
}
pub fn apply_frame_document<P: Planet>(world: &mut World, doc: &FrameDocument) {
{
let mut time = world.resource_mut::<SimulationTimeR>();
assert!(
time.simtime.to_bits() == 0.0_f64.to_bits(),
"apply_frame_document: simulation clock is at simtime {} (expected 0) — \
restore targets a freshly built App before its first FixedUpdate tick.",
time.simtime
);
assert!(
time.tai_tjt_at_epoch.to_bits() == doc.header.tai_tjt_at_epoch.to_bits(),
"apply_frame_document: time-epoch mismatch — the document was produced at \
tai_tjt_at_epoch {}, this App is configured at {}. Derived time scales \
(TDB, GMST) are functions of the epoch; rebuild the App with the \
producer's epoch.",
doc.header.tai_tjt_at_epoch,
time.tai_tjt_at_epoch
);
time.advance(doc.header.simtime);
}
let mut body_of_frame: HashMap<Entity, Entity> = HashMap::new();
let mut bodies = world.query_filtered::<(Entity, &FrameEntityC), With<DynamicsConfigC>>();
for (entity, fe) in bodies.iter(world) {
body_of_frame.insert(fe.0, entity);
}
let mut source_of_frame: HashMap<Entity, Entity> = HashMap::new();
let mut sources = world.query_filtered::<(Entity, &FrameEntityC), With<GravitySourceC>>();
for (entity, fe) in sources.iter(world) {
source_of_frame.insert(fe.0, entity);
}
for (pos, rec) in doc.records.iter().enumerate() {
let uid = &doc.uids[rec.uid_index as usize];
let entity = world
.resource::<FrameUidIndexR>()
.get(uid)
.unwrap_or_else(|| {
panic!(
"apply_frame_document: record {pos} (`{}`) has identity `{uid}` but \
this App's frame store has no such frame — rebuild the scenario \
configuration to match the document's frame population (and drive \
app.update() once so registration runs before apply).",
rec.name
)
});
let actual_parent = world.entity(entity).get::<ChildOf>().map(|c| {
world
.entity(c.parent())
.get::<FrameUidC>()
.unwrap_or_else(|| {
panic!(
"apply_frame_document: frame entity {entity:?}'s parent carries \
no FrameUidC — frame entities are stamped at registration."
)
})
.0
.clone()
});
let declared_parent = rec.parent.map(|p| doc.uids[p as usize].clone());
assert!(
actual_parent == declared_parent,
"apply_frame_document: topology mismatch at record {pos} (`{}`): the \
document declares parent {declared_parent:?}, but this App's hierarchy \
has {actual_parent:?}. apply never reparents — rebuild the configuration \
to the document's topology (for a post-frame-switch snapshot: wire the \
body's IntegSourceC to the switch target and pre-flip the gravity \
controls).",
rec.name
);
let state = record_state(rec);
let epoch = record_epoch(rec).unwrap_or_else(|| {
panic!(
"apply_frame_document: record {pos} (`{}`) carries no epoch — \
producer trees stamp every node (issue #662/#664).",
rec.name
)
});
let mut entity_mut = world.entity_mut(entity);
*entity_mut
.get_mut::<FrameTransC>()
.expect("frame entity has FrameTransC") = FrameTransC {
position: state.trans.position,
velocity: state.trans.velocity,
};
*entity_mut
.get_mut::<FrameRotC>()
.expect("frame entity has FrameRotC") = FrameRotC {
q_parent_this: state.rot.q_parent_this,
t_parent_this: state.rot.t_parent_this,
};
*entity_mut
.get_mut::<FrameAngVelC>()
.expect("frame entity has FrameAngVelC") = FrameAngVelC(state.rot.ang_vel_this);
*entity_mut
.get_mut::<FrameEpochC>()
.expect("frame entity has FrameEpochC") = FrameEpochC(epoch);
match &rec.origin {
Origin::Integrated {
attitude_quat,
ang_vel_body,
} => {
let body = *body_of_frame.get(&entity).unwrap_or_else(|| {
panic!(
"apply_frame_document: record {pos} (`{}`) is Integrated but \
frame entity {entity:?} is not a registered body's frame.",
rec.name
)
});
let mut body_mut = world.entity_mut(body);
let mut trans = body_mut
.get_mut::<TranslationalStateC<P>>()
.unwrap_or_else(|| {
panic!(
"apply_frame_document: body {body:?} (record `{}`) has no \
TranslationalStateC<{}> — apply is single-planet, matching \
populate_app's central-planet shape; call \
apply_frame_document::<P> with the App's central planet.",
rec.name,
core::any::type_name::<P>()
)
});
trans.0 = astrodyn::typed_bridge::trans_raw_to_planet::<P>(
&astrodyn::TranslationalState {
position: state.trans.position,
velocity: state.trans.velocity,
},
);
let has_rot = body_mut.get::<RotationalStateC>().is_some();
match (attitude_quat, ang_vel_body, has_rot) {
(Some(q), Some(w), true) => {
let mut rot = body_mut
.get_mut::<RotationalStateC>()
.expect("checked has_rot above");
rot.0 = astrodyn::typed_bridge::rot_raw_to_self_ref(
&astrodyn::RotationalState {
quaternion: astrodyn::JeodQuat::from_array(*q),
ang_vel_body: glam::DVec3::from_array(*w),
},
);
}
(None, None, false) => {}
(payload_q, payload_w, has_rot) => panic!(
"apply_frame_document: record {pos} (`{}`) rotational payload \
(attitude: {}, ang_vel: {}) disagrees with the body's degrees \
of freedom (6-DOF: {has_rot}) — the rebuilt configuration must \
match the producer's.",
rec.name,
payload_q.is_some(),
payload_w.is_some(),
),
}
}
Origin::Derived { .. } | Origin::Injected => {
if let Some(&source) = source_of_frame.get(&entity) {
let mut source_mut = world.entity_mut(source);
if let Some(mut pos_c) = source_mut.get_mut::<SourceInertialPositionC>() {
pos_c.0 = astrodyn::Position::<astrodyn::RootInertial>::from_raw_si(
state.trans.position,
);
}
if let Some(mut vel_c) = source_mut.get_mut::<SourceInertialVelocityC>() {
vel_c.0 = astrodyn::Velocity::<astrodyn::RootInertial>::from_raw_si(
state.trans.velocity,
);
}
if let Some(mut trans_c) = source_mut.get_mut::<TranslationalStateC<P>>() {
trans_c.0 = astrodyn::typed_bridge::trans_raw_to_planet::<P>(
&astrodyn::TranslationalState {
position: state.trans.position,
velocity: state.trans.velocity,
},
);
}
}
}
}
}
}