engine: add the keyframe family to the node facade
- keyframed state, count, read (time in sequence timebase + mapped values), add/remove, easing read/write (linear/bezier/hold with control points), and clear - all undoable with full undo/redo assertions - easing set commands are minimal capi-local UndoCommands matching the app-layer semantics (engine has none of its own); same-time duplicates are rejected with E_STATE instead of hitting the engine's debug assert
This commit is contained in:
@@ -32,9 +32,11 @@
|
||||
|
||||
#include "coreengine.h"
|
||||
#include "node/factory.h"
|
||||
#include "node/keyframe.h"
|
||||
#include "node/node.h"
|
||||
#include "node/nodeundo.h"
|
||||
#include "node/project.h"
|
||||
#include "node/project/sequence/sequence.h"
|
||||
#include "node/value.h"
|
||||
#include "undo/undocommand.h"
|
||||
#include "undo/undostack.h"
|
||||
@@ -242,6 +244,197 @@ olive::NodeValue::Type checked_input(const olive::Node *self,
|
||||
return self->get_input_data_type(id);
|
||||
}
|
||||
|
||||
/* ---- Keyframe helpers ------------------------------------------------------ */
|
||||
|
||||
// facade easing type <-> engine NodeKeyframe::Type (different orders).
|
||||
olive::NodeKeyframe::Type to_engine_easing(int type)
|
||||
{
|
||||
switch (type) {
|
||||
case 1:
|
||||
return olive::NodeKeyframe::k_bezier;
|
||||
case 2:
|
||||
return olive::NodeKeyframe::k_hold;
|
||||
default:
|
||||
return olive::NodeKeyframe::k_linear;
|
||||
}
|
||||
}
|
||||
|
||||
int from_engine_easing(olive::NodeKeyframe::Type type)
|
||||
{
|
||||
switch (type) {
|
||||
case olive::NodeKeyframe::k_bezier:
|
||||
return 1;
|
||||
case olive::NodeKeyframe::k_hold:
|
||||
return 2;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Frame-timestamp timebase for keyframes: the frame rate of the project's
|
||||
// first sequence, or the engine default (1001/30000 s per frame).
|
||||
olive::Rational project_time_base(const olive::Node *node)
|
||||
{
|
||||
if (const olive::Project *p = olive::Project::get_project_from_object(node)) {
|
||||
for (olive::Node *n : p->nodes()) {
|
||||
if (const olive::Sequence *s = dynamic_cast<olive::Sequence *>(n)) {
|
||||
const olive::Rational fr = s->get_video_params().frame_rate();
|
||||
if (!fr.isNull() && !fr.isNaN()) {
|
||||
return fr.flipped();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return olive::Rational(1001, 30000);
|
||||
}
|
||||
|
||||
int64_t kf_time_to_ts(const olive::Rational &time, const olive::Rational &tb)
|
||||
{
|
||||
return olive::core::Timecode::time_to_timestamp(
|
||||
time, tb, olive::core::Timecode::k_round);
|
||||
}
|
||||
|
||||
// Map a track-0 keyframe component value into the POD. For split-track
|
||||
// types the component lands in f[0] (COLOR/VEC) or num; single-track types
|
||||
// map fully. Returns false for unmappable types.
|
||||
bool kf_value_to_c(olive::NodeValue::Type type, const QVariant &component,
|
||||
oak_node_value *out)
|
||||
{
|
||||
memset(out, 0, sizeof(*out));
|
||||
switch (type) {
|
||||
case olive::NodeValue::k_int:
|
||||
case olive::NodeValue::k_combo:
|
||||
out->type = OAK_NODE_VALUE_INT;
|
||||
out->num = component.toLongLong();
|
||||
return true;
|
||||
case olive::NodeValue::k_float:
|
||||
out->type = OAK_NODE_VALUE_FLOAT;
|
||||
out->f[0] = component.toDouble();
|
||||
return true;
|
||||
case olive::NodeValue::k_boolean:
|
||||
out->type = OAK_NODE_VALUE_BOOL;
|
||||
out->num = component.toBool() ? 1 : 0;
|
||||
return true;
|
||||
case olive::NodeValue::k_rational: {
|
||||
const olive::Rational r = component.value<olive::Rational>();
|
||||
out->type = OAK_NODE_VALUE_RATIONAL;
|
||||
out->num = r.numerator();
|
||||
out->den = r.denominator();
|
||||
return true;
|
||||
}
|
||||
case olive::NodeValue::k_color:
|
||||
out->type = OAK_NODE_VALUE_COLOR;
|
||||
out->f[0] = component.toFloat();
|
||||
return true;
|
||||
case olive::NodeValue::k_vec2:
|
||||
out->type = OAK_NODE_VALUE_VEC2;
|
||||
out->f[0] = component.toFloat();
|
||||
return true;
|
||||
case olive::NodeValue::k_vec3:
|
||||
out->type = OAK_NODE_VALUE_VEC3;
|
||||
out->f[0] = component.toFloat();
|
||||
return true;
|
||||
case olive::NodeValue::k_vec4:
|
||||
out->type = OAK_NODE_VALUE_VEC4;
|
||||
out->f[0] = component.toFloat();
|
||||
return true;
|
||||
case olive::NodeValue::k_file:
|
||||
out->type = OAK_NODE_VALUE_STRING;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Undo commands for easing changes. The engine's undo stack has no
|
||||
// keyframe type/bezier commands -- they live in the application layer
|
||||
// (app/widget/keyframeviewundo.h), so the facade carries minimal
|
||||
// equivalents with the same old/new semantics.
|
||||
class KeyframeSetTypeCommand : public olive::UndoCommand {
|
||||
public:
|
||||
KeyframeSetTypeCommand(olive::NodeKeyframe *key,
|
||||
olive::NodeKeyframe::Type type)
|
||||
: key_(key)
|
||||
, new_type_(type)
|
||||
{
|
||||
}
|
||||
|
||||
virtual olive::Project *get_relevant_project() const override
|
||||
{
|
||||
return key_->parent() ? key_->parent()->project() : nullptr;
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void redo() override
|
||||
{
|
||||
old_type_ = key_->type();
|
||||
key_->set_type(new_type_);
|
||||
}
|
||||
|
||||
virtual void undo() override
|
||||
{
|
||||
key_->set_type(old_type_);
|
||||
}
|
||||
|
||||
private:
|
||||
olive::NodeKeyframe *key_;
|
||||
olive::NodeKeyframe::Type old_type_ = olive::NodeKeyframe::k_linear;
|
||||
olive::NodeKeyframe::Type new_type_;
|
||||
};
|
||||
|
||||
class KeyframeSetBezierPointCommand : public olive::UndoCommand {
|
||||
public:
|
||||
KeyframeSetBezierPointCommand(olive::NodeKeyframe *key,
|
||||
olive::NodeKeyframe::BezierType mode,
|
||||
const QPointF &point)
|
||||
: key_(key)
|
||||
, mode_(mode)
|
||||
, new_point_(point)
|
||||
{
|
||||
}
|
||||
|
||||
virtual olive::Project *get_relevant_project() const override
|
||||
{
|
||||
return key_->parent() ? key_->parent()->project() : nullptr;
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void redo() override
|
||||
{
|
||||
old_point_ = key_->bezier_control(mode_);
|
||||
key_->set_bezier_control(mode_, new_point_);
|
||||
}
|
||||
|
||||
virtual void undo() override
|
||||
{
|
||||
key_->set_bezier_control(mode_, old_point_);
|
||||
}
|
||||
|
||||
private:
|
||||
olive::NodeKeyframe *key_;
|
||||
olive::NodeKeyframe::BezierType mode_;
|
||||
QPointF old_point_;
|
||||
QPointF new_point_;
|
||||
};
|
||||
|
||||
// Validate a keyframing target: node + known input id + mappable type.
|
||||
// Returns the declared type (k_none on failure; error reported).
|
||||
olive::NodeValue::Type checked_keyframe_input(const olive::Node *self,
|
||||
const char *input_id)
|
||||
{
|
||||
const olive::NodeValue::Type type = checked_input(self, input_id);
|
||||
if (!self || !input_id) {
|
||||
set_error(QStringLiteral("invalid arguments"));
|
||||
return olive::NodeValue::k_none;
|
||||
}
|
||||
if (type == olive::NodeValue::k_none) {
|
||||
set_error(QStringLiteral("unknown input id \"%1\"")
|
||||
.arg(QString::fromUtf8(input_id)));
|
||||
return olive::NodeValue::k_none;
|
||||
}
|
||||
return type;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C"
|
||||
@@ -559,4 +752,247 @@ int oakengine_node_disconnect(OakEngineNode *input_node, const char *input_id)
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
/* ---- Parameter animation (keyframes) -------------------------------------- */
|
||||
|
||||
int oakengine_node_input_is_keyframed(const OakEngineNode *self,
|
||||
const char *input_id)
|
||||
{
|
||||
if (!self || !input_id) {
|
||||
return 0;
|
||||
}
|
||||
return impl(self)->is_input_keyframing(QString::fromUtf8(input_id)) ? 1 :
|
||||
0;
|
||||
}
|
||||
|
||||
int oakengine_node_keyframe_count(const OakEngineNode *self,
|
||||
const char *input_id)
|
||||
{
|
||||
if (!self || !input_id) {
|
||||
return 0;
|
||||
}
|
||||
const QVector<olive::NodeKeyframeTrack> &tracks =
|
||||
impl(self)->get_keyframe_tracks(QString::fromUtf8(input_id), -1);
|
||||
return tracks.isEmpty() ? 0 : tracks.first().size();
|
||||
}
|
||||
|
||||
int oakengine_node_keyframe_at(const OakEngineNode *self,
|
||||
const char *input_id, int index,
|
||||
int64_t *time_ts, oak_node_value *value)
|
||||
{
|
||||
set_error(QString());
|
||||
const olive::NodeValue::Type type = checked_keyframe_input(impl(self), input_id);
|
||||
if (type == olive::NodeValue::k_none) {
|
||||
return self && input_id ? OAKENGINE_E_NOT_FOUND : OAKENGINE_E_INVALID;
|
||||
}
|
||||
const QVector<olive::NodeKeyframeTrack> &tracks =
|
||||
impl(self)->get_keyframe_tracks(QString::fromUtf8(input_id), -1);
|
||||
if (tracks.isEmpty() || index < 0 || index >= tracks.first().size()) {
|
||||
set_error(QStringLiteral("no keyframe at index %1").arg(index));
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
const olive::NodeKeyframe *key = tracks.first().at(index);
|
||||
if (time_ts) {
|
||||
*time_ts = kf_time_to_ts(key->time(), project_time_base(impl(self)));
|
||||
}
|
||||
if (value && !kf_value_to_c(type, key->value(), value)) {
|
||||
set_error(QStringLiteral(
|
||||
"input \"%1\" has no POD keyframe representation")
|
||||
.arg(QString::fromUtf8(input_id)));
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_node_keyframe_get_easing(const OakEngineNode *self,
|
||||
const char *input_id, int index,
|
||||
float *x1, float *y1, float *x2,
|
||||
float *y2, int *type)
|
||||
{
|
||||
set_error(QString());
|
||||
if (checked_keyframe_input(impl(self), input_id) == olive::NodeValue::k_none) {
|
||||
return self && input_id ? OAKENGINE_E_NOT_FOUND : OAKENGINE_E_INVALID;
|
||||
}
|
||||
const QVector<olive::NodeKeyframeTrack> &tracks =
|
||||
impl(self)->get_keyframe_tracks(QString::fromUtf8(input_id), -1);
|
||||
if (tracks.isEmpty() || index < 0 || index >= tracks.first().size()) {
|
||||
set_error(QStringLiteral("no keyframe at index %1").arg(index));
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
const olive::NodeKeyframe *key = tracks.first().at(index);
|
||||
if (type) {
|
||||
*type = from_engine_easing(key->type());
|
||||
}
|
||||
const bool bezier = key->type() == olive::NodeKeyframe::k_bezier;
|
||||
const QPointF in =
|
||||
bezier ? key->bezier_control_in() : QPointF(0, 0);
|
||||
const QPointF out =
|
||||
bezier ? key->bezier_control_out() : QPointF(0, 0);
|
||||
if (x1) {
|
||||
*x1 = float(in.x());
|
||||
}
|
||||
if (y1) {
|
||||
*y1 = float(in.y());
|
||||
}
|
||||
if (x2) {
|
||||
*x2 = float(out.x());
|
||||
}
|
||||
if (y2) {
|
||||
*y2 = float(out.y());
|
||||
}
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_node_keyframe_add(OakEngineNode *self, const char *input_id,
|
||||
int64_t time_ts, const oak_node_value *value,
|
||||
int type, float x1, float y1, float x2,
|
||||
float y2)
|
||||
{
|
||||
set_error(QString());
|
||||
olive::Node *node = impl(self);
|
||||
const olive::NodeValue::Type declared =
|
||||
checked_keyframe_input(node, input_id);
|
||||
if (declared == olive::NodeValue::k_none) {
|
||||
return self && input_id ? OAKENGINE_E_NOT_FOUND : OAKENGINE_E_INVALID;
|
||||
}
|
||||
if (!value || type < 0 || type > 2) {
|
||||
set_error(QStringLiteral("invalid arguments or easing type"));
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const QString id = QString::fromUtf8(input_id);
|
||||
const olive::Rational tb = project_time_base(node);
|
||||
const olive::Rational time =
|
||||
olive::core::Timecode::timestamp_to_time(time_ts, tb);
|
||||
|
||||
if (node->get_keyframe_at_time_on_track(id, time, 0)) {
|
||||
set_error(QStringLiteral(
|
||||
"a keyframe already exists at time %1 on \"%2\"")
|
||||
.arg(time_ts)
|
||||
.arg(id));
|
||||
return OAKENGINE_E_STATE;
|
||||
}
|
||||
|
||||
// Map the POD to an engine value, then split it like
|
||||
// Node::set_standard_value() does; track 0 takes the first component.
|
||||
QVariant normal;
|
||||
if (!value_from_c(value, declared, &normal)) {
|
||||
set_error(QStringLiteral("value type %1 does not match the declared "
|
||||
"type of \"%2\"")
|
||||
.arg(value->type)
|
||||
.arg(id));
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const olive::SplitValue split =
|
||||
olive::NodeValue::split_normal_value_into_track_values(declared,
|
||||
normal);
|
||||
if (split.isEmpty()) {
|
||||
set_error(QStringLiteral(
|
||||
"input \"%1\" has no keyframable representation")
|
||||
.arg(id));
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
|
||||
auto *key = new olive::NodeKeyframe(time, split.first(),
|
||||
to_engine_easing(type), 0, -1, id);
|
||||
if (type == 1) {
|
||||
key->set_bezier_control_in(QPointF(x1, y1));
|
||||
key->set_bezier_control_out(QPointF(x2, y2));
|
||||
}
|
||||
|
||||
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
|
||||
if (!node->is_input_keyframing(id)) {
|
||||
command->add_child(new olive::NodeParamSetKeyframingCommand(
|
||||
olive::NodeInput(node, id), true));
|
||||
}
|
||||
command->add_child(new olive::NodeParamInsertKeyframeCommand(node, key));
|
||||
push_or_run(command, QStringLiteral("Add Keyframe"));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_node_keyframe_remove(OakEngineNode *self, const char *input_id,
|
||||
int64_t time_ts)
|
||||
{
|
||||
set_error(QString());
|
||||
olive::Node *node = impl(self);
|
||||
if (checked_keyframe_input(node, input_id) == olive::NodeValue::k_none) {
|
||||
return self && input_id ? OAKENGINE_E_NOT_FOUND : OAKENGINE_E_INVALID;
|
||||
}
|
||||
const QString id = QString::fromUtf8(input_id);
|
||||
const olive::Rational time = olive::core::Timecode::timestamp_to_time(
|
||||
time_ts, project_time_base(node));
|
||||
olive::NodeKeyframe *key =
|
||||
node->get_keyframe_at_time_on_track(id, time, 0);
|
||||
if (!key) {
|
||||
set_error(QStringLiteral("no keyframe at time %1 on \"%2\"")
|
||||
.arg(time_ts)
|
||||
.arg(id));
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
push_or_run(new olive::NodeParamRemoveKeyframeCommand(key),
|
||||
QStringLiteral("Remove Keyframe"));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_node_keyframe_set_easing(OakEngineNode *self,
|
||||
const char *input_id, int64_t time_ts,
|
||||
int type, float x1, float y1,
|
||||
float x2, float y2)
|
||||
{
|
||||
set_error(QString());
|
||||
olive::Node *node = impl(self);
|
||||
if (checked_keyframe_input(node, input_id) == olive::NodeValue::k_none) {
|
||||
return self && input_id ? OAKENGINE_E_NOT_FOUND : OAKENGINE_E_INVALID;
|
||||
}
|
||||
if (type < 0 || type > 2) {
|
||||
set_error(QStringLiteral("unknown easing type %1").arg(type));
|
||||
return OAKENGINE_E_INVALID;
|
||||
}
|
||||
const QString id = QString::fromUtf8(input_id);
|
||||
const olive::Rational time = olive::core::Timecode::timestamp_to_time(
|
||||
time_ts, project_time_base(node));
|
||||
olive::NodeKeyframe *key =
|
||||
node->get_keyframe_at_time_on_track(id, time, 0);
|
||||
if (!key) {
|
||||
set_error(QStringLiteral("no keyframe at time %1 on \"%2\"")
|
||||
.arg(time_ts)
|
||||
.arg(id));
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
|
||||
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
|
||||
command->add_child(
|
||||
new KeyframeSetTypeCommand(key, to_engine_easing(type)));
|
||||
if (type == 1) {
|
||||
command->add_child(new KeyframeSetBezierPointCommand(
|
||||
key, olive::NodeKeyframe::k_in_handle, QPointF(x1, y1)));
|
||||
command->add_child(new KeyframeSetBezierPointCommand(
|
||||
key, olive::NodeKeyframe::k_out_handle, QPointF(x2, y2)));
|
||||
}
|
||||
push_or_run(command, QStringLiteral("Set Keyframe Easing"));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
int oakengine_node_keyframes_clear(OakEngineNode *self, const char *input_id)
|
||||
{
|
||||
set_error(QString());
|
||||
olive::Node *node = impl(self);
|
||||
if (checked_keyframe_input(impl(self), input_id) ==
|
||||
olive::NodeValue::k_none) {
|
||||
return self && input_id ? OAKENGINE_E_NOT_FOUND : OAKENGINE_E_INVALID;
|
||||
}
|
||||
const QString id = QString::fromUtf8(input_id);
|
||||
olive::NodeInputImmediate *imm = node->get_immediate(id, -1);
|
||||
if (!imm) {
|
||||
set_error(QStringLiteral("unknown input id \"%1\"").arg(id));
|
||||
return OAKENGINE_E_NOT_FOUND;
|
||||
}
|
||||
// Nothing to clear: succeed without pushing an empty undo command.
|
||||
if (node->get_keyframe_tracks(id, -1).isEmpty() ||
|
||||
node->get_keyframe_tracks(id, -1).first().isEmpty()) {
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
push_or_run(new olive::NodeImmediateRemoveAllKeyframesCommand(imm),
|
||||
QStringLiteral("Clear Keyframes"));
|
||||
return OAKENGINE_OK;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
Reference in New Issue
Block a user