use astrodyn_frames::{FrameId, FrameTree};
use astrodyn_quantities::frame_descriptor::FrameUid;
pub use astrodyn_frames::{
compute_relative_state_typed, frame_origin, frame_origin_typed, sync_pfix_rotation,
};
use crate::vehicle_config::{FrameSwitchConfig, SwitchSense};
use crate::GravityControls;
use crate::TranslationalState;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FrameSwitchTargetMissing {
pub body_idx: usize,
pub target: FrameUid,
pub num_sources: usize,
}
#[allow(clippy::too_many_arguments)]
pub fn evaluate_and_apply_frame_switch<F>(
frame_tree: &mut FrameTree,
root_frame_id: FrameId,
body_frame_id: FrameId,
integ_frame_id: &mut FrameId,
trans: &mut TranslationalState,
frame_switches: &mut [FrameSwitchConfig],
gravity_controls: &mut GravityControls,
resolve_target: F,
num_sources: usize,
body_idx: usize,
) -> Result<bool, FrameSwitchTargetMissing>
where
F: Fn(&FrameUid) -> Option<FrameId>,
{
if frame_switches.is_empty() {
return Ok(false);
}
let mut switch_idx = None;
for (idx, sw) in frame_switches.iter().enumerate() {
if !sw.active {
continue;
}
assert!(
sw.target.class.may_be_root_or_integ(),
"evaluate_and_apply_frame_switch: body {body_idx} switch target `{}` has \
class {:?}, which may not host integration — switch targets must be a \
gravity source's inertial frame (RootInertial / PlanetInertial / \
BarycenterInertial). Fix the FrameSwitchConfig target identity.",
sw.target,
sw.target.class,
);
let target_fid = resolve_target(&sw.target).ok_or_else(|| FrameSwitchTargetMissing {
body_idx,
target: sw.target.clone(),
num_sources,
})?;
let (target_origin, _) = frame_origin(frame_tree, root_frame_id, target_fid);
let (current_origin, _) = frame_origin(frame_tree, root_frame_id, *integ_frame_id);
let body_pos_eci = trans.position + current_origin;
let threshold_sq = sw.switch_distance * sw.switch_distance;
let triggered = match sw.switch_sense {
SwitchSense::OnApproach => {
(body_pos_eci - target_origin).length_squared() < threshold_sq
}
SwitchSense::OnDeparture => trans.position.length_squared() > threshold_sq,
};
if triggered {
switch_idx = Some(idx);
break;
}
}
let Some(idx) = switch_idx else {
return Ok(false);
};
let target = frame_switches[idx].target.clone();
frame_switches[idx].active = false;
let new_integ_fid = resolve_target(&target).expect(
"evaluate_and_apply_frame_switch: target source resolved during evaluation \
but failed during application — caller-side mutation between lookups",
);
frame_tree.reparent(body_frame_id, new_integ_fid);
let new_state = frame_tree.get(body_frame_id).state;
trans.position = new_state.trans.position;
trans.velocity = new_state.trans.velocity;
*integ_frame_id = new_integ_fid;
for ctrl in &mut gravity_controls.controls {
ctrl.differential = ctrl.source != target;
}
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
use astrodyn_quantities::frame::{Earth, Moon, PlanetInertial, RootInertial};
use astrodyn_quantities::frame_descriptor::FrameUid;
fn switch_fixture() -> (
astrodyn_frames::FrameTree,
astrodyn_frames::FrameId,
astrodyn_frames::FrameId,
astrodyn_frames::FrameId,
) {
let mut tree = astrodyn_frames::FrameTree::new();
let root = tree.add_root_typed::<RootInertial>("root".into());
let earth = tree.add_child_uid(
root,
FrameUid::of::<PlanetInertial<Earth>>(),
"Earth.inertial".into(),
astrodyn_frames::RefFrameState::default(),
None,
);
let body = tree.add_child_uid(
earth,
crate::named_body_frame_uid("switch-test-body"),
"body".into(),
astrodyn_frames::RefFrameState::default(),
None,
);
(tree, root, earth, body)
}
#[test]
#[should_panic(expected = "may not host integration")]
fn frame_switch_rejects_non_inertial_class_target() {
let (mut tree, root, earth, body) = switch_fixture();
let mut integ = earth;
let mut trans = TranslationalState::default();
let mut switches = [FrameSwitchConfig {
target: crate::named_body_frame_uid("some-other-body"),
switch_sense: SwitchSense::OnApproach,
switch_distance: 1.0e9,
active: true,
}];
let mut controls = GravityControls::default();
let _ = evaluate_and_apply_frame_switch(
&mut tree,
root,
body,
&mut integ,
&mut trans,
&mut switches,
&mut controls,
|_| None,
1,
0,
);
}
#[test]
fn frame_switch_unresolved_target_errors_with_identity() {
let (mut tree, root, earth, body) = switch_fixture();
let mut integ = earth;
let mut trans = TranslationalState::default();
let moon = FrameUid::of::<PlanetInertial<Moon>>();
let mut switches = [FrameSwitchConfig {
target: moon.clone(),
switch_sense: SwitchSense::OnApproach,
switch_distance: 1.0e9,
active: true,
}];
let mut controls = GravityControls::default();
let err = evaluate_and_apply_frame_switch(
&mut tree,
root,
body,
&mut integ,
&mut trans,
&mut switches,
&mut controls,
|_| None, 1,
0,
)
.unwrap_err();
assert_eq!(err.target, moon, "the error names the missing identity");
assert_eq!(err.body_idx, 0);
}
}