diff --git a/app/dialog/keyframeproperties/keyframeproperties.cpp b/app/dialog/keyframeproperties/keyframeproperties.cpp index cfbad8689..846f6e467 100644 --- a/app/dialog/keyframeproperties/keyframeproperties.cpp +++ b/app/dialog/keyframeproperties/keyframeproperties.cpp @@ -24,13 +24,55 @@ #include #include -#include "core.h" -#include "node/nodeundo.h" -#include "widget/keyframeview/keyframeviewundo.h" +#include "oakengine/node.h" namespace olive { +namespace +{ + +// Selected keyframes grouped by owning input (node/id/element), with +// times converted to the facade's frame timestamps. The dialog's +// per-property writes go through the facade as ONE undoable command per +// group (usually just one). +struct KeyGroup { + Node *node; + QString input; + int element; + QVector times; + QVector tracks; +}; + +QVector group_keys(const std::vector &keys) +{ + QVector groups; + for (NodeKeyframe *item : keys) { + int g = 0; + for (; g < groups.size(); g++) { + if (groups.at(g).node == item->parent() && + groups.at(g).input == item->input() && + groups.at(g).element == item->element()) { + break; + } + } + if (g == groups.size()) { + groups.append({ item->parent(), item->input(), item->element(), + {}, {} }); + } + OakEngineNode *handle = + reinterpret_cast(item->parent()); + int tbn = 0, tbd = 0; + oakengine_node_frame_time_base(handle, &tbn, &tbd); + groups[g].times.append(Timecode::time_to_timestamp( + item->time(), Rational(tbn, tbd), Timecode::k_round)); + groups[g].tracks.append(item->track()); + } + return groups; +} + +} // namespace + KeyframePropertiesDialog::KeyframePropertiesDialog( const std::vector &keys, const Rational &timebase, QWidget *parent) @@ -203,37 +245,57 @@ KeyframePropertiesDialog::KeyframePropertiesDialog( void KeyframePropertiesDialog::accept() { - MultiUndoCommand *command = new MultiUndoCommand(); + const Rational new_time = time_slider_->get_value(); + const int new_type = type_select_->currentData().toInt(); - Rational new_time = time_slider_->get_value(); - int new_type = type_select_->currentData().toInt(); + const QVector groups = group_keys(keys_); - foreach (NodeKeyframe *key, keys_) { - if (time_slider_->isEnabled() && !time_slider_->is_tristate()) { - command->add_child( - new NodeParamSetKeyframeTimeCommand(key, new_time)); + if (new_type > -1) { + // Engine type (linear 0 / hold 1 / bezier 2) to the facade's + // easing order (linear 0 / bezier 1 / hold 2). + int facade_type = 0; + if (new_type == NodeKeyframe::k_bezier) { + facade_type = 1; + } else if (new_type == NodeKeyframe::k_hold) { + facade_type = 2; } - - if (new_type > -1) { - command->add_child(new KeyframeSetTypeCommand( - key, static_cast(new_type))); - } - - if (bezier_group_->isEnabled()) { - command->add_child(new KeyframeSetBezierControlPoint( - key, NodeKeyframe::k_in_handle, - QPointF(bezier_in_x_slider_->get_value(), - bezier_in_y_slider_->get_value()))); - - command->add_child(new KeyframeSetBezierControlPoint( - key, NodeKeyframe::k_out_handle, - QPointF(bezier_out_x_slider_->get_value(), - bezier_out_y_slider_->get_value()))); + foreach (const KeyGroup &g, groups) { + oakengine_node_keyframes_set_type_many( + reinterpret_cast(g.node), + g.input.toUtf8().constData(), g.element, g.times.constData(), + g.tracks.data(), g.times.size(), facade_type); } } - Core::instance()->undo_stack()->push(command, - tr("Set Keyframe Properties")); + if (bezier_group_->isEnabled()) { + foreach (const KeyGroup &g, groups) { + oakengine_node_keyframes_set_bezier_many( + reinterpret_cast(g.node), + g.input.toUtf8().constData(), g.element, g.times.constData(), + g.tracks.data(), g.times.size(), + bezier_in_x_slider_->get_value(), + bezier_in_y_slider_->get_value(), + bezier_out_x_slider_->get_value(), + bezier_out_y_slider_->get_value()); + } + } + + // Time moves go LAST: the facade addresses keyframes by time, so the + // type/bezier writes above must happen while the keys still sit at + // the times the groups were built from. + if (time_slider_->isEnabled() && !time_slider_->is_tristate()) { + foreach (const KeyGroup &g, groups) { + OakEngineNode *handle = + reinterpret_cast(g.node); + int tbn = 0, tbd = 0; + oakengine_node_frame_time_base(handle, &tbn, &tbd); + oakengine_node_keyframes_set_time_many( + handle, g.input.toUtf8().constData(), g.element, + g.times.constData(), g.tracks.data(), g.times.size(), + Timecode::time_to_timestamp(new_time, Rational(tbn, tbd), + Timecode::k_round)); + } + } QDialog::accept(); } diff --git a/app/widget/curvewidget/curveview.cpp b/app/widget/curvewidget/curveview.cpp index 9ced144fe..855a4c320 100644 --- a/app/widget/curvewidget/curveview.cpp +++ b/app/widget/curvewidget/curveview.cpp @@ -30,14 +30,67 @@ #include "common/decibel.h" #include "common/qtutils.h" -#include "node/nodeundo.h" -#include "widget/keyframeview/keyframeviewundo.h" +#include "oakengine/node.h" namespace olive { #define super KeyframeView +namespace +{ + +// Map a keyframe track's scalar QVariant into the facade POD for the +// input's declared type (the curve view drags numeric tracks). +void track_value_to_c(NodeValue::Type declared, const QVariant &value, + oak_node_value *out) +{ + memset(out, 0, sizeof(*out)); + switch (declared) { + case NodeValue::k_int: + out->type = OAK_NODE_VALUE_INT; + out->num = value.toLongLong(); + break; + case NodeValue::k_combo: + out->type = OAK_NODE_VALUE_COMBO; + out->num = value.toLongLong(); + break; + case NodeValue::k_boolean: + out->type = OAK_NODE_VALUE_BOOL; + out->num = value.toBool() ? 1 : 0; + break; + case NodeValue::k_rational: { + const Rational r = value.value(); + out->type = OAK_NODE_VALUE_RATIONAL; + out->num = r.numerator(); + out->den = r.denominator(); + break; + } + case NodeValue::k_color: + out->type = OAK_NODE_VALUE_COLOR; + out->f[0] = value.toDouble(); + break; + case NodeValue::k_vec2: + out->type = OAK_NODE_VALUE_VEC2; + out->f[0] = value.toDouble(); + break; + case NodeValue::k_vec3: + out->type = OAK_NODE_VALUE_VEC3; + out->f[0] = value.toDouble(); + break; + case NodeValue::k_vec4: + out->type = OAK_NODE_VALUE_VEC4; + out->f[0] = value.toDouble(); + break; + default: + out->type = OAK_NODE_VALUE_FLOAT; + out->f[0] = value.toDouble(); + break; + } +} + +} // namespace + CurveView::CurveView(QWidget *parent) : KeyframeView(parent) , dragging_bezier_pt_(nullptr) @@ -410,28 +463,40 @@ void CurveView::first_chance_mouse_move(QMouseEvent *event) void CurveView::first_chance_mouse_release(QMouseEvent *event) { - MultiUndoCommand *command = new MultiUndoCommand(); - - // Create undo command with the current bezier point and the old one - command->add_child(new KeyframeSetBezierControlPoint( - dragging_bezier_pt_->keyframe, dragging_bezier_pt_->type, - dragging_bezier_pt_->keyframe->bezier_control(dragging_bezier_pt_->type), - dragging_bezier_point_start_)); + // Through the liboakengine C ABI facade with the drag-start point(s) + // as the explicit old values (the drag already live-set the new + // ones); one undoable command per handle, same as the old + // KeyframeSetBezierControlPoint children. + NodeKeyframe *key = dragging_bezier_pt_->keyframe; + OakEngineNode *handle = + reinterpret_cast(key->parent()); + int tbn = 0, tbd = 0; + oakengine_node_frame_time_base(handle, &tbn, &tbd); + const int64_t ts = Timecode::time_to_timestamp( + key->time(), Rational(tbn, tbd), Timecode::k_round); + const QPointF current = + key->bezier_control(dragging_bezier_pt_->type); + oakengine_node_keyframe_set_bezier_point( + handle, key->input().toUtf8().constData(), key->element(), ts, + key->track(), + (dragging_bezier_pt_->type == NodeKeyframe::k_in_handle) ? 0 : 1, + current.x(), current.y(), dragging_bezier_point_start_.x(), + dragging_bezier_point_start_.y()); if (!(event->modifiers() & Qt::ControlModifier)) { auto opposing_type = NodeKeyframe::get_opposing_bezier_type(dragging_bezier_pt_->type); - - command->add_child(new KeyframeSetBezierControlPoint( - dragging_bezier_pt_->keyframe, opposing_type, - dragging_bezier_pt_->keyframe->bezier_control(opposing_type), - dragging_bezier_point_opposing_start_)); + const QPointF opposing_current = key->bezier_control(opposing_type); + oakengine_node_keyframe_set_bezier_point( + handle, key->input().toUtf8().constData(), key->element(), ts, + key->track(), + (opposing_type == NodeKeyframe::k_in_handle) ? 0 : 1, + opposing_current.x(), opposing_current.y(), + dragging_bezier_point_opposing_start_.x(), + dragging_bezier_point_opposing_start_.y()); } dragging_bezier_pt_ = nullptr; - - Core::instance()->undo_stack()->push( - command, tr("Moved Keyframe Bezier Control Point")); } void CurveView::keyframe_drag_start(QMouseEvent *event) @@ -517,13 +582,65 @@ void CurveView::keyframe_drag_move(QMouseEvent *event, QString &tip) void CurveView::keyframe_drag_release(QMouseEvent *event, MultiUndoCommand *command) { + Q_UNUSED(command) // the facade pushes its own single command below + + // Group the changed keys by owning input and push ONE undoable + // command per group through the liboakengine C ABI facade, with the + // drag-start values as the explicit undo values (the drag already + // live-set the new ones). + struct ValueGroup { + Node *node; + QString input; + int element; + QVector times; + QVector tracks; + std::vector values; + std::vector olds; + }; + QVector groups; for (size_t i = 0; i < get_selected_keyframes().size(); i++) { NodeKeyframe *k = get_selected_keyframes().at(i); - if (!qFuzzyCompare(k->value().toDouble(), - drag_keyframe_values_.at(i).toDouble())) { - command->add_child(new NodeParamSetKeyframeValueCommand( - k, k->value(), drag_keyframe_values_.at(i))); + if (qFuzzyCompare(k->value().toDouble(), + drag_keyframe_values_.at(i).toDouble())) { + continue; } + + int g = 0; + for (; g < groups.size(); g++) { + if (groups.at(g).node == k->parent() && + groups.at(g).input == k->input() && + groups.at(g).element == k->element()) { + break; + } + } + if (g == groups.size()) { + groups.append( + { k->parent(), k->input(), k->element(), {}, {}, {}, {} }); + } + + OakEngineNode *handle = + reinterpret_cast(k->parent()); + int tbn = 0, tbd = 0; + oakengine_node_frame_time_base(handle, &tbn, &tbd); + groups[g].times.append(Timecode::time_to_timestamp( + k->time(), Rational(tbn, tbd), Timecode::k_round)); + groups[g].tracks.append(k->track()); + + const NodeValue::Type declared = + k->parent()->get_input_data_type(k->input()); + oak_node_value new_v, old_v; + track_value_to_c(declared, k->value(), &new_v); + track_value_to_c(declared, drag_keyframe_values_.at(i), &old_v); + groups[g].values.push_back(new_v); + groups[g].olds.push_back(old_v); + } + + foreach (const ValueGroup &g, groups) { + oakengine_node_keyframes_set_value_many( + reinterpret_cast(g.node), + g.input.toUtf8().constData(), g.element, g.times.constData(), + g.tracks.data(), g.times.size(), g.values.data(), + g.olds.data()); } } diff --git a/app/widget/keyframeview/CMakeLists.txt b/app/widget/keyframeview/CMakeLists.txt index 9e982fd60..d07f764c0 100644 --- a/app/widget/keyframeview/CMakeLists.txt +++ b/app/widget/keyframeview/CMakeLists.txt @@ -20,7 +20,5 @@ set(OLIVE_SOURCES widget/keyframeview/keyframeview.h widget/keyframeview/keyframeviewinputconnection.cpp widget/keyframeview/keyframeviewinputconnection.h - widget/keyframeview/keyframeviewundo.cpp - widget/keyframeview/keyframeviewundo.h PARENT_SCOPE ) diff --git a/app/widget/keyframeview/keyframeviewundo.cpp b/app/widget/keyframeview/keyframeviewundo.cpp deleted file mode 100644 index 268cda9c2..000000000 --- a/app/widget/keyframeview/keyframeviewundo.cpp +++ /dev/null @@ -1,87 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2022 Olive Team - Modifications Copyright (C) 2025 mikesolar - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#include "keyframeviewundo.h" - -#include "node/node.h" -#include "node/project/sequence/sequence.h" - -namespace olive -{ - -KeyframeSetTypeCommand::KeyframeSetTypeCommand(NodeKeyframe *key, - NodeKeyframe::Type type) - : key_(key) - , old_type_(key->type()) - , new_type_(type) -{ -} - -Project *KeyframeSetTypeCommand::get_relevant_project() const -{ - return key_->parent()->project(); -} - -void KeyframeSetTypeCommand::redo() -{ - key_->set_type(new_type_); -} - -void KeyframeSetTypeCommand::undo() -{ - key_->set_type(old_type_); -} - -KeyframeSetBezierControlPoint::KeyframeSetBezierControlPoint( - NodeKeyframe *key, NodeKeyframe::BezierType mode, const QPointF &point) - : key_(key) - , mode_(mode) - , old_point_(key->bezier_control(mode_)) - , new_point_(point) -{ -} - -KeyframeSetBezierControlPoint::KeyframeSetBezierControlPoint( - NodeKeyframe *key, NodeKeyframe::BezierType mode, const QPointF &new_point, - const QPointF &old_point) - : key_(key) - , mode_(mode) - , old_point_(old_point) - , new_point_(new_point) -{ -} - -Project *KeyframeSetBezierControlPoint::get_relevant_project() const -{ - return key_->parent()->project(); -} - -void KeyframeSetBezierControlPoint::redo() -{ - key_->set_bezier_control(mode_, new_point_); -} - -void KeyframeSetBezierControlPoint::undo() -{ - key_->set_bezier_control(mode_, old_point_); -} - -} diff --git a/app/widget/keyframeview/keyframeviewundo.h b/app/widget/keyframeview/keyframeviewundo.h deleted file mode 100644 index a737f7ee8..000000000 --- a/app/widget/keyframeview/keyframeviewundo.h +++ /dev/null @@ -1,77 +0,0 @@ -/*** - - Olive - Non-Linear Video Editor - Copyright (C) 2022 Olive Team - Modifications Copyright (C) 2025 mikesolar - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -***/ - -#ifndef OAK_KEYFRAMEVIEWUNDO_H -#define OAK_KEYFRAMEVIEWUNDO_H - -#include "node/keyframe.h" -#include "undo/undocommand.h" - -namespace olive -{ - -class KeyframeSetTypeCommand : public UndoCommand { -public: - KeyframeSetTypeCommand(NodeKeyframe *key, NodeKeyframe::Type type); - - virtual Project *get_relevant_project() const override; - -protected: - virtual void redo() override; - virtual void undo() override; - -private: - NodeKeyframe *key_; - - NodeKeyframe::Type old_type_; - - NodeKeyframe::Type new_type_; -}; - -class KeyframeSetBezierControlPoint : public UndoCommand { -public: - KeyframeSetBezierControlPoint(NodeKeyframe *key, - NodeKeyframe::BezierType mode, - const QPointF &point); - KeyframeSetBezierControlPoint(NodeKeyframe *key, - NodeKeyframe::BezierType mode, - const QPointF &new_point, - const QPointF &old_point); - - virtual Project *get_relevant_project() const override; - -protected: - virtual void redo() override; - virtual void undo() override; - -private: - NodeKeyframe *key_; - - NodeKeyframe::BezierType mode_; - - QPointF old_point_; - - QPointF new_point_; -}; - -} - -#endif // OAK_KEYFRAMEVIEWUNDO_H diff --git a/engine/include/oakengine/node.h b/engine/include/oakengine/node.h index 2e5a9f40b..bd6b5daeb 100644 --- a/engine/include/oakengine/node.h +++ b/engine/include/oakengine/node.h @@ -437,6 +437,72 @@ OAKENGINE_API int oakengine_node_keyframes_set_type_many( OakEngineNode *self, const char *input_id, int element, const int64_t *times_ts, const int *tracks, int count, int type); +/** + * @brief Move several keyframes of one input to `new_time_ts` (undoable, + * ONE command; olive::NodeParamSetKeyframeTimeCommand per key, like the + * application's keyframe properties dialog). + * + * Keyframes are addressed individually by (`old_times_ts`[i], + * `tracks`[i]); `element` addresses the input's array element (-1 for + * non-array). Every old address must name an existing keyframe + * (OAKENGINE_E_NOT_FOUND otherwise), and no other keyframe may already + * sit at `new_time_ts` on a target track (OAKENGINE_E_STATE; moving to + * the key's own current time is allowed). On any failure nothing is + * pushed. Returns the number of moved keyframes (>= 0) or a negative + * code. + */ +OAKENGINE_API int oakengine_node_keyframes_set_time_many( + OakEngineNode *self, const char *input_id, int element, + const int64_t *old_times_ts, const int *tracks, int count, + int64_t new_time_ts); + +/** + * @brief Change the value of several keyframes of one input (undoable, + * ONE command; olive::NodeParamSetKeyframeValueCommand per key). + * + * `values`[i] is the new per-track component (mapped like + * oakengine_node_set_input_at_time(); the type must match the input's + * declared type). When `old_values` is not NULL, `old_values`[i] is + * recorded as the undo value -- for callers that already live-set the + * new values (the curve view's drag release); when NULL, each key's + * current value is captured at apply time. Every address must name an + * existing keyframe (OAKENGINE_E_NOT_FOUND; nothing pushed on failure). + * Returns the number of changed keyframes (>= 0) or a negative code. + */ +OAKENGINE_API int oakengine_node_keyframes_set_value_many( + OakEngineNode *self, const char *input_id, int element, + const int64_t *times_ts, const int *tracks, int count, + const oak_node_value *values, const oak_node_value *old_values); + +/** + * @brief Set both bezier control points of several keyframes of one + * input (undoable, ONE command; two point commands per key, like the + * keyframe properties dialog). The previous points are captured per + * key. Every address must name an existing keyframe + * (OAKENGINE_E_NOT_FOUND; nothing pushed on failure). Returns the + * number of affected keyframes (>= 0) or a negative code. + */ +OAKENGINE_API int oakengine_node_keyframes_set_bezier_many( + OakEngineNode *self, const char *input_id, int element, + const int64_t *times_ts, const int *tracks, int count, double in_x, + double in_y, double out_x, double out_y); + +/** + * @brief Set one bezier control point of one keyframe (undoable; + * the curve view's bezier-handle drag release). + * + * `point_index` is 0 for the in-handle and 1 for the out-handle. The + * new point is (x, y); (`old_x`, `old_y`) is the undo point recorded + * for callers that already live-set the new point during the drag -- + * pass NaN for either old component to capture the key's current point + * at apply time instead. OAKENGINE_E_NOT_FOUND when no keyframe exists + * at that time/track. + */ +OAKENGINE_API int oakengine_node_keyframe_set_bezier_point( + OakEngineNode *self, const char *input_id, int element, + int64_t time_ts, int track, int point_index, double x, double y, + double old_x, double old_y); + /** * @brief Remove all keyframes from the input (undoable, * olive::NodeImmediateRemoveAllKeyframesCommand). A no-op (OAKENGINE_OK) diff --git a/engine/src/capi/node.cpp b/engine/src/capi/node.cpp index 368c6d6aa..dfc791dd1 100644 --- a/engine/src/capi/node.cpp +++ b/engine/src/capi/node.cpp @@ -347,9 +347,9 @@ bool kf_value_to_c(olive::NodeValue::Type type, const QVariant &component, } // 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. +// keyframe type/bezier commands (the application layer used to carry +// them in app/widget/keyframeviewundo.h, since migrated here), so the +// facade carries minimal equivalents with the same old/new semantics. class KeyframeSetTypeCommand : public olive::UndoCommand { public: KeyframeSetTypeCommand(olive::NodeKeyframe *key, @@ -393,6 +393,20 @@ public: { } + // Explicit old point for callers that already live-set the new one + // (mirrors the application class's second constructor). + KeyframeSetBezierPointCommand(olive::NodeKeyframe *key, + olive::NodeKeyframe::BezierType mode, + const QPointF &new_point, + const QPointF &old_point) + : key_(key) + , mode_(mode) + , has_old_(true) + , old_point_(old_point) + , new_point_(new_point) + { + } + virtual olive::Project *get_relevant_project() const override { return key_->parent() ? key_->parent()->project() : nullptr; @@ -401,7 +415,10 @@ public: protected: virtual void redo() override { - old_point_ = key_->bezier_control(mode_); + if (!has_old_) { + old_point_ = key_->bezier_control(mode_); + has_old_ = true; + } key_->set_bezier_control(mode_, new_point_); } @@ -413,6 +430,7 @@ protected: private: olive::NodeKeyframe *key_; olive::NodeKeyframe::BezierType mode_; + bool has_old_ = false; QPointF old_point_; QPointF new_point_; }; @@ -435,6 +453,27 @@ olive::NodeValue::Type checked_keyframe_input(const olive::Node *self, return type; } +// Time-exact keyframe lookup that does not depend on the input's +// keyframing-enabled flag (Node::get_keyframe_at_time_on_track reports +// nothing when keyframing is off, but the application's keyframe editing +// operates on the keyframe objects regardless of the flag). +olive::NodeKeyframe *find_keyframe(const olive::Node *node, + const olive::NodeInput &input, + const olive::Rational &time, int track) +{ + const QVector &tracks = + node->get_keyframe_tracks(input.input(), input.element()); + if (track < 0 || track >= tracks.size()) { + return nullptr; + } + for (olive::NodeKeyframe *key : tracks.at(track)) { + if (key->time() == time) { + return key; + } + } + return nullptr; +} + } // namespace extern "C" @@ -1231,7 +1270,7 @@ int oakengine_node_keyframes_set_type_many(OakEngineNode *self, const olive::Rational time = olive::core::Timecode::timestamp_to_time(times_ts[i], tb); olive::NodeKeyframe *key = - node->get_keyframe_at_time_on_track(input, time, tracks[i]); + find_keyframe(node, input, time, tracks[i]); if (!key) { set_error(QStringLiteral("no keyframe at time %1 track %2 on " "\"%3\"") @@ -1248,6 +1287,222 @@ int oakengine_node_keyframes_set_type_many(OakEngineNode *self, return count; } +int oakengine_node_keyframes_set_time_many(OakEngineNode *self, + const char *input_id, int element, + const int64_t *old_times_ts, + const int *tracks, int count, + int64_t new_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; + } + if (count < 0 || (count > 0 && (!old_times_ts || !tracks)) || + new_time_ts < 0) { + set_error(QStringLiteral("invalid arguments")); + return OAKENGINE_E_INVALID; + } + if (count == 0) { + return 0; + } + const QString id = QString::fromUtf8(input_id); + const olive::Rational tb = project_time_base(node); + const olive::NodeInput input(node, id, element); + const olive::Rational new_time = + olive::core::Timecode::timestamp_to_time(new_time_ts, tb); + + // Resolve and conflict-check every key first so a failure has no side + // effects. + olive::MultiUndoCommand *command = new olive::MultiUndoCommand(); + for (int i = 0; i < count; i++) { + const olive::Rational old_time = + olive::core::Timecode::timestamp_to_time(old_times_ts[i], tb); + olive::NodeKeyframe *key = + find_keyframe(node, input, old_time, tracks[i]); + if (!key) { + set_error(QStringLiteral("no keyframe at time %1 track %2 on " + "\"%3\"") + .arg(old_times_ts[i]) + .arg(tracks[i]) + .arg(id)); + delete command; + return OAKENGINE_E_NOT_FOUND; + } + olive::NodeKeyframe *occupant = + find_keyframe(node, input, new_time, tracks[i]); + if (occupant && occupant != key) { + set_error(QStringLiteral("a keyframe already exists at time %1 " + "on track %2") + .arg(new_time_ts) + .arg(tracks[i])); + delete command; + return OAKENGINE_E_STATE; + } + command->add_child(new olive::NodeParamSetKeyframeTimeCommand( + key, new_time, key->time())); + } + push_or_run(command, QStringLiteral("Set Keyframe Time")); + return count; +} + +int oakengine_node_keyframes_set_value_many(OakEngineNode *self, + const char *input_id, int element, + const int64_t *times_ts, + const int *tracks, int count, + const oak_node_value *values, + const oak_node_value *old_values) +{ + 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 (count < 0 || (count > 0 && (!times_ts || !tracks || !values))) { + set_error(QStringLiteral("invalid arguments")); + return OAKENGINE_E_INVALID; + } + if (count == 0) { + return 0; + } + const QString id = QString::fromUtf8(input_id); + const olive::Rational tb = project_time_base(node); + const olive::NodeInput input(node, id, element); + + olive::MultiUndoCommand *command = new olive::MultiUndoCommand(); + for (int i = 0; i < count; i++) { + const olive::Rational time = + olive::core::Timecode::timestamp_to_time(times_ts[i], tb); + olive::NodeKeyframe *key = + find_keyframe(node, input, time, tracks[i]); + if (!key) { + set_error(QStringLiteral("no keyframe at time %1 track %2 on " + "\"%3\"") + .arg(times_ts[i]) + .arg(tracks[i]) + .arg(id)); + delete command; + return OAKENGINE_E_NOT_FOUND; + } + QVariant new_value; + if (!component_from_c(&values[i], declared, 0, &new_value)) { + set_error(QStringLiteral( + "value type does not match the declared input type")); + delete command; + return OAKENGINE_E_INVALID; + } + if (old_values) { + QVariant old_value; + if (!component_from_c(&old_values[i], declared, 0, &old_value)) { + set_error(QStringLiteral( + "old value type does not match the declared input type")); + delete command; + return OAKENGINE_E_INVALID; + } + command->add_child(new olive::NodeParamSetKeyframeValueCommand( + key, new_value, old_value)); + } else { + command->add_child(new olive::NodeParamSetKeyframeValueCommand( + key, new_value, key->value())); + } + } + push_or_run(command, QStringLiteral("Set Keyframe Value")); + return count; +} + +int oakengine_node_keyframes_set_bezier_many(OakEngineNode *self, + const char *input_id, + int element, + const int64_t *times_ts, + const int *tracks, int count, + double in_x, double in_y, + double out_x, double out_y) +{ + 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 (count < 0 || (count > 0 && (!times_ts || !tracks))) { + set_error(QStringLiteral("invalid arguments")); + return OAKENGINE_E_INVALID; + } + if (count == 0) { + return 0; + } + const QString id = QString::fromUtf8(input_id); + const olive::Rational tb = project_time_base(node); + const olive::NodeInput input(node, id, element); + + olive::MultiUndoCommand *command = new olive::MultiUndoCommand(); + for (int i = 0; i < count; i++) { + const olive::Rational time = + olive::core::Timecode::timestamp_to_time(times_ts[i], tb); + olive::NodeKeyframe *key = + find_keyframe(node, input, time, tracks[i]); + if (!key) { + set_error(QStringLiteral("no keyframe at time %1 track %2 on " + "\"%3\"") + .arg(times_ts[i]) + .arg(tracks[i]) + .arg(id)); + delete command; + return OAKENGINE_E_NOT_FOUND; + } + command->add_child(new KeyframeSetBezierPointCommand( + key, olive::NodeKeyframe::k_in_handle, QPointF(in_x, in_y))); + command->add_child(new KeyframeSetBezierPointCommand( + key, olive::NodeKeyframe::k_out_handle, QPointF(out_x, out_y))); + } + push_or_run(command, QStringLiteral("Set Keyframe Bezier Points")); + return count; +} + +int oakengine_node_keyframe_set_bezier_point( + OakEngineNode *self, const char *input_id, int element, int64_t time_ts, + int track, int point_index, double x, double y, double old_x, + double old_y) +{ + 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 (point_index < 0 || point_index > 1) { + set_error(QStringLiteral("invalid bezier point index %1") + .arg(point_index)); + 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); + olive::NodeKeyframe *key = find_keyframe( + node, olive::NodeInput(node, id, element), time, track); + if (!key) { + set_error(QStringLiteral("no keyframe at time %1 track %2 on \"%3\"") + .arg(time_ts) + .arg(track) + .arg(id)); + return OAKENGINE_E_NOT_FOUND; + } + const olive::NodeKeyframe::BezierType mode = + (point_index == 0) ? olive::NodeKeyframe::k_in_handle : + olive::NodeKeyframe::k_out_handle; + if (std::isnan(old_x) || std::isnan(old_y)) { + push_or_run(new KeyframeSetBezierPointCommand(key, mode, + QPointF(x, y)), + QStringLiteral("Set Keyframe Bezier Point")); + } else { + push_or_run(new KeyframeSetBezierPointCommand( + key, mode, QPointF(x, y), QPointF(old_x, old_y)), + QStringLiteral("Set Keyframe Bezier Point")); + } + return OAKENGINE_OK; +} + int oakengine_node_keyframes_clear(OakEngineNode *self, const char *input_id) { set_error(QString()); diff --git a/engine/tests/oakengine_keyframe_test.cpp b/engine/tests/oakengine_keyframe_test.cpp index d28bdb809..345acf1a6 100644 --- a/engine/tests/oakengine_keyframe_test.cpp +++ b/engine/tests/oakengine_keyframe_test.cpp @@ -418,6 +418,150 @@ static void test_panel_paths(OakEngineProject *project, assert(oakengine_node_keyframe_count(opacity, "opacity_in") == 3); } +static void test_keyframe_properties(OakEngineProject *project, + OakEngineNode *opacity) +{ + // `opacity` has three keys at 0/15/30 (values 0.0/0.5/1.0) with + // easings hold/bezier/linear from the earlier tests. + oak_node_value out; + int64_t ts = -1; + const int tr1[1] = { 0 }; + + // set_time_many: move 15 -> 20 and undo. + const int64_t olds[1] = { 15 }; + assert(oakengine_node_keyframes_set_time_many(opacity, "opacity_in", -1, + olds, tr1, 1, 20) == 1); + assert(oakengine_node_keyframe_at(opacity, "opacity_in", 1, &ts, NULL) == + OAKENGINE_OK); + assert(ts == 20); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + assert(oakengine_node_keyframe_at(opacity, "opacity_in", 1, &ts, NULL) == + OAKENGINE_OK); + assert(ts == 15); + + // Moving onto an occupied time is a conflict; onto the key's own time + // is allowed; a missing key is NOT_FOUND. Failures push nothing. + assert(oakengine_node_keyframes_set_time_many(opacity, "opacity_in", -1, + olds, tr1, 1, + 30) == OAKENGINE_E_STATE); + assert(oakengine_node_keyframes_set_time_many(opacity, "opacity_in", -1, + olds, tr1, 1, 15) == 1); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + const int64_t miss[1] = { 99 }; + assert(oakengine_node_keyframes_set_time_many(opacity, "opacity_in", -1, + miss, tr1, 1, + 40) == OAKENGINE_E_NOT_FOUND); + assert(oakengine_node_keyframes_set_time_many(NULL, "opacity_in", -1, + olds, tr1, 1, + 40) == OAKENGINE_E_INVALID); + assert(oakengine_node_keyframes_set_time_many(opacity, "opacity_in", -1, + olds, tr1, 0, 40) == 0); + assert(oakengine_node_keyframe_count(opacity, "opacity_in") == 3); + + // set_value_many: captured old values are restored by undo. + const int64_t times2[2] = { 0, 15 }; + const int tr2[2] = { 0, 0 }; + oak_node_value nv[2]; + nv[0] = float_value(0.7); + nv[1] = float_value(0.6); + assert(oakengine_node_keyframes_set_value_many(opacity, "opacity_in", -1, + times2, tr2, 2, nv, + NULL) == 2); + assert(oakengine_node_keyframe_at(opacity, "opacity_in", 0, &ts, &out) == + OAKENGINE_OK); + assert(fabs(out.f[0] - 0.7) < 1e-9); + assert(oakengine_node_keyframe_at(opacity, "opacity_in", 1, &ts, &out) == + OAKENGINE_OK); + assert(fabs(out.f[0] - 0.6) < 1e-9); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + assert(oakengine_node_keyframe_at(opacity, "opacity_in", 0, &ts, &out) == + OAKENGINE_OK); + assert(fabs(out.f[0] - 0.0) < 1e-9); + assert(oakengine_node_keyframe_at(opacity, "opacity_in", 1, &ts, &out) == + OAKENGINE_OK); + assert(fabs(out.f[0] - 0.5) < 1e-9); + + // Explicit old values (the live-set drag pattern): undo restores the + // recorded old, redo the new. + oak_node_value ov[2]; + ov[0] = float_value(9.9); + ov[1] = float_value(9.8); + assert(oakengine_node_keyframes_set_value_many(opacity, "opacity_in", -1, + times2, tr2, 2, nv, + ov) == 2); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + assert(oakengine_node_keyframe_at(opacity, "opacity_in", 0, &ts, &out) == + OAKENGINE_OK); + assert(fabs(out.f[0] - 9.9) < 1e-9); + assert(oakengine_project_redo(project) == OAKENGINE_OK); + oak_node_value orig[2]; + orig[0] = float_value(0.0); + orig[1] = float_value(0.5); + assert(oakengine_node_keyframes_set_value_many(opacity, "opacity_in", -1, + times2, tr2, 2, orig, + NULL) == 2); + + // bezier_many: both control points on both keys, undo restores each + // key's own previous points (key 15 had 0.1/0.2/0.3/0.4). + assert(oakengine_node_keyframes_set_bezier_many( + opacity, "opacity_in", -1, times2, tr2, 2, 0.11f, 0.22f, 0.33f, + 0.44f) == 2); + float x1 = 0, y1 = 0, x2 = 0, y2 = 0; + int type = -1; + assert(oakengine_node_keyframe_get_easing(opacity, "opacity_in", 1, &x1, + &y1, &x2, &y2, + &type) == OAKENGINE_OK); + assert(fabsf(x1 - 0.11f) < 1e-6f && fabsf(y1 - 0.22f) < 1e-6f && + fabsf(x2 - 0.33f) < 1e-6f && fabsf(y2 - 0.44f) < 1e-6f); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + assert(oakengine_node_keyframe_get_easing(opacity, "opacity_in", 1, &x1, + &y1, &x2, &y2, + &type) == OAKENGINE_OK); + assert(fabsf(x1 - 0.1f) < 1e-6f && fabsf(y1 - 0.2f) < 1e-6f); + assert(oakengine_node_keyframes_set_bezier_many( + opacity, "opacity_in", -1, miss, tr1, 1, 0.f, 0.f, 0.f, + 0.f) == OAKENGINE_E_NOT_FOUND); + + // set_bezier_point: NaN old captures the current point. + assert(oakengine_node_keyframe_set_bezier_point( + opacity, "opacity_in", -1, 15, 0, 0, 0.5f, 0.6f, NAN, + NAN) == OAKENGINE_OK); + assert(oakengine_node_keyframe_get_easing(opacity, "opacity_in", 1, &x1, + &y1, &x2, &y2, + &type) == OAKENGINE_OK); + assert(fabsf(x1 - 0.5f) < 1e-6f && fabsf(y1 - 0.6f) < 1e-6f); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + assert(oakengine_node_keyframe_get_easing(opacity, "opacity_in", 1, &x1, + &y1, &x2, &y2, + &type) == OAKENGINE_OK); + assert(fabsf(x1 - 0.1f) < 1e-6f && fabsf(y1 - 0.2f) < 1e-6f); + + // Explicit old restores the recorded point on undo. + assert(oakengine_node_keyframe_set_bezier_point( + opacity, "opacity_in", -1, 15, 0, 0, 0.7f, 0.8f, 9.0f, + 9.0f) == OAKENGINE_OK); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + assert(oakengine_node_keyframe_get_easing(opacity, "opacity_in", 1, &x1, + &y1, &x2, &y2, + &type) == OAKENGINE_OK); + assert(fabsf(x1 - 9.0f) < 1e-6f); + // Put the point back so later state matches the earlier tests. + assert(oakengine_node_keyframe_set_bezier_point( + opacity, "opacity_in", -1, 15, 0, 0, 0.1f, 0.2f, NAN, + NAN) == OAKENGINE_OK); + + // Errors: bad point index, missing key, NULL node. + assert(oakengine_node_keyframe_set_bezier_point( + opacity, "opacity_in", -1, 15, 0, 2, 0.f, 0.f, 0.f, + 0.f) == OAKENGINE_E_INVALID); + assert(oakengine_node_keyframe_set_bezier_point( + opacity, "opacity_in", -1, 99, 0, 0, 0.f, 0.f, 0.f, + 0.f) == OAKENGINE_E_NOT_FOUND); + assert(oakengine_node_keyframe_set_bezier_point( + NULL, "opacity_in", -1, 15, 0, 0, 0.f, 0.f, 0.f, + 0.f) == OAKENGINE_E_INVALID); +} + int main(void) { make_tmpdir(); @@ -452,6 +596,7 @@ int main(void) test_easing_and_remove(project, opacity); test_rational_and_color(project, timeremap, solid); test_panel_paths(project, opacity, solid); + test_keyframe_properties(project, opacity); oakengine_project_free(project); assert(oakengine_shutdown() == OAKENGINE_OK); diff --git a/tests/gtest/dialog_editing_test.cpp b/tests/gtest/dialog_editing_test.cpp index 9bd7ce4ab..70865f7cd 100644 --- a/tests/gtest/dialog_editing_test.cpp +++ b/tests/gtest/dialog_editing_test.cpp @@ -251,6 +251,16 @@ TEST(DialogKeyframeProperties, SingleKeyAcceptWritesAllFields) ensure_app_singletons(); auto project = create_project(); + // A 24 fps sequence gives the facade's keyframe timestamps a + // frame-aligned timebase (the dialog moves the key to 1/2 s = 12 + // frames; without a sequence the facade's default timebase would not + // land on it exactly). + auto *seq = new olive::Sequence(); + seq->setParent(project.get()); + seq->set_video_params(olive::VideoParams( + 1920, 1080, olive::Rational(1, 24), olive::PixelFormat::f32, + olive::VideoParams::k_internal_channel_count)); + auto *node = new olive::MathNode(); node->setParent(project.get()); auto *key = new olive::NodeKeyframe(olive::Rational(0), 1.0, diff --git a/tests/gtest/widget_curve_keyframe_test.cpp b/tests/gtest/widget_curve_keyframe_test.cpp index a8241dc4e..967109e41 100644 --- a/tests/gtest/widget_curve_keyframe_test.cpp +++ b/tests/gtest/widget_curve_keyframe_test.cpp @@ -1,5 +1,6 @@ #include +#include #include #include @@ -10,11 +11,12 @@ #include "node/math/math/math.h" #include "node/nodeundo.h" #include "node/project.h" +#include "oakengine/node.h" +#include "oakengine/project.h" #include "render/diskmanager.h" #include "widget/curvewidget/curveview.h" #include "widget/curvewidget/curvewidget.h" #include "widget/keyframeview/keyframeview.h" -#include "widget/keyframeview/keyframeviewundo.h" #include "widget/nodetreeview/nodetreeview.h" using namespace olive; @@ -197,6 +199,7 @@ protected: void SetUp() override { ColorManager::set_up_default_config(); + ensure_app_singletons(); project_ = std::make_unique(); project_->initialize(); @@ -209,54 +212,73 @@ protected: MathNode *node_ = nullptr; }; -TEST_F(KeyframeViewUndoTest, SetTypeCommandSwitchesAndRestoresType) +TEST_F(KeyframeViewUndoTest, SetTypeSwitchesAndRestoresType) { NodeKeyframe *key = insert_keyframe(node_, MathNode::k_param_a_in, Rational(0), 0.0); ASSERT_EQ(key->type(), NodeKeyframe::k_linear); - KeyframeSetTypeCommand command(key, NodeKeyframe::k_bezier); - EXPECT_EQ(command.get_relevant_project(), project_.get()); - - command.redo_now(); + // The app-side command class was replaced by the facade batch call; + // undo/redo ride the same global undo stack. + const int64_t times[1] = { 0 }; + const int tracks[1] = { 0 }; + EXPECT_EQ(oakengine_node_keyframes_set_type_many( + reinterpret_cast(node_), + MathNode::k_param_a_in.toUtf8().constData(), -1, times, + tracks, 1, 1), + 1); EXPECT_EQ(key->type(), NodeKeyframe::k_bezier); - command.undo_now(); + EXPECT_EQ(oakengine_project_undo( + reinterpret_cast(project_.get())), + OAKENGINE_OK); EXPECT_EQ(key->type(), NodeKeyframe::k_linear); + + EXPECT_EQ(oakengine_project_redo( + reinterpret_cast(project_.get())), + OAKENGINE_OK); + EXPECT_EQ(key->type(), NodeKeyframe::k_bezier); } -TEST_F(KeyframeViewUndoTest, SetBezierControlPointCapturesOldPointFromKeyframe) +TEST_F(KeyframeViewUndoTest, SetBezierPointCapturesOldPointFromKeyframe) { NodeKeyframe *key = insert_keyframe(node_, MathNode::k_param_a_in, Rational(0), 0.0); key->set_type(NodeKeyframe::k_bezier); key->set_bezier_control_in(QPointF(0.1, 0.2)); - KeyframeSetBezierControlPoint command(key, NodeKeyframe::k_in_handle, - QPointF(0.5, 0.6)); - EXPECT_EQ(command.get_relevant_project(), project_.get()); - - command.redo_now(); + // NaN old components make the facade capture the current point. + EXPECT_EQ(oakengine_node_keyframe_set_bezier_point( + reinterpret_cast(node_), + MathNode::k_param_a_in.toUtf8().constData(), -1, 0, 0, 0, + 0.5, 0.6, NAN, NAN), + OAKENGINE_OK); EXPECT_EQ(key->bezier_control_in(), QPointF(0.5, 0.6)); - command.undo_now(); + EXPECT_EQ(oakengine_project_undo( + reinterpret_cast(project_.get())), + OAKENGINE_OK); EXPECT_EQ(key->bezier_control_in(), QPointF(0.1, 0.2)); } -TEST_F(KeyframeViewUndoTest, SetBezierControlPointWithExplicitOldPoint) +TEST_F(KeyframeViewUndoTest, SetBezierPointWithExplicitOldPoint) { NodeKeyframe *key = insert_keyframe(node_, MathNode::k_param_a_in, Rational(0), 0.0); key->set_type(NodeKeyframe::k_bezier); - // The four-argument overload does not read the current control point - KeyframeSetBezierControlPoint command(key, NodeKeyframe::k_out_handle, - QPointF(0.7, 0.8), QPointF(0.3, 0.4)); - - command.redo_now(); + // Explicit old components are recorded for undo (the live-set drag + // pattern; the current point is not read). + EXPECT_EQ(oakengine_node_keyframe_set_bezier_point( + reinterpret_cast(node_), + MathNode::k_param_a_in.toUtf8().constData(), -1, 0, 0, 1, + 0.7, 0.8, 0.3, 0.4), + OAKENGINE_OK); EXPECT_EQ(key->bezier_control_out(), QPointF(0.7, 0.8)); - command.undo_now(); + EXPECT_EQ(oakengine_project_undo( + reinterpret_cast(project_.get())), + OAKENGINE_OK); EXPECT_EQ(key->bezier_control_out(), QPointF(0.3, 0.4)); }