ability to add keys for selected nodes in the animation editor

- adds `add key` button that captures the current state of the properties that are added in the animation
This commit is contained in:
Dmitry Stepanov
2026-02-24 17:17:05 +03:00
parent d1957934f3
commit b1a87182ed
3 changed files with 239 additions and 88 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 865 B

+46 -7
View File
@@ -65,6 +65,8 @@ use crate::{
ui_scene::UiScene,
Editor, Message,
};
use fyrox::gui::UiNode;
use fyrox::scene::node::Node;
use std::any::{Any, TypeId};
pub mod command;
@@ -74,6 +76,40 @@ mod thumb;
mod toolbar;
mod track;
pub trait TransformProvider {
fn position(&self) -> &[f32];
fn rotation(&self) -> Option<&[f32]>;
fn scale(&self) -> Option<&[f32]>;
}
impl TransformProvider for Node {
fn position(&self) -> &[f32] {
self.local_transform().position().as_slice()
}
fn rotation(&self) -> Option<&[f32]> {
Some(self.local_transform().rotation().coords.as_slice())
}
fn scale(&self) -> Option<&[f32]> {
Some(self.local_transform().scale().as_slice())
}
}
impl TransformProvider for UiNode {
fn position(&self) -> &[f32] {
self.desired_local_position.as_slice()
}
fn rotation(&self) -> Option<&[f32]> {
None
}
fn scale(&self) -> Option<&[f32]> {
None
}
}
pub trait PreviewData {
fn enter(&mut self);
}
@@ -338,7 +374,7 @@ impl AnimationEditor {
) where
P: PrefabData<Graph = G> + AnimationSource<Node = N, SceneGraph = G, Prefab = P>,
G: SceneGraph<Node = N, Prefab = P>,
N: SceneGraphNode<SceneGraph = G, ResourceData = P>,
N: SceneGraphNode<SceneGraph = G, ResourceData = P> + TransformProvider,
{
let selection = fetch_selection(self, graph, editor_selection);
@@ -544,8 +580,15 @@ impl AnimationEditor {
}
}
self.track_list
.handle_ui_message(message, &selection, root, sender, ui, graph);
self.track_list.handle_ui_message(
message,
editor_selection,
&selection,
root,
sender,
ui,
graph,
);
}
self.toolbar.post_handle_ui_message(
@@ -673,10 +716,6 @@ impl AnimationEditor {
G: SceneGraph<Node = N>,
N: SceneGraphNode<SceneGraph = G>,
{
if !self.is_in_preview_mode() {
return;
}
let selection = fetch_selection(self, graph, editor_selection);
if let Some(container) = animation_container_ref(graph, selection.animation_player) {
+193 -81
View File
@@ -20,6 +20,8 @@
#![allow(clippy::manual_map)]
use crate::plugins::animation::command::ReplaceTrackCurveCommand;
use crate::plugins::animation::TransformProvider;
use crate::{
command::{Command, CommandGroup},
fyrox::{
@@ -99,6 +101,8 @@ use crate::{
},
utils::{self},
};
use fyrox::core::math::curve::{CurveKey, CurveKeyKind};
use fyrox::core::{ok_or_continue, some_or_return};
use fyrox::gui::utils::ImageButtonBuilder;
use std::{
any::TypeId,
@@ -581,6 +585,7 @@ pub struct TrackList {
property_binding_mode: PropertyBindingMode,
scroll_viewer: Handle<ScrollViewer>,
selected_animation: ErasedHandle,
add_key: Handle<Button>,
}
#[derive(Clone)]
@@ -604,10 +609,43 @@ impl TrackList {
let toolbar = Toolbar::new(ctx);
let tree_root;
let add_track;
let add_position_track;
let add_rotation_track;
let add_scale_track;
let add_track = ImageButtonBuilder::default()
.with_image(load_image!("../../../resources/property_track.png"))
.with_tooltip(
"Add Property Track.\nCreate generic property binding to a numeric property.",
)
.with_tab_index(Some(0))
.build_button(ctx);
let add_position_track = ImageButtonBuilder::default()
.with_image(load_image!("../../../resources/position_track.png"))
.with_tooltip(
"Add Position Track.\nCreates a binding to a local position of a node. Such \
binding is much more performant than generic property binding",
)
.with_tab_index(Some(1))
.build_button(ctx);
let add_rotation_track = ImageButtonBuilder::default()
.with_image(load_image!("../../../resources/rotation_track.png"))
.with_tooltip(
"Add Rotation Track.\nCreates a binding to a local rotation of a node. \
Such binding is much more performant than generic property binding",
)
.with_tab_index(Some(3))
.build_button(ctx);
let add_scale_track = ImageButtonBuilder::default()
.with_image(load_image!("../../../resources/scaling_track.png"))
.with_tooltip(
"Add Scale Track.\nCreates a binding to a local scale of a node. Such \
binding is much more performant than generic property binding",
)
.with_tab_index(Some(2))
.build_button(ctx);
let add_key = ImageButtonBuilder::default()
.with_image_color(Color::GREEN_YELLOW)
.with_image(load_image!("../../../resources/key.png"))
.with_tooltip("Add key.\nCaptures the state of all properties of the selected object")
.with_tab_index(Some(4))
.build_button(ctx);
let scroll_viewer;
let panel = GridBuilder::new(
@@ -640,64 +678,11 @@ impl TrackList {
StackPanelBuilder::new(
WidgetBuilder::new()
.with_margin(Thickness::uniform(2.0))
.with_child({
add_track = ImageButtonBuilder::default()
.with_image(load_image!(
"../../../resources/property_track.png"
))
.with_tooltip(
"Add Property Track.\n\
Create generic property binding to a numeric property.",
)
.with_tab_index(Some(0))
.build_button(ctx);
add_track
})
.with_child({
add_position_track = ImageButtonBuilder::default()
.with_image(load_image!(
"../../../resources/position_track.png"
))
.with_tooltip(
"Add Position Track.\n\
Creates a binding to a local position of a node. \
Such binding is much more performant than generic \
property binding",
)
.with_tab_index(Some(1))
.build_button(ctx);
add_position_track
})
.with_child({
add_scale_track = ImageButtonBuilder::default()
.with_image(load_image!(
"../../../resources/scaling_track.png"
))
.with_tooltip(
"Add Scale Track.\n\
Creates a binding to a local scale of a node. \
Such binding is much more performant than generic \
property binding",
)
.with_tab_index(Some(2))
.build_button(ctx);
add_scale_track
})
.with_child({
add_rotation_track = ImageButtonBuilder::default()
.with_image(load_image!(
"../../../resources/rotation_track.png"
))
.with_tooltip(
"Add Rotation Track.\n\
Creates a binding to a local rotation of a node. \
Such binding is much more performant than generic \
property binding",
)
.with_tab_index(Some(3))
.build_button(ctx);
add_rotation_track
}),
.with_child(add_track)
.with_child(add_position_track)
.with_child(add_scale_track)
.with_child(add_rotation_track)
.with_child(add_key),
)
.with_orientation(Orientation::Horizontal)
.build(ctx),
@@ -732,12 +717,132 @@ impl TrackList {
property_binding_mode: PropertyBindingMode::Generic,
scroll_viewer,
selected_animation: Default::default(),
add_key,
}
}
fn on_add_key_pressed<G, N>(
&self,
selection: &Selection,
animation_player: Handle<N>,
animation_handle: Handle<Animation<Handle<N>>>,
graph: &G,
sender: &MessageSender,
) where
G: SceneGraph<Node = N>,
N: SceneGraphNode<SceneGraph = G> + TransformProvider,
{
let nodes = if let Some(ui) = selection.as_ui() {
ui.widgets
.iter()
.map(|h| ErasedHandle::from(*h))
.collect::<Vec<_>>()
} else if let Some(scene) = selection.as_graph() {
scene
.nodes
.iter()
.map(|h| ErasedHandle::from(*h))
.collect::<Vec<_>>()
} else {
return;
};
let animation = some_or_return!(animation_container_ref(graph, animation_player)
.and_then(|c| c.try_get(animation_handle).ok()));
let state = animation.tracks_data().state();
let tracks_data = some_or_return!(state.data_ref());
let mut commands = vec![];
for node_handle in nodes {
let node_handle = Handle::<N>::from(node_handle);
let node_ref = ok_or_continue!(graph.try_get(node_handle));
for (track_id, binding) in animation.track_bindings() {
if binding.target != node_handle {
continue;
};
for track in tracks_data.tracks() {
if track.id() != *track_id {
continue;
}
fn add_keys<G, N>(
animation_player: Handle<N>,
animation_handle: Handle<Animation<Handle<N>>>,
animation: &Animation<Handle<N>>,
track: &Track,
values: &[f32],
commands: &mut Vec<Command>,
) where
G: SceneGraph<Node = N>,
N: SceneGraphNode<SceneGraph = G> + TransformProvider,
{
for (curve, current_value) in
track.data_container().curves_ref().iter().zip(values)
{
let mut curve_clone = curve.clone();
curve_clone.add_key(CurveKey::new(
animation.time_position(),
*current_value,
CurveKeyKind::Linear,
));
commands.push(Command::new(ReplaceTrackCurveCommand {
animation_player,
animation: animation_handle,
curve: curve_clone,
}));
}
}
match track.value_binding() {
ValueBinding::Position => add_keys(
animation_player,
animation_handle,
animation,
track,
node_ref.position(),
&mut commands,
),
ValueBinding::Scale => {
if let Some(values) = node_ref.scale() {
add_keys(
animation_player,
animation_handle,
animation,
track,
values,
&mut commands,
)
}
}
ValueBinding::Rotation => {
if let Some(values) = node_ref.rotation() {
add_keys(
animation_player,
animation_handle,
animation,
track,
values,
&mut commands,
)
}
}
ValueBinding::Property { .. } => {
// TODO.
}
}
}
}
}
sender.do_command_group(commands);
}
pub fn handle_ui_message<G, N>(
&mut self,
message: &UiMessage,
editor_selection: &Selection,
selection: &AnimationSelection<N>,
root: Handle<N>,
sender: &MessageSender,
@@ -745,7 +850,7 @@ impl TrackList {
graph: &G,
) where
G: SceneGraph<Node = N>,
N: SceneGraphNode<SceneGraph = G>,
N: SceneGraphNode<SceneGraph = G> + TransformProvider,
{
let selected_animation = animation_container_ref(graph, selection.animation_player)
.and_then(|c| c.try_get(selection.animation).ok());
@@ -793,6 +898,14 @@ impl TrackList {
ui.send(self.tree_root, TreeRootMessage::ExpandAll);
} else if message.destination() == self.toolbar.collapse_all {
ui.send(self.tree_root, TreeRootMessage::CollapseAll);
} else if message.destination() == self.add_key {
self.on_add_key_pressed(
editor_selection,
selection.animation_player,
selection.animation,
graph,
sender,
);
}
} else if let Some(SearchBarMessage::Text(text)) =
message.data_from(self.toolbar.search_bar)
@@ -1304,18 +1417,17 @@ impl TrackList {
.values()
.all(|v| ui[*v].id != model_track.id())
{
let parent_group =
match self.group_views.entry(model_track_binding.target().into()) {
Entry::Occupied(entry) => *entry.get(),
Entry::Vacant(entry) => {
let ctx = &mut ui.build_ctx();
let group = TreeBuilder::new(WidgetBuilder::new())
.with_content(
TextBuilder::new(
WidgetBuilder::new().with_vertical_alignment(
VerticalAlignment::Center,
),
)
let parent_group = match self
.group_views
.entry(model_track_binding.target().into())
{
Entry::Occupied(entry) => *entry.get(),
Entry::Vacant(entry) => {
let ctx = &mut ui.build_ctx();
let group = TreeBuilder::new(WidgetBuilder::new())
.with_content(
TextBuilder::new(WidgetBuilder::new())
.with_vertical_text_alignment(VerticalAlignment::Center)
.with_text(format!(
"{} ({}:{})",
graph
@@ -1326,12 +1438,12 @@ impl TrackList {
model_track_binding.target().generation()
))
.build(ctx),
)
.build(ctx);
ui.send_sync(self.tree_root, TreeRootMessage::AddItem(group));
*entry.insert(group)
}
};
)
.build(ctx);
ui.send_sync(self.tree_root, TreeRootMessage::AddItem(group));
*entry.insert(group)
}
};
let ctx = &mut ui.build_ctx();