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;
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()
},
ChildOf(row_entity),
))
.id();
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),
),
ChildOf(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;
};
let source_entity = button_ref.source_entity;
let component_type_path = button_ref.component_type_path.clone();
let field_path = button_ref.field_path.clone();
commands.queue(move |world: &mut World| {
let cursor_time = world
.get_resource::<TimelineCursor>()
.map(|c| c.seek_time)
.unwrap_or(0.0);
let clip_entity = find_or_create_clip(world, source_entity);
let Some(clip_entity) = clip_entity else {
warn!(
"Diamond click: 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);
spawn_typed_keyframe(
world,
source_entity,
track_entity,
&component_type_path,
&field_path,
cursor_time,
);
if let Some(mut clip) = world.get_mut::<Clip>(clip_entity) {
if 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) {
if 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(
world: &mut World,
source_entity: Entity,
track_entity: Entity,
component_type_path: &str,
field_path: &str,
time: f32,
) {
match (component_type_path, field_path) {
(TRANSFORM, "translation") => {
let Some(transform) = world.get::<Transform>(source_entity).copied() else {
warn!("Diamond click: source has no Transform");
return;
};
world.spawn((
Vec3Keyframe {
time,
value: transform.translation,
},
ChildOf(track_entity),
));
}
(TRANSFORM, "rotation") => {
let Some(transform) = world.get::<Transform>(source_entity).copied() else {
warn!("Diamond click: source has no Transform");
return;
};
world.spawn((
QuatKeyframe {
time,
value: transform.rotation,
},
ChildOf(track_entity),
));
}
(TRANSFORM, "scale") => {
let Some(transform) = world.get::<Transform>(source_entity).copied() else {
warn!("Diamond click: source has no Transform");
return;
};
world.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);
}
}
#[allow(clippy::too_many_arguments)]
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 {
if (t - cursor_time).abs() < CURSOR_ON_KEYFRAME_EPS {
return DiamondState::OnKeyframe;
}
}
}
DiamondState::HasTrack
}