1198 lines
No EOL
30 KiB
C++
1198 lines
No EOL
30 KiB
C++
#include "animation_editor.h"
|
|
#include "animation/animation.h"
|
|
#include "animation/animation_scene.h"
|
|
#include "animation/controller.h"
|
|
#include "animation/editor/state_machine_editor.h"
|
|
#include "animation/events.h"
|
|
#include "animation/state_machine.h"
|
|
#include "editor/asset_browser.h"
|
|
#include "editor/ieditor_command.h"
|
|
#include "editor/platform_interface.h"
|
|
#include "editor/property_grid.h"
|
|
#include "editor/utils.h"
|
|
#include "editor/world_editor.h"
|
|
#include "engine/blob.h"
|
|
#include "engine/crc32.h"
|
|
#include "engine/engine.h"
|
|
#include "engine/fs/os_file.h"
|
|
#include "engine/hash_map.h"
|
|
#include "engine/log.h"
|
|
#include "engine/path.h"
|
|
#include "engine/path_utils.h"
|
|
#include "engine/plugin_manager.h"
|
|
#include "engine/properties.h"
|
|
#include "engine/resource_manager.h"
|
|
#include "engine/universe/universe.h"
|
|
#include <SDL.h>
|
|
|
|
|
|
static ImVec2 operator+(const ImVec2& lhs, const ImVec2& rhs) { return ImVec2(lhs.x + rhs.x, lhs.y + rhs.y); }
|
|
|
|
|
|
namespace Lumix
|
|
{
|
|
|
|
|
|
static const ComponentType ANIMABLE_HASH = Properties::getComponentType("animable");
|
|
static const ComponentType CONTROLLER_TYPE = Properties::getComponentType("anim_controller");
|
|
static const ResourceType ANIMATION_TYPE("animation");
|
|
static const ResourceType CONTROLLER_RESOURCE_TYPE("anim_controller");
|
|
|
|
|
|
namespace AnimEditor
|
|
{
|
|
|
|
|
|
struct BeginGroupCommand LUMIX_FINAL : IEditorCommand
|
|
{
|
|
BeginGroupCommand() {}
|
|
BeginGroupCommand(WorldEditor&) {}
|
|
|
|
bool execute() override { return true; }
|
|
void undo() override { ASSERT(false); }
|
|
void serialize(JsonSerializer& serializer) override {}
|
|
void deserialize(JsonSerializer& serializer) override {}
|
|
bool merge(IEditorCommand& command) override { ASSERT(false); return false; }
|
|
const char* getType() override { return "begin_group"; }
|
|
};
|
|
|
|
|
|
struct EndGroupCommand LUMIX_FINAL : IEditorCommand
|
|
{
|
|
EndGroupCommand() {}
|
|
EndGroupCommand(WorldEditor&) {}
|
|
|
|
bool execute() override { return true; }
|
|
void undo() override { ASSERT(false); }
|
|
void serialize(JsonSerializer& serializer) override {}
|
|
void deserialize(JsonSerializer& serializer) override {}
|
|
bool merge(IEditorCommand& command) override { ASSERT(false); return false; }
|
|
const char* getType() override { return "end_group"; }
|
|
|
|
u32 group_type;
|
|
};
|
|
|
|
|
|
struct MoveAnimNodeCommand : IEditorCommand
|
|
{
|
|
MoveAnimNodeCommand(ControllerResource& controller, Node* node, const ImVec2& pos)
|
|
: m_controller(controller)
|
|
, m_node_uid(node->engine_cmp->uid)
|
|
, m_new_pos(pos)
|
|
, m_old_pos(node->pos)
|
|
{
|
|
}
|
|
|
|
|
|
bool execute() override
|
|
{
|
|
auto* node = (Node*)m_controller.getByUID(m_node_uid);
|
|
node->pos = m_new_pos;
|
|
return true;
|
|
}
|
|
|
|
|
|
void undo() override
|
|
{
|
|
auto* node = (Node*)m_controller.getByUID(m_node_uid);
|
|
node->pos = m_old_pos;
|
|
}
|
|
|
|
|
|
const char* getType() override { return "move_anim_node"; }
|
|
|
|
|
|
bool merge(IEditorCommand& command) override
|
|
{
|
|
auto& cmd = (MoveAnimNodeCommand&)command;
|
|
if (m_node_uid != cmd.m_node_uid || &cmd.m_controller != &m_controller) return false;
|
|
cmd.m_new_pos = m_new_pos;
|
|
return true;
|
|
}
|
|
|
|
|
|
void serialize(JsonSerializer& serializer) override
|
|
{
|
|
// TODO
|
|
ASSERT(false);
|
|
}
|
|
|
|
|
|
void deserialize(JsonSerializer& serializer) override
|
|
{
|
|
// TODO
|
|
ASSERT(false);
|
|
}
|
|
|
|
ControllerResource& m_controller;
|
|
int m_node_uid;
|
|
ImVec2 m_new_pos;
|
|
ImVec2 m_old_pos;
|
|
};
|
|
|
|
|
|
struct CreateAnimNodeCommand : IEditorCommand
|
|
{
|
|
CreateAnimNodeCommand(ControllerResource& controller,
|
|
Container* container,
|
|
Anim::Component::Type type,
|
|
const ImVec2& pos)
|
|
: m_controller(controller)
|
|
, m_node_uid(-1)
|
|
, m_container_uid(container->engine_cmp->uid)
|
|
, m_type(type)
|
|
, m_pos(pos)
|
|
{
|
|
}
|
|
|
|
|
|
bool execute() override
|
|
{
|
|
auto* container = (Container*)m_controller.getByUID(m_container_uid);
|
|
if (m_node_uid < 0) m_node_uid = m_controller.createUID();
|
|
container->createNode(m_type, m_node_uid, m_pos);
|
|
return true;
|
|
}
|
|
|
|
|
|
void undo() override
|
|
{
|
|
auto* container = (Container*)m_controller.getByUID(m_container_uid);
|
|
container->destroyChild(m_node_uid);
|
|
}
|
|
|
|
|
|
const char* getType() override { return "create_anim_node"; }
|
|
|
|
|
|
bool merge(IEditorCommand& command) override { return false; }
|
|
|
|
|
|
void serialize(JsonSerializer& serializer) override
|
|
{
|
|
// TODO
|
|
ASSERT(false);
|
|
}
|
|
|
|
|
|
void deserialize(JsonSerializer& serializer) override
|
|
{
|
|
// TODO
|
|
ASSERT(false);
|
|
}
|
|
|
|
ControllerResource& m_controller;
|
|
int m_container_uid;
|
|
int m_node_uid;
|
|
ImVec2 m_pos;
|
|
Anim::Component::Type m_type;
|
|
};
|
|
|
|
|
|
struct DestroyAnimEdgeCommand : IEditorCommand
|
|
{
|
|
DestroyAnimEdgeCommand(ControllerResource& controller, int edge_uid)
|
|
: m_controller(controller)
|
|
, m_edge_uid(edge_uid)
|
|
, m_original_values(controller.getAllocator())
|
|
{
|
|
Edge* edge = (Edge*)m_controller.getByUID(edge_uid);
|
|
ASSERT(!edge->isNode());
|
|
Container* parent = edge->getParent();
|
|
ASSERT(parent);
|
|
m_original_container_uid = parent->engine_cmp->uid;
|
|
m_from_uid = edge->getFrom()->engine_cmp->uid;
|
|
m_to_uid = edge->getTo()->engine_cmp->uid;
|
|
}
|
|
|
|
|
|
bool execute() override
|
|
{
|
|
m_original_values.clear();
|
|
Edge* edge = (Edge*)m_controller.getByUID(m_edge_uid);
|
|
edge->serialize(m_original_values);
|
|
LUMIX_DELETE(m_controller.getAllocator(), edge);
|
|
return true;
|
|
}
|
|
|
|
|
|
void undo() override
|
|
{
|
|
|
|
Container* container = (Container*)m_controller.getByUID(m_original_container_uid);
|
|
container->createEdge(m_from_uid, m_to_uid, m_edge_uid);
|
|
Edge* edge = (Edge*)container->getByUID(m_edge_uid);
|
|
InputBlob input(m_original_values);
|
|
edge->deserialize(input);
|
|
}
|
|
|
|
|
|
const char* getType() override { return "destroy_anim_edge"; }
|
|
|
|
|
|
bool merge(IEditorCommand& command) override { return false; }
|
|
|
|
|
|
void serialize(JsonSerializer& serializer) override
|
|
{
|
|
ASSERT(false);
|
|
}
|
|
|
|
|
|
void deserialize(JsonSerializer& serializer) override
|
|
{
|
|
ASSERT(false);
|
|
}
|
|
|
|
ControllerResource& m_controller;
|
|
int m_edge_uid;
|
|
int m_from_uid;
|
|
int m_to_uid;
|
|
OutputBlob m_original_values;
|
|
int m_original_container_uid;
|
|
};
|
|
|
|
|
|
struct DestroyNodeCommand : IEditorCommand
|
|
{
|
|
DestroyNodeCommand(ControllerResource& controller, int node_uid)
|
|
: m_controller(controller)
|
|
, m_node_uid(node_uid)
|
|
, m_original_values(controller.getAllocator())
|
|
{
|
|
Component* cmp = m_controller.getByUID(m_node_uid);
|
|
ASSERT(cmp->isNode());
|
|
Container* parent = cmp->getParent();
|
|
ASSERT(parent);
|
|
m_original_container = parent->engine_cmp->uid;
|
|
}
|
|
|
|
|
|
bool execute() override
|
|
{
|
|
m_original_values.clear();
|
|
Node* node = (Node*)m_controller.getByUID(m_node_uid);
|
|
node->serialize(m_original_values);
|
|
m_cmp_type = node->engine_cmp->type;
|
|
ASSERT(node->getEdges().empty());
|
|
ASSERT(node->getInEdges().empty());
|
|
LUMIX_DELETE(m_controller.getAllocator(), node);
|
|
return true;
|
|
}
|
|
|
|
|
|
void undo() override
|
|
{
|
|
auto* container = (Container*)m_controller.getByUID(m_original_container);
|
|
container->createNode(m_cmp_type, m_node_uid, ImVec2(0, 0));
|
|
Component* cmp = m_controller.getByUID(m_node_uid);
|
|
ASSERT(cmp->isNode());
|
|
Node* node = (Node*)cmp;
|
|
InputBlob input(m_original_values);
|
|
cmp->deserialize(input);
|
|
}
|
|
|
|
|
|
const char* getType() override { return "destroy_anim_node"; }
|
|
|
|
|
|
bool merge(IEditorCommand& command) override { return false; }
|
|
|
|
|
|
void serialize(JsonSerializer& serializer) override
|
|
{
|
|
// TODO
|
|
ASSERT(false);
|
|
}
|
|
|
|
|
|
void deserialize(JsonSerializer& serializer) override
|
|
{
|
|
// TODO
|
|
ASSERT(false);
|
|
}
|
|
|
|
ControllerResource& m_controller;
|
|
int m_node_uid;
|
|
OutputBlob m_original_values;
|
|
int m_original_container;
|
|
Anim::Component::Type m_cmp_type;
|
|
};
|
|
|
|
|
|
|
|
struct CreateAnimEdgeCommand : IEditorCommand
|
|
{
|
|
CreateAnimEdgeCommand(ControllerResource& controller, Container* container, Node* from, Node* to)
|
|
: m_controller(controller)
|
|
, m_from_uid(from->engine_cmp->uid)
|
|
, m_to_uid(to->engine_cmp->uid)
|
|
, m_edge_uid(-1)
|
|
, m_container_uid(container->engine_cmp->uid)
|
|
{
|
|
}
|
|
|
|
bool execute() override
|
|
{
|
|
auto* container = (Container*)m_controller.getByUID(m_container_uid);
|
|
if (m_edge_uid < 0) m_edge_uid = m_controller.createUID();
|
|
container->createEdge(m_from_uid, m_to_uid, m_edge_uid);
|
|
return true;
|
|
}
|
|
|
|
|
|
void undo() override
|
|
{
|
|
auto* container = (Container*)m_controller.getByUID(m_container_uid);
|
|
container->destroyChild(m_edge_uid);
|
|
}
|
|
|
|
|
|
const char* getType() override { return "create_anim_edge"; }
|
|
|
|
|
|
bool merge(IEditorCommand& command) override { return false; }
|
|
|
|
|
|
void serialize(JsonSerializer& serializer) override
|
|
{
|
|
// TODO
|
|
ASSERT(false);
|
|
}
|
|
|
|
|
|
void deserialize(JsonSerializer& serializer) override
|
|
{
|
|
// TODO
|
|
ASSERT(false);
|
|
}
|
|
|
|
ControllerResource& m_controller;
|
|
int m_from_uid;
|
|
int m_to_uid;
|
|
int m_container_uid;
|
|
int m_edge_uid;
|
|
};
|
|
|
|
|
|
class AnimationEditor : public IAnimationEditor
|
|
{
|
|
public:
|
|
AnimationEditor(StudioApp& app);
|
|
~AnimationEditor();
|
|
|
|
void update(float time_delta) override;
|
|
const char* getName() const override { return "animation_editor"; }
|
|
void setContainer(Container* container) override { m_container = container; }
|
|
bool isEditorOpen() override { return m_editor_open; }
|
|
void toggleEditorOpen() override { m_editor_open = !m_editor_open; }
|
|
bool isInputsOpen() override { return m_inputs_open; }
|
|
void toggleInputsOpen() override { m_inputs_open = !m_inputs_open; }
|
|
void onWindowGUI() override;
|
|
StudioApp& getApp() override { return m_app; }
|
|
int getEventTypesCount() const override;
|
|
EventType& createEventType(const char* type) override;
|
|
EventType& getEventTypeByIdx(int idx) override { return m_event_types[idx]; }
|
|
EventType& getEventType(u32 type) override;
|
|
void executeCommand(IEditorCommand& command);
|
|
void createEdge(ControllerResource& ctrl, Container* container, Node* from, Node* to) override;
|
|
void moveNode(ControllerResource& ctrl, Node* node, const ImVec2& pos) override;
|
|
void createNode(ControllerResource& ctrl,
|
|
Container* container,
|
|
Anim::Node::Type type,
|
|
const ImVec2& pos) override;
|
|
void destroyNode(ControllerResource& ctrl, Node* node) override;
|
|
void destroyEdge(ControllerResource& ctrl, Edge* edge) override;
|
|
bool hasFocus() override { return m_is_focused; }
|
|
|
|
private:
|
|
void checkShortcuts();
|
|
void beginCommandGroup(u32 type);
|
|
void endCommandGroup();
|
|
void newController();
|
|
void save();
|
|
void saveAs();
|
|
void drawGraph();
|
|
void load();
|
|
void loadFromEntity();
|
|
void loadFromFile();
|
|
void editorGUI();
|
|
void inputsGUI();
|
|
void constantsGUI();
|
|
void animationSlotsGUI();
|
|
void menuGUI();
|
|
void onSetInputGUI(u8* data, Component& component);
|
|
void undo();
|
|
void redo();
|
|
void clearUndoStack();
|
|
|
|
private:
|
|
StudioApp& m_app;
|
|
bool m_editor_open;
|
|
bool m_inputs_open;
|
|
ImVec2 m_offset;
|
|
ControllerResource* m_resource;
|
|
Container* m_container;
|
|
StaticString<MAX_PATH_LENGTH> m_path;
|
|
Array<EventType> m_event_types;
|
|
Array<IEditorCommand*> m_undo_stack;
|
|
int m_undo_index = -1;
|
|
bool m_is_playing = false;
|
|
bool m_is_focused = false;
|
|
u32 m_current_group_type;
|
|
};
|
|
|
|
|
|
AnimationEditor::AnimationEditor(StudioApp& app)
|
|
: m_app(app)
|
|
, m_editor_open(false)
|
|
, m_inputs_open(false)
|
|
, m_offset(0, 0)
|
|
, m_event_types(app.getWorldEditor().getAllocator())
|
|
, m_undo_stack(app.getWorldEditor().getAllocator())
|
|
{
|
|
m_path = "";
|
|
IAllocator& allocator = app.getWorldEditor().getAllocator();
|
|
|
|
auto* action = LUMIX_NEW(allocator, Action)("Animation Editor", "animation_editor");
|
|
action->func.bind<AnimationEditor, &AnimationEditor::toggleEditorOpen>(this);
|
|
action->is_selected.bind<AnimationEditor, &AnimationEditor::isEditorOpen>(this);
|
|
app.addWindowAction(action);
|
|
|
|
action = LUMIX_NEW(allocator, Action)("Animation Inputs", "animation_inputs");
|
|
action->func.bind<AnimationEditor, &AnimationEditor::toggleInputsOpen>(this);
|
|
action->is_selected.bind<AnimationEditor, &AnimationEditor::isInputsOpen>(this);
|
|
app.addWindowAction(action);
|
|
|
|
Engine& engine = m_app.getWorldEditor().getEngine();
|
|
auto* manager = engine.getResourceManager().get(CONTROLLER_RESOURCE_TYPE);
|
|
m_resource = LUMIX_NEW(allocator, ControllerResource)(*this, *manager, allocator);
|
|
m_container = (Container*)m_resource->getRoot();
|
|
|
|
EventType& event_type = createEventType("set_input");
|
|
event_type.size = sizeof(Anim::SetInputEvent);
|
|
event_type.label = "Set Input";
|
|
event_type.editor.bind<AnimationEditor, &AnimationEditor::onSetInputGUI>(this);
|
|
|
|
Action* undo_action = LUMIX_NEW(allocator, Action)("Undo", "animeditor_undo", SDL_SCANCODE_LCTRL, 'Z', -1);
|
|
undo_action->is_global = true;
|
|
undo_action->plugin = this;
|
|
undo_action->func.bind<AnimationEditor, &AnimationEditor::undo>(this);
|
|
app.addAction(undo_action);
|
|
|
|
Action* redo_action = LUMIX_NEW(allocator, Action)("Redo", "animeditor_redo", SDL_SCANCODE_LCTRL, KMOD_SHIFT, 'Z');
|
|
redo_action->is_global = true;
|
|
redo_action->plugin = this;
|
|
redo_action->func.bind<AnimationEditor, &AnimationEditor::redo>(this);
|
|
app.addAction(redo_action);
|
|
}
|
|
|
|
|
|
AnimationEditor::~AnimationEditor()
|
|
{
|
|
IAllocator& allocator = m_app.getWorldEditor().getAllocator();
|
|
LUMIX_DELETE(allocator, m_resource);
|
|
for (auto& cmd : m_undo_stack) {
|
|
LUMIX_DELETE(allocator, cmd);
|
|
}
|
|
}
|
|
|
|
|
|
void AnimationEditor::moveNode(ControllerResource& ctrl, Node* node, const ImVec2& pos)
|
|
{
|
|
IAllocator& allocator = m_app.getWorldEditor().getAllocator();
|
|
auto* cmd = LUMIX_NEW(allocator, MoveAnimNodeCommand)(ctrl, node, pos);
|
|
executeCommand(*cmd);
|
|
}
|
|
|
|
|
|
void AnimationEditor::createNode(ControllerResource& ctrl,
|
|
Container* container,
|
|
Anim::Node::Type type,
|
|
const ImVec2& pos)
|
|
{
|
|
IAllocator& allocator = m_app.getWorldEditor().getAllocator();
|
|
auto* cmd = LUMIX_NEW(allocator, CreateAnimNodeCommand)(ctrl, container, type, pos);
|
|
executeCommand(*cmd);
|
|
}
|
|
|
|
|
|
void AnimationEditor::destroyEdge(ControllerResource& ctrl, Edge* edge)
|
|
{
|
|
IAllocator& allocator = m_app.getWorldEditor().getAllocator();
|
|
auto* cmd = LUMIX_NEW(allocator, DestroyAnimEdgeCommand)(ctrl, edge->engine_cmp->uid);
|
|
executeCommand(*cmd);
|
|
}
|
|
|
|
|
|
void AnimationEditor::destroyNode(ControllerResource& ctrl, Node* node)
|
|
{
|
|
beginCommandGroup(crc32("destroy_node_group"));
|
|
|
|
while (!node->getEdges().empty())
|
|
{
|
|
destroyEdge(ctrl, node->getEdges().back());
|
|
}
|
|
|
|
while (!node->getInEdges().empty())
|
|
{
|
|
destroyEdge(ctrl, node->getInEdges().back());
|
|
}
|
|
|
|
IAllocator& allocator = m_app.getWorldEditor().getAllocator();
|
|
auto* cmd = LUMIX_NEW(allocator, DestroyNodeCommand)(ctrl, node->engine_cmp->uid);
|
|
executeCommand(*cmd);
|
|
endCommandGroup();
|
|
}
|
|
|
|
|
|
void AnimationEditor::createEdge(ControllerResource& ctrl, Container* container, Node* from, Node* to)
|
|
{
|
|
IAllocator& allocator = m_app.getWorldEditor().getAllocator();
|
|
auto* cmd = LUMIX_NEW(allocator, CreateAnimEdgeCommand)(ctrl, container, from, to);
|
|
executeCommand(*cmd);
|
|
}
|
|
|
|
|
|
void AnimationEditor::beginCommandGroup(u32 type)
|
|
{
|
|
IAllocator& allocator = m_app.getWorldEditor().getAllocator();
|
|
|
|
if (m_undo_index < m_undo_stack.size() - 1)
|
|
{
|
|
for (int i = m_undo_stack.size() - 1; i > m_undo_index; --i)
|
|
{
|
|
LUMIX_DELETE(allocator, m_undo_stack[i]);
|
|
}
|
|
m_undo_stack.resize(m_undo_index + 1);
|
|
}
|
|
|
|
if (m_undo_index >= 0)
|
|
{
|
|
static const u32 end_group_hash = crc32("end_group");
|
|
if (crc32(m_undo_stack[m_undo_index]->getType()) == end_group_hash)
|
|
{
|
|
if (static_cast<EndGroupCommand*>(m_undo_stack[m_undo_index])->group_type == type)
|
|
{
|
|
LUMIX_DELETE(allocator, m_undo_stack[m_undo_index]);
|
|
--m_undo_index;
|
|
m_undo_stack.pop();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
m_current_group_type = type;
|
|
auto* cmd = LUMIX_NEW(allocator, BeginGroupCommand);
|
|
m_undo_stack.push(cmd);
|
|
++m_undo_index;
|
|
}
|
|
|
|
|
|
void AnimationEditor::endCommandGroup()
|
|
{
|
|
IAllocator& allocator = m_app.getWorldEditor().getAllocator();
|
|
|
|
if (m_undo_index < m_undo_stack.size() - 1)
|
|
{
|
|
for (int i = m_undo_stack.size() - 1; i > m_undo_index; --i)
|
|
{
|
|
LUMIX_DELETE(allocator, m_undo_stack[i]);
|
|
}
|
|
m_undo_stack.resize(m_undo_index + 1);
|
|
}
|
|
|
|
auto* cmd = LUMIX_NEW(allocator, EndGroupCommand);
|
|
cmd->group_type = m_current_group_type;
|
|
m_undo_stack.push(cmd);
|
|
++m_undo_index;
|
|
}
|
|
|
|
|
|
void AnimationEditor::executeCommand(IEditorCommand& command)
|
|
{
|
|
// TODO clean memory in destructor
|
|
IAllocator& allocator = m_app.getWorldEditor().getAllocator();
|
|
while (m_undo_stack.size() > m_undo_index + 1)
|
|
{
|
|
LUMIX_DELETE(allocator, m_undo_stack.back());
|
|
m_undo_stack.pop();
|
|
}
|
|
|
|
if (!m_undo_stack.empty())
|
|
{
|
|
auto* back = m_undo_stack.back();
|
|
if (back->getType() == command.getType())
|
|
{
|
|
if (command.merge(*back))
|
|
{
|
|
back->execute();
|
|
LUMIX_DELETE(allocator, &command);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
m_undo_stack.push(&command);
|
|
++m_undo_index;
|
|
command.execute();
|
|
}
|
|
|
|
|
|
AnimationEditor::EventType& AnimationEditor::getEventType(u32 type)
|
|
{
|
|
for (auto& i : m_event_types)
|
|
{
|
|
if (i.type == type) return i;
|
|
}
|
|
return m_event_types[0];
|
|
}
|
|
|
|
|
|
void AnimationEditor::onSetInputGUI(u8* data, Component& component)
|
|
{
|
|
auto event = (Anim::SetInputEvent*)data;
|
|
auto& input_decl = component.getController().getEngineResource()->m_input_decl;
|
|
auto getter = [](void* data, int idx, const char** out) -> bool {
|
|
const auto& input_decl = *(Anim::InputDecl*)data;
|
|
int i = input_decl.inputFromLinearIdx(idx);
|
|
*out = input_decl.inputs[i].name;
|
|
return true;
|
|
};
|
|
int idx = input_decl.inputToLinearIdx(event->input_idx);
|
|
ImGui::Combo("Input", &idx, getter, &input_decl, input_decl.inputs_count);
|
|
event->input_idx = input_decl.inputFromLinearIdx(idx);
|
|
|
|
if (event->input_idx >= 0 && event->input_idx < lengthOf(input_decl.inputs))
|
|
{
|
|
switch (input_decl.inputs[event->input_idx].type)
|
|
{
|
|
case Anim::InputDecl::BOOL: ImGui::Checkbox("Value", &event->b_value); break;
|
|
case Anim::InputDecl::INT: ImGui::InputInt("Value", &event->i_value); break;
|
|
case Anim::InputDecl::FLOAT: ImGui::InputFloat("Value", &event->f_value); break;
|
|
default: ASSERT(false); break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void AnimationEditor::onWindowGUI()
|
|
{
|
|
editorGUI();
|
|
inputsGUI();
|
|
}
|
|
|
|
|
|
void AnimationEditor::saveAs()
|
|
{
|
|
if (!PlatformInterface::getSaveFilename(m_path.data, lengthOf(m_path.data), "Animation controllers\0*.act\0", "")) return;
|
|
save();
|
|
}
|
|
|
|
|
|
void AnimationEditor::save()
|
|
{
|
|
if (m_path[0] == 0 &&
|
|
!PlatformInterface::getSaveFilename(m_path.data, lengthOf(m_path.data), "Animation controllers\0*.act\0", ""))
|
|
return;
|
|
IAllocator& allocator = m_app.getWorldEditor().getAllocator();
|
|
OutputBlob blob(allocator);
|
|
m_resource->serialize(blob);
|
|
FS::OsFile file;
|
|
file.open(m_path, FS::Mode::CREATE_AND_WRITE, allocator);
|
|
file.write(blob.getData(), blob.getPos());
|
|
file.close();
|
|
}
|
|
|
|
|
|
void AnimationEditor::drawGraph()
|
|
{
|
|
ImGui::BeginChild("canvas", ImVec2(0, 0), true);
|
|
if (ImGui::IsWindowHovered() && !ImGui::IsAnyItemActive() && ImGui::IsMouseDragging(2, 0.0f))
|
|
{
|
|
m_offset = m_offset + ImGui::GetIO().MouseDelta;
|
|
}
|
|
|
|
auto* scene = (AnimationScene*)m_app.getWorldEditor().getUniverse()->getScene(ANIMABLE_HASH);
|
|
auto& entities = m_app.getWorldEditor().getSelectedEntities();
|
|
Anim::ComponentInstance* runtime = nullptr;
|
|
if (!entities.empty())
|
|
{
|
|
ComponentHandle ctrl = scene->getComponent(entities[0], CONTROLLER_TYPE);
|
|
if (ctrl.isValid())
|
|
{
|
|
runtime = scene->getControllerRoot(ctrl);
|
|
}
|
|
}
|
|
|
|
ImDrawList* draw = ImGui::GetWindowDrawList();
|
|
auto canvas_screen_pos = ImGui::GetCursorScreenPos() + m_offset;
|
|
m_container->drawInside(draw, canvas_screen_pos);
|
|
if(runtime) m_resource->getRoot()->debugInside(draw, canvas_screen_pos, runtime, m_container);
|
|
|
|
ImGui::EndChild();
|
|
}
|
|
|
|
|
|
void AnimationEditor::loadFromEntity()
|
|
{
|
|
auto& entities = m_app.getWorldEditor().getSelectedEntities();
|
|
if (entities.empty()) return;
|
|
auto* scene = (AnimationScene*)m_app.getWorldEditor().getUniverse()->getScene(ANIMABLE_HASH);
|
|
ComponentHandle ctrl = scene->getComponent(entities[0], CONTROLLER_TYPE);
|
|
if (!ctrl.isValid()) return;
|
|
m_path = scene->getControllerSource(ctrl).c_str();
|
|
clearUndoStack();
|
|
load();
|
|
}
|
|
|
|
|
|
void AnimationEditor::load()
|
|
{
|
|
IAllocator& allocator = m_app.getWorldEditor().getAllocator();
|
|
FS::OsFile file;
|
|
file.open(m_path, FS::Mode::OPEN_AND_READ, allocator);
|
|
Array<u8> data(allocator);
|
|
data.resize((int)file.size());
|
|
file.read(&data[0], data.size());
|
|
InputBlob blob(&data[0], data.size());
|
|
if (m_resource->deserialize(blob, m_app.getWorldEditor().getEngine(), allocator))
|
|
{
|
|
m_container = (Container*)m_resource->getRoot();
|
|
}
|
|
else
|
|
{
|
|
LUMIX_DELETE(allocator, m_resource);
|
|
Engine& engine = m_app.getWorldEditor().getEngine();
|
|
auto* manager = engine.getResourceManager().get(CONTROLLER_RESOURCE_TYPE);
|
|
m_resource = LUMIX_NEW(allocator, ControllerResource)(*this, *manager, allocator);
|
|
m_container = (Container*)m_resource->getRoot();
|
|
}
|
|
file.close();
|
|
}
|
|
|
|
|
|
void AnimationEditor::loadFromFile()
|
|
{
|
|
if (!PlatformInterface::getOpenFilename(m_path.data, lengthOf(m_path.data), "Animation controllers\0*.act\0", "")) return;
|
|
clearUndoStack();
|
|
load();
|
|
}
|
|
|
|
|
|
void AnimationEditor::newController()
|
|
{
|
|
IAllocator& allocator = m_app.getWorldEditor().getAllocator();
|
|
LUMIX_DELETE(allocator, m_resource);
|
|
Engine& engine = m_app.getWorldEditor().getEngine();
|
|
auto* manager = engine.getResourceManager().get(CONTROLLER_RESOURCE_TYPE);
|
|
m_resource = LUMIX_NEW(allocator, ControllerResource)(*this, *manager, allocator);
|
|
m_container = (Container*)m_resource->getRoot();
|
|
m_path = "";
|
|
clearUndoStack();
|
|
}
|
|
|
|
|
|
int AnimationEditor::getEventTypesCount() const
|
|
{
|
|
return m_event_types.size();
|
|
}
|
|
|
|
|
|
AnimationEditor::EventType& AnimationEditor::createEventType(const char* type)
|
|
{
|
|
EventType& event_type = m_event_types.emplace();
|
|
event_type.type = crc32(type);
|
|
return event_type;
|
|
}
|
|
|
|
|
|
void AnimationEditor::menuGUI()
|
|
{
|
|
if (ImGui::BeginMenuBar())
|
|
{
|
|
if (ImGui::BeginMenu("File"))
|
|
{
|
|
if (ImGui::MenuItem("New")) newController();
|
|
if (ImGui::MenuItem("Save")) save();
|
|
if (ImGui::MenuItem("Save As")) saveAs();
|
|
if (ImGui::MenuItem("Open")) loadFromFile();
|
|
if (ImGui::MenuItem("Open from selected entity")) loadFromEntity();
|
|
ImGui::EndMenu();
|
|
}
|
|
if (ImGui::BeginMenu("Edit"))
|
|
{
|
|
if (ImGui::MenuItem("Undo")) undo();
|
|
if (ImGui::MenuItem("Redo")) redo();
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
ImGui::SameLine();
|
|
ImGui::Checkbox("Play", &m_is_playing);
|
|
ImGui::SameLine();
|
|
if (ImGui::MenuItem("Go up", nullptr, false, m_container->getParent() != nullptr))
|
|
{
|
|
m_container = m_container->getParent();
|
|
}
|
|
|
|
ImGui::EndMenuBar();
|
|
}
|
|
}
|
|
|
|
|
|
void AnimationEditor::redo()
|
|
{
|
|
if (m_undo_index == m_undo_stack.size() - 1) return;
|
|
|
|
static const u32 end_group_hash = crc32("end_group");
|
|
static const u32 begin_group_hash = crc32("begin_group");
|
|
|
|
++m_undo_index;
|
|
if (crc32(m_undo_stack[m_undo_index]->getType()) == begin_group_hash)
|
|
{
|
|
++m_undo_index;
|
|
while (crc32(m_undo_stack[m_undo_index]->getType()) != end_group_hash)
|
|
{
|
|
m_undo_stack[m_undo_index]->execute();
|
|
++m_undo_index;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_undo_stack[m_undo_index]->execute();
|
|
}
|
|
}
|
|
|
|
|
|
void AnimationEditor::undo()
|
|
{
|
|
if (m_undo_index >= m_undo_stack.size() || m_undo_index < 0) return;
|
|
|
|
static const u32 end_group_hash = crc32("end_group");
|
|
static const u32 begin_group_hash = crc32("begin_group");
|
|
|
|
if (crc32(m_undo_stack[m_undo_index]->getType()) == end_group_hash)
|
|
{
|
|
--m_undo_index;
|
|
while (crc32(m_undo_stack[m_undo_index]->getType()) != begin_group_hash)
|
|
{
|
|
m_undo_stack[m_undo_index]->undo();
|
|
--m_undo_index;
|
|
}
|
|
--m_undo_index;
|
|
}
|
|
else
|
|
{
|
|
m_undo_stack[m_undo_index]->undo();
|
|
--m_undo_index;
|
|
}
|
|
}
|
|
|
|
|
|
void AnimationEditor::clearUndoStack()
|
|
{
|
|
IAllocator& allocator = m_app.getWorldEditor().getAllocator();
|
|
for (auto& cmd : m_undo_stack) {
|
|
LUMIX_DELETE(allocator, cmd);
|
|
}
|
|
m_undo_stack.clear();
|
|
m_undo_index = -1;
|
|
}
|
|
|
|
|
|
void AnimationEditor::checkShortcuts()
|
|
{
|
|
|
|
}
|
|
|
|
|
|
void AnimationEditor::update(float time_delta)
|
|
{
|
|
checkShortcuts();
|
|
if (!m_is_playing) return;
|
|
auto& entities = m_app.getWorldEditor().getSelectedEntities();
|
|
if (entities.empty()) return;
|
|
auto* scene = (AnimationScene*)m_app.getWorldEditor().getUniverse()->getScene(ANIMABLE_HASH);
|
|
ComponentHandle ctrl = scene->getComponent(entities[0], CONTROLLER_TYPE);
|
|
if (!ctrl.isValid()) return;
|
|
scene->updateController(ctrl, time_delta);
|
|
}
|
|
|
|
|
|
void AnimationEditor::editorGUI()
|
|
{
|
|
if (ImGui::BeginDock("Animation Editor", &m_editor_open, ImGuiWindowFlags_MenuBar))
|
|
{
|
|
m_is_focused = ImGui::IsFocusedHierarchy();
|
|
menuGUI();
|
|
ImGui::Columns(2);
|
|
drawGraph();
|
|
ImGui::NextColumn();
|
|
ImGui::Text("Properties");
|
|
if(m_container->getSelectedComponent()) m_container->getSelectedComponent()->onGUI();
|
|
ImGui::Columns();
|
|
}
|
|
else
|
|
{
|
|
m_is_focused = false;
|
|
}
|
|
ImGui::EndDock();
|
|
}
|
|
|
|
|
|
void AnimationEditor::inputsGUI()
|
|
{
|
|
if (ImGui::BeginDock("Animation inputs", &m_inputs_open))
|
|
{
|
|
if (ImGui::CollapsingHeader("Inputs"))
|
|
{
|
|
const auto& selected_entities = m_app.getWorldEditor().getSelectedEntities();
|
|
auto* scene = (AnimationScene*)m_app.getWorldEditor().getUniverse()->getScene(ANIMABLE_HASH);
|
|
ComponentHandle cmp = selected_entities.empty() ? INVALID_COMPONENT : scene->getComponent(selected_entities[0], CONTROLLER_TYPE);
|
|
u8* input_data = cmp.isValid() ? scene->getControllerInput(cmp) : nullptr;
|
|
Anim::InputDecl& input_decl = m_resource->getEngineResource()->m_input_decl;
|
|
|
|
for (auto& input : input_decl.inputs)
|
|
{
|
|
if (input.type == Anim::InputDecl::EMPTY) continue;
|
|
ImGui::PushID(&input);
|
|
ImGui::PushItemWidth(100);
|
|
ImGui::InputText("##name", input.name, lengthOf(input.name));
|
|
ImGui::SameLine();
|
|
if (ImGui::Combo("##type", (int*)&input.type, "float\0int\0bool\0"))
|
|
{
|
|
input_decl.recalculateOffsets();
|
|
}
|
|
if (input_data)
|
|
{
|
|
ImGui::SameLine();
|
|
switch (input.type)
|
|
{
|
|
case Anim::InputDecl::FLOAT: ImGui::DragFloat("##value", (float*)(input_data + input.offset)); break;
|
|
case Anim::InputDecl::BOOL: ImGui::Checkbox("##value", (bool*)(input_data + input.offset)); break;
|
|
case Anim::InputDecl::INT: ImGui::InputInt("##value", (int*)(input_data + input.offset)); break;
|
|
default: ASSERT(false); break;
|
|
}
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("x"))
|
|
{
|
|
input.type = Anim::InputDecl::EMPTY;
|
|
--input_decl.inputs_count;
|
|
}
|
|
|
|
ImGui::PopItemWidth();
|
|
ImGui::PopID();
|
|
}
|
|
|
|
if (input_decl.inputs_count < lengthOf(input_decl.inputs) && ImGui::Button("Add"))
|
|
{
|
|
for (auto& input : input_decl.inputs)
|
|
{
|
|
if (input.type == Anim::InputDecl::EMPTY)
|
|
{
|
|
input.name[0] = 0;
|
|
input.type = Anim::InputDecl::BOOL;
|
|
input.offset = input_decl.getSize();
|
|
++input_decl.inputs_count;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
constantsGUI();
|
|
animationSlotsGUI();
|
|
}
|
|
ImGui::EndDock();
|
|
}
|
|
|
|
|
|
void AnimationEditor::constantsGUI()
|
|
{
|
|
if (!ImGui::CollapsingHeader("Constants")) return;
|
|
|
|
Anim::InputDecl& input_decl = m_resource->getEngineResource()->m_input_decl;
|
|
ImGui::PushID("consts");
|
|
for (auto& constant : input_decl.constants)
|
|
{
|
|
if (constant.type == Anim::InputDecl::EMPTY) continue;
|
|
ImGui::PushID(&constant);
|
|
ImGui::PushItemWidth(100);
|
|
ImGui::InputText("", constant.name, lengthOf(constant.name));
|
|
ImGui::SameLine();
|
|
if (ImGui::Combo("##cmb", (int*)&constant.type, "float\0int\0bool\0"))
|
|
{
|
|
input_decl.recalculateOffsets();
|
|
}
|
|
ImGui::SameLine();
|
|
switch (constant.type)
|
|
{
|
|
case Anim::InputDecl::FLOAT: ImGui::DragFloat("##val", &constant.f_value); break;
|
|
case Anim::InputDecl::BOOL: ImGui::Checkbox("##val", &constant.b_value); break;
|
|
case Anim::InputDecl::INT: ImGui::InputInt("##val", &constant.i_value); break;
|
|
default: ASSERT(false); break;
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("x"))
|
|
{
|
|
constant.type = Anim::InputDecl::EMPTY;
|
|
--input_decl.constants_count;
|
|
}
|
|
ImGui::PopItemWidth();
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::PopID();
|
|
|
|
if (input_decl.constants_count < lengthOf(input_decl.constants) && ImGui::Button("Add##add_const"))
|
|
{
|
|
for (auto& constant : input_decl.constants)
|
|
{
|
|
if (constant.type == Anim::InputDecl::EMPTY)
|
|
{
|
|
constant.name[0] = 0;
|
|
constant.type = Anim::InputDecl::BOOL;
|
|
constant.b_value = true;
|
|
++input_decl.constants_count;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void AnimationEditor::animationSlotsGUI()
|
|
{
|
|
if (!ImGui::CollapsingHeader("Animation slots")) return;
|
|
ImGui::PushID("anim_slots");
|
|
auto& engine_anim_set = m_resource->getEngineResource()->m_animation_set;
|
|
auto& slots = m_resource->getAnimationSlots();
|
|
auto& sets = m_resource->getEngineResource()->m_sets_names;
|
|
ImGui::PushItemWidth(-1);
|
|
ImGui::Columns(sets.size() + 1);
|
|
ImGui::NextColumn();
|
|
ImGui::PushID("header");
|
|
for (int j = 0; j < sets.size(); ++j)
|
|
{
|
|
ImGui::PushID(j);
|
|
ImGui::PushItemWidth(-1);
|
|
ImGui::InputText("", sets[j].data, lengthOf(sets[j].data));
|
|
ImGui::PopItemWidth();
|
|
ImGui::PopID();
|
|
ImGui::NextColumn();
|
|
}
|
|
ImGui::PopID();
|
|
ImGui::Separator();
|
|
for (int i = 0; i < slots.size(); ++i)
|
|
{
|
|
const string& slot = slots[i];
|
|
ImGui::PushID(i);
|
|
char slot_cstr[64];
|
|
copyString(slot_cstr, slot.c_str());
|
|
|
|
ImGui::PushItemWidth(-20);
|
|
if (ImGui::InputText("##name", slot_cstr, lengthOf(slot_cstr), ImGuiInputTextFlags_EnterReturnsTrue))
|
|
{
|
|
bool exists = slots.find([&slot_cstr](const string& val) { return val == slot_cstr; }) >= 0;
|
|
|
|
if (exists)
|
|
{
|
|
g_log_error.log("Animation") << "Slot " << slot_cstr << " already exists.";
|
|
}
|
|
else
|
|
{
|
|
u32 old_hash = crc32(slot.c_str());
|
|
u32 new_hash = crc32(slot_cstr);
|
|
|
|
for (auto& entry : engine_anim_set)
|
|
{
|
|
if (entry.hash == old_hash) entry.hash = new_hash;
|
|
}
|
|
slots[i] = slot_cstr;
|
|
}
|
|
}
|
|
ImGui::PopItemWidth();
|
|
ImGui::SameLine();
|
|
u32 slot_hash = crc32(slot.c_str());
|
|
if (ImGui::Button("x"))
|
|
{
|
|
slots.erase(i);
|
|
engine_anim_set.eraseItems([slot_hash](Anim::ControllerResource::AnimSetEntry& val) { return val.hash == slot_hash; });
|
|
--i;
|
|
}
|
|
ImGui::NextColumn();
|
|
for (int j = 0; j < sets.size(); ++j)
|
|
{
|
|
Anim::ControllerResource::AnimSetEntry* entry = nullptr;
|
|
for (auto& e : engine_anim_set)
|
|
{
|
|
if (e.set == j && e.hash == slot_hash)
|
|
{
|
|
entry = &e;
|
|
break;
|
|
}
|
|
}
|
|
|
|
ImGui::PushItemWidth(ImGui::GetColumnWidth());
|
|
char tmp[MAX_PATH_LENGTH];
|
|
copyString(tmp, entry && entry->animation ? entry->animation->getPath().c_str() : "");
|
|
ImGui::PushID(j);
|
|
if (m_app.getAssetBrowser().resourceInput("", "##res", tmp, lengthOf(tmp), ANIMATION_TYPE))
|
|
{
|
|
if (entry && entry->animation) entry->animation->getResourceManager().unload(*entry->animation);
|
|
auto* manager = m_app.getWorldEditor().getEngine().getResourceManager().get(ANIMATION_TYPE);
|
|
if (entry)
|
|
{
|
|
entry->animation = (Animation*)manager->load(Path(tmp));
|
|
}
|
|
else
|
|
{
|
|
engine_anim_set.push({j, slot_hash, (Animation*)manager->load(Path(tmp))});
|
|
}
|
|
}
|
|
ImGui::PopID();
|
|
ImGui::PopItemWidth();
|
|
|
|
|
|
ImGui::NextColumn();
|
|
}
|
|
ImGui::PopID();
|
|
}
|
|
ImGui::Columns();
|
|
|
|
if (ImGui::Button("Add slot (row)"))
|
|
{
|
|
bool exists = slots.find([](const string& val) { return val == ""; }) >= 0;
|
|
|
|
if (exists)
|
|
{
|
|
g_log_error.log("Animation") << "Slot with empty name already exists. Please rename it and then you can create a new slot.";
|
|
}
|
|
else
|
|
{
|
|
IAllocator& allocator = m_app.getWorldEditor().getAllocator();
|
|
slots.emplace("", allocator);
|
|
}
|
|
}
|
|
if (ImGui::Button("Add set (column)"))
|
|
{
|
|
IAllocator& allocator = m_app.getWorldEditor().getAllocator();
|
|
m_resource->getEngineResource()->m_sets_names.emplace("new set");
|
|
}
|
|
ImGui::PopItemWidth();
|
|
ImGui::PopID();
|
|
|
|
|
|
}
|
|
|
|
|
|
IAnimationEditor* IAnimationEditor::create(IAllocator& allocator, StudioApp& app)
|
|
{
|
|
return LUMIX_NEW(allocator, AnimationEditor)(app);
|
|
}
|
|
|
|
|
|
} // namespace AnimEditor
|
|
|
|
|
|
} // namespace Lumix
|