use bevy::prelude::*;
use jackdaw_animation::{
AnimationTrack, Clip, F32Keyframe, QuatKeyframe, SelectedClip, TimelineCursor, TimelineDirty,
Vec3Keyframe,
};
use jackdaw_feathers::button::{ButtonClickEvent, ButtonProps, ButtonSize, ButtonVariant, button};
use jackdaw_feathers::icons::Icon;
use super::InspectorFieldRow;
use crate::prelude::*;
const CURSOR_ON_KEYFRAME_EPS: f32 = 0.02;
const TRANSFORM: &str = "bevy_transform::components::transform::Transform";
const ANIMATABLE_FIELDS: &[(&str, &str)] = &[
(TRANSFORM, "translation"),
(TRANSFORM, "rotation"),
(TRANSFORM, "scale"),
];
#[derive(Component, Clone, Debug)]
pub struct AnimDiamondButton {
pub source_entity: Entity,
pub component_type_path: String,
pub field_path: String,
}
fn is_animatable(component_type_path: &str, field_path: &str) -> bool {
ANIMATABLE_FIELDS
.iter()
.any(|(t, f)| *t == component_type_path && *f == field_path)
}
pub fn decorate_animatable_fields(
new_rows: Query<(Entity, &InspectorFieldRow), Added<InspectorFieldRow>>,
mut commands: Commands,
) {
for (row_entity, row) in &new_rows {
if !is_animatable(&row.type_path, &row.field_path) {
continue;
}
let wrapper = commands
.spawn((Node {
position_type: PositionType::Absolute,
top: Val::Px(0.0),
right: Val::Px(4.0),
..default()
},))
.id();
let button_entity = commands
.spawn((
AnimDiamondButton {
source_entity: row.source_entity,
component_type_path: row.type_path.clone(),
field_path: row.field_path.clone(),
},
button(
ButtonProps::new("")
.with_variant(ButtonVariant::Ghost)
.with_size(ButtonSize::IconSM)
.with_left_icon(Icon::Diamond),
),
))
.id();
jackdaw_feathers::utils::attach_or_despawn(&mut commands, wrapper, button_entity);
jackdaw_feathers::utils::attach_or_despawn(&mut commands, row_entity, wrapper);
}
}
pub fn on_diamond_click(
event: On<ButtonClickEvent>,
buttons: Query<&AnimDiamondButton>,
mut commands: Commands,
) {
let Ok(button_ref) = buttons.get(event.entity) else {
return;
};
commands
.operator(super::ops::AnimationToggleKeyframeOp::ID)
.param("entity", button_ref.source_entity)
.param(
"component_type_path",
button_ref.component_type_path.clone(),
)
.param("field_path", button_ref.field_path.clone())
.call();
}
pub(crate) fn toggle_keyframe(
In((source_entity, component_type_path, field_path)): In<(Entity, String, String)>,
world: &mut World,
) {
let cursor_time = world
.get_resource::<TimelineCursor>()
.map(|c| c.seek_time)
.unwrap_or(0.0);
let Some(clip_entity) = find_or_create_clip(world, source_entity) else {
warn!(
"toggle_keyframe: source entity {source_entity} has no Name - \
give it one in the inspector first so the clip's target can \
resolve"
);
return;
};
let track_entity = find_or_create_track(world, clip_entity, &component_type_path, &field_path);
world
.run_system_cached_with(
spawn_typed_keyframe,
(
source_entity,
track_entity,
component_type_path.clone(),
field_path.clone(),
cursor_time,
),
)
.ok();
if let Some(mut clip) = world.get_mut::<Clip>(clip_entity)
&& cursor_time > clip.duration
{
clip.duration = cursor_time;
}
if let Some(mut selected) = world.get_resource_mut::<SelectedClip>() {
selected.0 = Some(clip_entity);
}
if let Some(mut dirty) = world.get_resource_mut::<TimelineDirty>() {
dirty.0 = true;
}
}
fn find_or_create_clip(world: &mut World, source_entity: Entity) -> Option<Entity> {
let target_name = world
.get::<Name>(source_entity)
.map(|n| n.as_str().to_string())?;
if let Some(children) = world.get::<Children>(source_entity) {
let children_vec: Vec<Entity> = children.iter().collect();
for child in children_vec {
if world.get::<Clip>(child).is_some() {
return Some(child);
}
}
}
let clip = world
.spawn((
Clip::default(),
Name::new(format!("{target_name} Clip")),
ChildOf(source_entity),
))
.id();
Some(clip)
}
fn find_or_create_track(
world: &mut World,
clip_entity: Entity,
component_type_path: &str,
field_path: &str,
) -> Entity {
if let Some(children) = world.get::<Children>(clip_entity) {
let children_vec: Vec<Entity> = children.iter().collect();
for child in children_vec {
if let Some(track) = world.get::<AnimationTrack>(child)
&& track.component_type_path == component_type_path
&& track.field_path == field_path
{
return child;
}
}
}
let label = format!("/ {field_path}");
world
.spawn((
AnimationTrack::new(component_type_path.to_string(), field_path.to_string()),
Name::new(label),
ChildOf(clip_entity),
))
.id()
}
fn spawn_typed_keyframe(
In((source_entity, track_entity, component_type_path, field_path, time)): In<(
Entity,
Entity,
String,
String,
f32,
)>,
transforms: Query<&Transform>,
mut commands: Commands,
) {
match (component_type_path.as_str(), field_path.as_str()) {
(TRANSFORM, "translation") => {
let Ok(&transform) = transforms.get(source_entity) else {
warn!("Diamond click: source has no Transform");
return;
};
commands.spawn((
Vec3Keyframe {
time,
value: transform.translation,
},
ChildOf(track_entity),
));
}
(TRANSFORM, "rotation") => {
let Ok(&transform) = transforms.get(source_entity) else {
warn!("Diamond click: source has no Transform");
return;
};
commands.spawn((
QuatKeyframe {
time,
value: transform.rotation,
},
ChildOf(track_entity),
));
}
(TRANSFORM, "scale") => {
let Ok(&transform) = transforms.get(source_entity) else {
warn!("Diamond click: source has no Transform");
return;
};
commands.spawn((
Vec3Keyframe {
time,
value: transform.scale,
},
ChildOf(track_entity),
));
}
_ => {
let _ = F32Keyframe::default();
warn!("Diamond click: no snapshot dispatch for {component_type_path}.{field_path}",);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DiamondState {
NoTrack,
HasTrack,
OnKeyframe,
}
pub fn update_anim_diamond_highlights(
buttons: Query<(Entity, &AnimDiamondButton)>,
children_query: Query<&Children>,
clips: Query<(), With<Clip>>,
tracks: Query<&AnimationTrack>,
vec3_keyframes: Query<&Vec3Keyframe>,
quat_keyframes: Query<&QuatKeyframe>,
f32_keyframes: Query<&F32Keyframe>,
cursor: Res<TimelineCursor>,
mut text_colors: Query<&mut TextColor>,
) {
for (btn_entity, btn) in &buttons {
let state = compute_diamond_state(
btn,
&children_query,
&clips,
&tracks,
&vec3_keyframes,
&quat_keyframes,
&f32_keyframes,
cursor.seek_time,
);
let color = match state {
DiamondState::NoTrack => Color::srgba(0.55, 0.55, 0.55, 0.65),
DiamondState::HasTrack => Color::srgb(0.38, 0.72, 1.0),
DiamondState::OnKeyframe => Color::srgb(1.0, 0.78, 0.12),
};
recolor_button_icon(btn_entity, color, &children_query, &mut text_colors);
}
}
fn recolor_button_icon(
root: Entity,
color: Color,
children_query: &Query<&Children>,
text_colors: &mut Query<&mut TextColor>,
) {
let Ok(children) = children_query.get(root) else {
return;
};
for child in children.iter() {
if let Ok(mut tc) = text_colors.get_mut(child) {
tc.0 = color;
}
recolor_button_icon(child, color, children_query, text_colors);
}
}
fn compute_diamond_state(
btn: &AnimDiamondButton,
children_query: &Query<&Children>,
clips: &Query<(), With<Clip>>,
tracks: &Query<&AnimationTrack>,
vec3_keyframes: &Query<&Vec3Keyframe>,
quat_keyframes: &Query<&QuatKeyframe>,
f32_keyframes: &Query<&F32Keyframe>,
cursor_time: f32,
) -> DiamondState {
let Ok(source_children) = children_query.get(btn.source_entity) else {
return DiamondState::NoTrack;
};
let clip_entity = source_children.iter().find(|c| clips.contains(*c));
let Some(clip_entity) = clip_entity else {
return DiamondState::NoTrack;
};
let Ok(clip_children) = children_query.get(clip_entity) else {
return DiamondState::NoTrack;
};
let track_entity = clip_children.iter().find(|c| {
tracks
.get(*c)
.map(|t| {
t.component_type_path == btn.component_type_path && t.field_path == btn.field_path
})
.unwrap_or(false)
});
let Some(track_entity) = track_entity else {
return DiamondState::NoTrack;
};
let Ok(track_children) = children_query.get(track_entity) else {
return DiamondState::HasTrack;
};
for kf in track_children.iter() {
let t = vec3_keyframes
.get(kf)
.map(|k| k.time)
.or_else(|_| quat_keyframes.get(kf).map(|k| k.time))
.or_else(|_| f32_keyframes.get(kf).map(|k| k.time))
.ok();
if let Some(t) = t
&& (t - cursor_time).abs() < CURSOR_ON_KEYFRAME_EPS
{
return DiamondState::OnKeyframe;
}
}
DiamondState::HasTrack
}