diff --git a/app/widget/curvewidget/curvewidget.cpp b/app/widget/curvewidget/curvewidget.cpp index a714917ae..fc35f94ea 100644 --- a/app/widget/curvewidget/curvewidget.cpp +++ b/app/widget/curvewidget/curvewidget.cpp @@ -30,7 +30,7 @@ #include "core.h" #include "common/qtutils.h" #include "node/node.h" -#include "widget/keyframeview/keyframeviewundo.h" +#include "oakengine/node.h" #include "widget/timeruler/timeruler.h" namespace olive @@ -341,14 +341,50 @@ void CurveWidget::keyframe_type_button_triggered(bool checked) // Ensure only the appropriate button is checked set_keyframe_button_checked_from_type(new_type); - MultiUndoCommand *command = new MultiUndoCommand(); - + // Through the liboakengine C ABI facade: one undoable command per + // distinct input (usually just one), with the same batch semantics as + // the old per-keyframe commands. + struct TypeGroup { + Node *node; + QString input; + int element; + QVector times; + QVector tracks; + }; + QVector groups; foreach (NodeKeyframe *item, selected) { - command->add_child(new KeyframeSetTypeCommand(item, new_type)); + 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()); + } + int facade_type = 0; + if (new_type == NodeKeyframe::k_bezier) { + facade_type = 1; + } else if (new_type == NodeKeyframe::k_hold) { + facade_type = 2; + } + foreach (const TypeGroup &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("Changed Type of %1 Keyframe(s) to %2")); } void CurveWidget::input_selection_changed(const NodeKeyframeTrackReference &ref) diff --git a/app/widget/keyframeview/keyframeview.cpp b/app/widget/keyframeview/keyframeview.cpp index 7a3fcb3e4..0b959d809 100644 --- a/app/widget/keyframeview/keyframeview.cpp +++ b/app/widget/keyframeview/keyframeview.cpp @@ -27,11 +27,11 @@ #include "common/qtutils.h" #include "dialog/keyframeproperties/keyframeproperties.h" -#include "keyframeviewundo.h" #include "node/group/group.h" #include "node/node.h" #include "node/nodeundo.h" #include "node/project/serializer/serializer.h" +#include "oakengine/node.h" #include "widget/menu/menu.h" #include "widget/menu/menushared.h" @@ -645,13 +645,51 @@ void KeyframeView::show_context_menu() new_type = NodeKeyframe::k_linear; } - MultiUndoCommand *command = new MultiUndoCommand(); + // Through the liboakengine C ABI facade: one undoable command + // per distinct input (usually just one), with the same batch + // semantics as the old per-keyframe commands. + struct TypeGroup { + Node *node; + QString input; + int element; + QVector times; + QVector tracks; + }; + QVector groups; foreach (NodeKeyframe *item, get_selected_keyframes()) { - command->add_child(new KeyframeSetTypeCommand(item, new_type)); + 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()); + } + int facade_type = 0; + if (new_type == NodeKeyframe::k_bezier) { + facade_type = 1; + } else if (new_type == NodeKeyframe::k_hold) { + facade_type = 2; + } + foreach (const TypeGroup &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 Type of %1 Keyframe(s)") - .arg(get_selected_keyframes().size())); } } } diff --git a/app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp b/app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp index 5efb922ff..fe4cd5b7e 100644 --- a/app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp +++ b/app/widget/nodeparamview/nodeparamviewconnectedlabel.cpp @@ -26,7 +26,7 @@ #include "common/qtutils.h" #include "core.h" #include "node/node.h" -#include "node/nodeundo.h" +#include "oakengine/node.h" #include "widget/collapsebutton/collapsebutton.h" #include "widget/menu/menu.h" @@ -150,9 +150,11 @@ void NodeParamViewConnectedLabel::show_label_context_menu() QAction *disconnect_action = m.addAction(tr("Disconnect")); connect(disconnect_action, &QAction::triggered, this, [this]() { - Core::instance()->undo_stack()->push( - new NodeEdgeRemoveCommand(connected_node_, input_), - Node::get_disconnect_command_string(connected_node_, input_)); + // Through the liboakengine C ABI facade (one undoable command, + // array element included, same as the old NodeEdgeRemoveCommand). + oakengine_node_disconnect_ex( + reinterpret_cast(input_.node()), + input_.input().toUtf8().constData(), input_.element()); }); m.exec(QCursor::pos()); diff --git a/app/widget/nodeparamview/nodeparamviewitem.cpp b/app/widget/nodeparamview/nodeparamviewitem.cpp index d2c4f1975..0f66f9189 100644 --- a/app/widget/nodeparamview/nodeparamviewitem.cpp +++ b/app/widget/nodeparamview/nodeparamviewitem.cpp @@ -25,11 +25,10 @@ #include #include "common/qtutils.h" -#include "core.h" #include "dialog/speedduration/speeddurationdialog.h" #include "node/group/group.h" -#include "node/nodeundo.h" #include "node/project/sequence/sequence.h" +#include "oakengine/node.h" #include "pluginSupport/oliveplugininstance.h" namespace olive @@ -630,13 +629,12 @@ void NodeParamViewItemBody::array_append_clicked() if (it.value().append_btn == sender()) { NodeInput real_input = NodeGroup::resolve_input( NodeInput(it.key().node, it.key().input)); - Core::instance()->undo_stack()->push( - new NodeArrayInsertCommand(real_input.node(), - real_input.input(), - real_input.get_array_size()), - tr("Appended Array Element In %1 - %2") - .arg(real_input.node()->get_label_and_name(), - real_input.get_input_name())); + // Through the liboakengine C ABI facade (one undoable command, + // same as the old NodeArrayInsertCommand push). + oakengine_node_array_insert_at( + reinterpret_cast(real_input.node()), + real_input.input().toUtf8().constData(), + real_input.get_array_size()); break; } } @@ -648,10 +646,10 @@ void NodeParamViewItemBody::array_insert_clicked() if (it.value().array_insert_btn == sender()) { // Found our input and element NodeInput ic = NodeGroup::resolve_input(it.key()); - Core::instance()->undo_stack()->push( - new NodeArrayInsertCommand(ic.node(), ic.input(), ic.element()), - tr("Inserted Array Element In %1 - %2") - .arg(ic.node()->get_label_and_name(), ic.get_input_name())); + // Through the liboakengine C ABI facade (one undoable command). + oakengine_node_array_insert_at( + reinterpret_cast(ic.node()), + ic.input().toUtf8().constData(), ic.element()); break; } } @@ -663,10 +661,10 @@ void NodeParamViewItemBody::array_remove_clicked() if (it.value().array_remove_btn == sender()) { // Found our input and element NodeInput ic = NodeGroup::resolve_input(it.key()); - Core::instance()->undo_stack()->push( - new NodeArrayRemoveCommand(ic.node(), ic.input(), ic.element()), - tr("Removed Array Element In %1 - %2") - .arg(ic.node()->get_label_and_name(), ic.get_input_name())); + // Through the liboakengine C ABI facade (one undoable command). + oakengine_node_array_remove_at( + reinterpret_cast(ic.node()), + ic.input().toUtf8().constData(), ic.element()); break; } } diff --git a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp index 492261d73..b5d40b37f 100644 --- a/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp +++ b/app/widget/nodeparamview/nodeparamviewwidgetbridge.cpp @@ -37,6 +37,7 @@ #include "node/project/sequence/sequence.h" #include "nodeparamviewarraywidget.h" #include "nodeparamviewtextedit.h" +#include "oakengine/node.h" #include "render/lutlibrary.h" #include "undo/undostack.h" #include "widget/bezier/bezierwidget.h" @@ -75,6 +76,74 @@ int get_slider_count(NodeValue::Type type) return NodeValue::get_number_of_keyframe_tracks(type); } +namespace +{ + +// Map a panel widget's per-track scalar QVariant into the facade POD. +// Returns false for types that have no facade mapping (the caller keeps +// the legacy path for those). +bool variant_to_c_value(NodeValue::Type type, const QVariant &value, + oak_node_value *out) +{ + memset(out, 0, sizeof(*out)); + switch (type) { + case NodeValue::k_int: + out->type = OAK_NODE_VALUE_INT; + out->num = value.toLongLong(); + return true; + case NodeValue::k_combo: + out->type = OAK_NODE_VALUE_COMBO; + out->num = value.toLongLong(); + return true; + case NodeValue::k_float: + case NodeValue::k_bezier: + out->type = OAK_NODE_VALUE_FLOAT; + out->f[0] = value.toDouble(); + return true; + case NodeValue::k_boolean: + out->type = OAK_NODE_VALUE_BOOL; + out->num = value.toBool() ? 1 : 0; + return true; + case NodeValue::k_rational: { + const Rational r = value.value(); + out->type = OAK_NODE_VALUE_RATIONAL; + out->num = r.numerator(); + out->den = r.denominator(); + return true; + } + case NodeValue::k_color: + out->type = OAK_NODE_VALUE_COLOR; + out->f[0] = value.toDouble(); + return true; + case NodeValue::k_vec2: + out->type = OAK_NODE_VALUE_VEC2; + out->f[0] = value.toDouble(); + return true; + case NodeValue::k_vec3: + out->type = OAK_NODE_VALUE_VEC3; + out->f[0] = value.toDouble(); + return true; + case NodeValue::k_vec4: + out->type = OAK_NODE_VALUE_VEC4; + out->f[0] = value.toDouble(); + return true; + default: + return false; + } +} + +// Convert rational node time to the facade's frame timestamps using the +// facade's own timebase (so the round trip is exact). +int64_t node_time_to_ts(OakEngineNode *node, const Rational &time) +{ + int tbn = 0, tbd = 0; + oakengine_node_frame_time_base(node, &tbn, &tbd); + return Timecode::time_to_timestamp(time, Rational(tbn, tbd), + Timecode::k_round); +} + +} // namespace + void NodeParamViewWidgetBridge::create_widgets() { QWidget *parent = dynamic_cast(this->parent()); @@ -236,11 +305,37 @@ void NodeParamViewWidgetBridge::create_widgets() void NodeParamViewWidgetBridge::set_input_value(const QVariant &value, int track) { - MultiUndoCommand *command = new MultiUndoCommand(); + // POD values go through the liboakengine C ABI facade (one undoable + // command with the same set-value-at-time semantics as the old + // app-side assembly); types without a facade mapping keep the legacy + // undo assembly below. + const NodeInput &input = get_inner_input(); + oak_node_value c_value; + if (!variant_to_c_value(get_data_type(), value, &c_value)) { + MultiUndoCommand *command = new MultiUndoCommand(); + set_input_value_internal(value, track, command, true); + Core::instance()->undo_stack()->push(command, get_command_name()); + return; + } - set_input_value_internal(value, track, command, true); + oakengine_node_set_input_at_time( + reinterpret_cast(input.node()), + input.input().toUtf8().constData(), input.element(), + node_time_to_ts(reinterpret_cast(input.node()), + get_current_time_as_node_time()), + track, &c_value, 1); +} - Core::instance()->undo_stack()->push(command, get_command_name()); +void NodeParamViewWidgetBridge::set_string_value(const QString &value) +{ + // String-family inputs (file/text/font/str_combo) through the facade. + const NodeInput &input = get_inner_input(); + oakengine_node_set_input_string_at_time( + reinterpret_cast(input.node()), + input.input().toUtf8().constData(), input.element(), + node_time_to_ts(reinterpret_cast(input.node()), + get_current_time_as_node_time()), + value.toUtf8().constData()); } void NodeParamViewWidgetBridge::set_input_value_internal( @@ -336,7 +431,7 @@ void NodeParamViewWidgetBridge::widget_callback() break; } case NodeValue::k_file: { - set_input_value(static_cast(sender())->get_filename(), 0); + set_string_value(static_cast(sender())->get_filename()); break; } case NodeValue::k_color: { @@ -345,15 +440,27 @@ void NodeParamViewWidgetBridge::widget_callback() FloatSlider *slider = static_cast(sender()); process_slider(slider, slider->get_value()); } else { - // Sender is a ColorButton + // Sender is a ColorButton: all four components go through the + // facade in one undoable command (track -1). The + // color-management input properties are not undoable in the + // engine and stay direct (same as the old code). ManagedColor c = static_cast(sender())->get_color(); - MultiUndoCommand *command = new MultiUndoCommand(); - - set_input_value_internal(c.red(), 0, command, false); - set_input_value_internal(c.green(), 1, command, false); - set_input_value_internal(c.blue(), 2, command, false); - set_input_value_internal(c.alpha(), 3, command, false); + const NodeInput &input = get_inner_input(); + oak_node_value c_value; + memset(&c_value, 0, sizeof(c_value)); + c_value.type = OAK_NODE_VALUE_COLOR; + c_value.f[0] = c.red(); + c_value.f[1] = c.green(); + c_value.f[2] = c.blue(); + c_value.f[3] = c.alpha(); + oakengine_node_set_input_at_time( + reinterpret_cast(input.node()), + input.input().toUtf8().constData(), input.element(), + node_time_to_ts( + reinterpret_cast(input.node()), + get_current_time_as_node_time()), + -1, &c_value, 0); Node *n = get_inner_input().node(); n->blockSignals(true); @@ -369,15 +476,12 @@ void NodeParamViewWidgetBridge::widget_callback() QStringLiteral("col_look"), c.color_output().look()); n->blockSignals(false); - - Core::instance()->undo_stack()->push(command, get_command_name()); } break; } case NodeValue::k_text: { // Sender is a NodeParamViewRichText - set_input_value(static_cast(sender())->text(), - 0); + set_string_value(static_cast(sender())->text()); break; } case NodeValue::k_binary: { @@ -397,8 +501,8 @@ void NodeParamViewWidgetBridge::widget_callback() } case NodeValue::k_font: { // Widget is a QFontComboBox - set_input_value( - static_cast(sender())->currentFont().family(), 0); + set_string_value( + static_cast(sender())->currentFont().family()); break; } case NodeValue::k_combo: { @@ -421,9 +525,9 @@ void NodeParamViewWidgetBridge::widget_callback() QComboBox *cb = static_cast(widgets_.first()); const QVariant data = cb->currentData(); if (data.isValid()) { - set_input_value(data.toString(), 0); + set_string_value(data.toString()); } else { - set_input_value(cb->currentText(), 0); + set_string_value(cb->currentText()); } break; } diff --git a/app/widget/nodeparamview/nodeparamviewwidgetbridge.h b/app/widget/nodeparamview/nodeparamviewwidgetbridge.h index dd970d114..6631dd8aa 100644 --- a/app/widget/nodeparamview/nodeparamviewwidgetbridge.h +++ b/app/widget/nodeparamview/nodeparamviewwidgetbridge.h @@ -66,6 +66,8 @@ private: void set_input_value(const QVariant &value, int track); + void set_string_value(const QString &value); + void set_input_value_internal(const QVariant &value, int track, MultiUndoCommand *command, bool insert_on_all_tracks_if_no_key); diff --git a/engine/include/oakengine/node.h b/engine/include/oakengine/node.h index 261e5dbff..2e5a9f40b 100644 --- a/engine/include/oakengine/node.h +++ b/engine/include/oakengine/node.h @@ -224,6 +224,66 @@ OAKENGINE_API int oakengine_node_set_input_string(OakEngineNode *self, const char *input_id, const char *s); +/** + * @brief The frame timebase used for keyframe/parameter frame timestamps + * (seconds per frame: the frame rate of the project's first sequence + * flipped, or the engine default 1001/30000). Any pointer may be NULL. + * Use it to convert rational seconds to the timestamps this family + * takes, exactly like the facade does internally. + */ +OAKENGINE_API int oakengine_node_frame_time_base( + const OakEngineNode *self, int *num, int *den); + +/** + * @brief Write an input's value at a time (undoable, ONE command; + * olive::Node::set_value_at_time() -- the application's parameter + * panel commit path). + * + * When the input is keyframed this inserts or updates the keyframe at + * `time_ts` (the engine's set_value_at_time semantics); otherwise it + * sets the standard value on `track`. `element` is the array element + * (-1 for non-array inputs). `track` is the keyframe track/component; + * pass -1 to write ALL components of a split-track type (COLOR/VEC2/3/4) + * from `v->f[]` in the same command. `v->type` must match the input's + * declared type; a k_bezier input takes an OAK_NODE_VALUE_FLOAT + * component per track. String-family inputs (k_file/k_text/k_font/ + * k_str_combo) are rejected -- use oakengine_node_set_input_string_at_ + * time(). `insert_on_all_tracks` mirrors set_value_at_time's + * insert_on_all_tracks_if_no_key (ignored for `track` -1). + */ +OAKENGINE_API int oakengine_node_set_input_at_time( + OakEngineNode *self, const char *input_id, int element, int64_t time_ts, + int track, const oak_node_value *v, int insert_on_all_tracks); + +/** + * @brief Write a string-family input's value at a time (undoable, ONE + * command; k_file/k_text/k_font/k_str_combo on track 0, with + * insert_on_all_tracks_if_no_key semantics like the panel). + */ +OAKENGINE_API int oakengine_node_set_input_string_at_time( + OakEngineNode *self, const char *input_id, int element, int64_t time_ts, + const char *value); + +/* ---- Array inputs --------------------------------------------------------- */ + +/** + * @brief Insert an element into an array input at `index` (undoable, + * olive::NodeArrayInsertCommand; the panel's array append/insert + * buttons). `index` must be >= 0. + */ +OAKENGINE_API int oakengine_node_array_insert_at(OakEngineNode *self, + const char *input_id, + int index); + +/** + * @brief Remove the array element at `index` (undoable, + * olive::NodeArrayRemoveCommand). OAKENGINE_E_NOT_FOUND for an + * out-of-range index. + */ +OAKENGINE_API int oakengine_node_array_remove_at(OakEngineNode *self, + const char *input_id, + int index); + /* ---- Graph editing ------------------------------------------------------------- */ /** @@ -262,6 +322,16 @@ OAKENGINE_API int oakengine_node_connect(OakEngineNode *output_node, OAKENGINE_API int oakengine_node_disconnect(OakEngineNode *input_node, const char *input_id); +/** + * @brief Remove the edge feeding `input_node`'s `input_id` at `element` + * (undoable). Same as oakengine_node_disconnect() (which passes element + * -1) but addresses an array element, like the panel's connected-label + * disconnect for array inputs. + */ +OAKENGINE_API int oakengine_node_disconnect_ex(OakEngineNode *input_node, + const char *input_id, + int element); + /* ---- Parameter animation (keyframes) -------------------------------------- * * Keyframes live on an input's keyframe tracks (olive::NodeKeyframe). All @@ -350,6 +420,23 @@ OAKENGINE_API 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); +/** + * @brief Change only the easing TYPE of several keyframes of one input + * (undoable, ONE command; the application's keyframe view + * KeyframeSetTypeCommand, batched like its context-menu action). + * + * Unlike the rest of this family (track 0 only), keyframes are addressed + * individually by (`times_ts`[i], `tracks`[i]) because the view's + * selection may span tracks; `element` addresses the input's array + * element (-1 for non-array). Bezier control points are left untouched. + * Every address must name an existing keyframe or the whole call fails + * with OAKENGINE_E_NOT_FOUND and nothing is pushed. Returns the number + * of affected keyframes (>= 0) or a negative code. + */ +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 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 a168f5e04..368c6d6aa 100644 --- a/engine/src/capi/node.cpp +++ b/engine/src/capi/node.cpp @@ -668,6 +668,221 @@ int oakengine_node_set_input_string(OakEngineNode *self, return OAKENGINE_OK; } +int oakengine_node_frame_time_base(const OakEngineNode *self, int *num, + int *den) +{ + if (!self) { + return OAKENGINE_E_INVALID; + } + const olive::Rational tb = project_time_base(impl(self)); + if (num) { + *num = tb.numerator(); + } + if (den) { + *den = tb.denominator(); + } + return OAKENGINE_OK; +} + +// Component QVariant of a per-track POD for set_value_at_time: the +// panel's sliders carry one scalar per track (int64/double/Rational/ +// bool). Returns false on a type that has no scalar component here. +static bool component_from_c(const oak_node_value *v, + olive::NodeValue::Type declared, int component, + QVariant *out) +{ + switch (declared) { + case olive::NodeValue::k_int: + case olive::NodeValue::k_combo: + if (v->type != OAK_NODE_VALUE_INT && + v->type != OAK_NODE_VALUE_COMBO) { + return false; + } + *out = QVariant::fromValue(v->num); + return true; + case olive::NodeValue::k_float: + case olive::NodeValue::k_bezier: + if (v->type != OAK_NODE_VALUE_FLOAT) { + return false; + } + *out = QVariant::fromValue(v->f[0]); + return true; + case olive::NodeValue::k_boolean: + if (v->type != OAK_NODE_VALUE_BOOL) { + return false; + } + *out = QVariant::fromValue(v->num != 0); + return true; + case olive::NodeValue::k_rational: + if (v->type != OAK_NODE_VALUE_RATIONAL) { + return false; + } + *out = QVariant::fromValue( + olive::Rational(int(v->num), int(v->den))); + return true; + case olive::NodeValue::k_color: + case olive::NodeValue::k_vec2: + case olive::NodeValue::k_vec3: + case olive::NodeValue::k_vec4: + if (int(v->type) != int(to_c_type(declared))) { + return false; + } + *out = QVariant::fromValue(v->f[component]); + return true; + default: + return false; + } +} + +int oakengine_node_set_input_at_time(OakEngineNode *self, + const char *input_id, int element, + int64_t time_ts, int track, + const oak_node_value *v, + int insert_on_all_tracks) +{ + set_error(QString()); + olive::Node *node = impl(self); + const olive::NodeValue::Type declared = checked_input(node, input_id); + if (declared == olive::NodeValue::k_none) { + set_error(self && input_id ? + QStringLiteral("unknown input id \"%1\"") + .arg(QString::fromUtf8(input_id)) : + QStringLiteral("invalid arguments")); + return self && input_id ? OAKENGINE_E_NOT_FOUND : OAKENGINE_E_INVALID; + } + if (declared == olive::NodeValue::k_file || + declared == olive::NodeValue::k_text || + declared == olive::NodeValue::k_font || + declared == olive::NodeValue::k_str_combo) { + set_error(QStringLiteral( + "string inputs use oakengine_node_set_input_string_at_time")); + return OAKENGINE_E_INVALID; + } + const int nb_tracks = + olive::NodeValue::get_number_of_keyframe_tracks(declared); + if (!v || track < -1 || track >= nb_tracks || nb_tracks == 0) { + set_error(QStringLiteral("invalid arguments")); + 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)); + const olive::NodeInput input(node, id, element); + + // The panel's commit path (Node::set_value_at_time), one undoable + // command: keyframed inputs insert/update the keyframe at the time, + // others set the standard value on the track. + olive::MultiUndoCommand *command = new olive::MultiUndoCommand(); + if (track == -1) { + for (int i = 0; i < nb_tracks; i++) { + QVariant component; + if (!component_from_c(v, declared, i, &component)) { + set_error(QStringLiteral( + "value type does not match the declared input type")); + delete command; + return OAKENGINE_E_INVALID; + } + olive::Node::set_value_at_time(input, time, component, i, + command, false); + } + } else { + QVariant component; + if (!component_from_c(v, declared, 0, &component)) { + set_error(QStringLiteral( + "value type does not match the declared input type")); + delete command; + return OAKENGINE_E_INVALID; + } + olive::Node::set_value_at_time(input, time, component, track, + command, insert_on_all_tracks != 0); + } + push_or_run(command, QStringLiteral("Set Input Value")); + return OAKENGINE_OK; +} + +int oakengine_node_set_input_string_at_time(OakEngineNode *self, + const char *input_id, int element, + int64_t time_ts, const char *value) +{ + set_error(QString()); + olive::Node *node = impl(self); + const olive::NodeValue::Type declared = checked_input(node, input_id); + if (declared == olive::NodeValue::k_none) { + set_error(self && input_id ? + QStringLiteral("unknown input id \"%1\"") + .arg(QString::fromUtf8(input_id)) : + QStringLiteral("invalid arguments")); + return self && input_id ? OAKENGINE_E_NOT_FOUND : OAKENGINE_E_INVALID; + } + if (declared != olive::NodeValue::k_file && + declared != olive::NodeValue::k_text && + declared != olive::NodeValue::k_font && + declared != olive::NodeValue::k_str_combo) { + set_error(QStringLiteral("\"%1\" is not a string input") + .arg(QString::fromUtf8(input_id))); + return OAKENGINE_E_INVALID; + } + const olive::Rational time = olive::core::Timecode::timestamp_to_time( + time_ts, project_time_base(node)); + const QVariant v = + QVariant::fromValue(QString::fromUtf8(value ? value : "")); + olive::MultiUndoCommand *command = new olive::MultiUndoCommand(); + olive::Node::set_value_at_time( + olive::NodeInput(node, QString::fromUtf8(input_id), element), time, v, + 0, command, true); + push_or_run(command, QStringLiteral("Set Input Value")); + return OAKENGINE_OK; +} + +int oakengine_node_array_insert_at(OakEngineNode *self, const char *input_id, + int index) +{ + set_error(QString()); + olive::Node *node = impl(self); + if (checked_input(node, input_id) == olive::NodeValue::k_none || + index < 0) { + set_error(self && input_id && index >= 0 ? + QStringLiteral("unknown input id \"%1\"") + .arg(QString::fromUtf8(input_id)) : + QStringLiteral("invalid arguments")); + return self && input_id && index >= 0 ? OAKENGINE_E_NOT_FOUND : + OAKENGINE_E_INVALID; + } + push_or_run(new olive::NodeArrayInsertCommand( + node, QString::fromUtf8(input_id), index), + QStringLiteral("Insert Array Element")); + return OAKENGINE_OK; +} + +int oakengine_node_array_remove_at(OakEngineNode *self, const char *input_id, + int index) +{ + set_error(QString()); + olive::Node *node = impl(self); + if (checked_input(node, input_id) == olive::NodeValue::k_none || + index < 0) { + set_error(self && input_id && index >= 0 ? + QStringLiteral("unknown input id \"%1\"") + .arg(QString::fromUtf8(input_id)) : + QStringLiteral("invalid arguments")); + return self && input_id && index >= 0 ? OAKENGINE_E_NOT_FOUND : + OAKENGINE_E_INVALID; + } + const QString id = QString::fromUtf8(input_id); + const int size = olive::NodeInput(node, id).get_array_size(); + if (index >= size) { + set_error(QStringLiteral("array index %1 out of range (size %2)") + .arg(index) + .arg(size)); + return OAKENGINE_E_NOT_FOUND; + } + push_or_run(new olive::NodeArrayRemoveCommand(node, id, index), + QStringLiteral("Remove Array Element")); + return OAKENGINE_OK; +} + + OakEngineNode *oakengine_project_add_node(OakEngineProject *project, const char *type_id) { @@ -739,6 +954,12 @@ int oakengine_node_connect(OakEngineNode *output_node, } int oakengine_node_disconnect(OakEngineNode *input_node, const char *input_id) +{ + return oakengine_node_disconnect_ex(input_node, input_id, -1); +} + +int oakengine_node_disconnect_ex(OakEngineNode *input_node, + const char *input_id, int element) { set_error(QString()); olive::Node *in_node = impl(input_node); @@ -751,14 +972,13 @@ int oakengine_node_disconnect(OakEngineNode *input_node, const char *input_id) set_error(QStringLiteral("unknown input id \"%1\"").arg(id)); return OAKENGINE_E_NOT_FOUND; } - olive::Node *connected = - in_node->get_connected_output(olive::NodeInput(in_node, id)); + const olive::NodeInput input(in_node, id, element); + olive::Node *connected = in_node->get_connected_output(input); if (!connected) { set_error(QStringLiteral("input \"%1\" is not connected").arg(id)); return OAKENGINE_E_NOT_FOUND; } - push_or_run(new olive::NodeEdgeRemoveCommand( - connected, olive::NodeInput(in_node, id)), + push_or_run(new olive::NodeEdgeRemoveCommand(connected, input), QStringLiteral("Disconnect Nodes")); return OAKENGINE_OK; } @@ -982,6 +1202,52 @@ int oakengine_node_keyframe_set_easing(OakEngineNode *self, return OAKENGINE_OK; } +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) +{ + 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 || 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); + + // Resolve every keyframe first so a bad address fails without side + // effects (same batch semantics as the view's context-menu action). + 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 = + node->get_keyframe_at_time_on_track(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 KeyframeSetTypeCommand(key, to_engine_easing(type))); + } + push_or_run(command, QStringLiteral("Set Keyframe Type")); + return count; +} + 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 7e06aba1b..d28bdb809 100644 --- a/engine/tests/oakengine_keyframe_test.cpp +++ b/engine/tests/oakengine_keyframe_test.cpp @@ -262,6 +262,162 @@ static void test_rational_and_color(OakEngineProject *project, assert(oakengine_node_keyframes_clear(solid, "color_in") == OAKENGINE_OK); } +static void test_panel_paths(OakEngineProject *project, + OakEngineNode *opacity, OakEngineNode *solid) +{ + oak_node_value out; + + // Frame time base (1001/30000 with the default sequence). + int tbn = 0, tbd = 0; + assert(oakengine_node_frame_time_base(opacity, &tbn, &tbd) == + OAKENGINE_OK); + assert(tbn > 0 && tbd > 0); + assert(oakengine_node_frame_time_base(NULL, &tbn, &tbd) == + OAKENGINE_E_INVALID); + + // set_input_at_time on a NON-keyframed input (fresh node) sets the + // standard value; undo restores it. + OakEngineNode *op2 = oakengine_project_add_node( + project, "org.olivevideoeditor.Olive.opacity"); + assert(op2 != NULL); + oak_node_value v = float_value(0.75); + assert(oakengine_node_set_input_at_time(op2, "opacity_in", -1, 10, 0, &v, + 1) == OAKENGINE_OK); + assert(oakengine_node_get_input(op2, "opacity_in", &out) == OAKENGINE_OK); + assert(fabs(out.f[0] - 0.75) < 1e-9); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + assert(oakengine_node_get_input(op2, "opacity_in", &out) == OAKENGINE_OK); + assert(fabs(out.f[0] - 0.75) > 1e-9); + + // Component type must match the declared type; bad track/id rejected. + oak_node_value wrong; + memset(&wrong, 0, sizeof(wrong)); + wrong.type = OAK_NODE_VALUE_INT; + assert(oakengine_node_set_input_at_time(op2, "opacity_in", -1, 0, 0, + &wrong, + 1) == OAKENGINE_E_INVALID); + assert(oakengine_node_set_input_at_time(op2, "opacity_in", -1, 0, 1, &v, + 1) == OAKENGINE_E_INVALID); + assert(oakengine_node_set_input_at_time(op2, "nope", -1, 0, 0, &v, + 1) == OAKENGINE_E_NOT_FOUND); + assert(oakengine_node_set_input_at_time(NULL, "opacity_in", -1, 0, 0, &v, + 1) == OAKENGINE_E_INVALID); + + // `opacity` is keyframed at this point (keys at 0/15/30 left by the + // earlier tests): the call writes a KEYFRAME at the time + // (set_value_at_time semantics) -- insert first, then update in place. + v = float_value(0.9); + assert(oakengine_node_set_input_at_time(opacity, "opacity_in", -1, 20, 0, + &v, 1) == OAKENGINE_OK); + assert(oakengine_node_keyframe_count(opacity, "opacity_in") == 4); + int64_t ts = -1; + assert(oakengine_node_keyframe_at(opacity, "opacity_in", 2, &ts, &out) == + OAKENGINE_OK); + assert(ts == 20 && fabs(out.f[0] - 0.9) < 1e-9); + v = float_value(0.8); + assert(oakengine_node_set_input_at_time(opacity, "opacity_in", -1, 20, 0, + &v, 1) == OAKENGINE_OK); + assert(oakengine_node_keyframe_count(opacity, "opacity_in") == 4); + assert(oakengine_node_keyframe_at(opacity, "opacity_in", 2, &ts, &out) == + OAKENGINE_OK); + assert(fabs(out.f[0] - 0.8) < 1e-9); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + assert(oakengine_node_keyframe_count(opacity, "opacity_in") == 3); + + // track -1 writes all color components in one command (fresh node, so + // the standard value itself is written and readable back). + OakEngineNode *solid2 = oakengine_project_add_node( + project, "org.olivevideoeditor.Olive.solidgenerator"); + assert(solid2 != NULL); + oak_node_value c; + memset(&c, 0, sizeof(c)); + c.type = OAK_NODE_VALUE_COLOR; + c.f[0] = 0.1; + c.f[1] = 0.2; + c.f[2] = 0.3; + c.f[3] = 0.4; + assert(oakengine_node_set_input_at_time(solid2, "color_in", -1, 5, -1, &c, + 0) == OAKENGINE_OK); + assert(oakengine_node_get_input(solid2, "color_in", &out) == + OAKENGINE_OK); + assert(fabs(out.f[0] - 0.1) < 1e-6 && fabs(out.f[1] - 0.2) < 1e-6 && + fabs(out.f[2] - 0.3) < 1e-6 && fabs(out.f[3] - 0.4) < 1e-6); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + + // String-at-time on the text node; the POD path rejects strings and + // vice versa. + OakEngineNode *text = oakengine_project_add_node( + project, "org.olivevideoeditor.Olive.text3"); + assert(text != NULL); + assert(oakengine_node_set_input_string_at_time(text, "text_in", -1, 0, + "hello") == OAKENGINE_OK); + assert(oakengine_node_set_input_string_at_time(text, "nope", -1, 0, + "x") == OAKENGINE_E_NOT_FOUND); + assert(oakengine_node_set_input_string_at_time(op2, "opacity_in", -1, 0, + "x") == OAKENGINE_E_INVALID); + assert(oakengine_node_set_input_at_time(text, "text_in", -1, 0, 0, &v, + 1) == OAKENGINE_E_INVALID); + + // Array insert/remove on the text node's args_in array. + assert(oakengine_node_array_insert_at(text, "args_in", 0) == + OAKENGINE_OK); + assert(oakengine_node_array_insert_at(text, "args_in", 1) == + OAKENGINE_OK); + assert(oakengine_node_array_remove_at(text, "args_in", 1) == + OAKENGINE_OK); + assert(oakengine_node_array_remove_at(text, "args_in", 5) == + OAKENGINE_E_NOT_FOUND); + assert(oakengine_node_array_remove_at(text, "args_in", 0) == + OAKENGINE_OK); + assert(oakengine_node_array_insert_at(text, "args_in", -1) == + OAKENGINE_E_INVALID); + assert(oakengine_node_array_remove_at(NULL, "args_in", 0) == + OAKENGINE_E_INVALID); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + assert(oakengine_project_undo(project) == OAKENGINE_OK); + + // keyframes_set_type_many: keys at 0 and 15 to hold in one command. + const int64_t times[2] = { 0, 15 }; + const int tracks[2] = { 0, 0 }; + assert(oakengine_node_keyframes_set_type_many(opacity, "opacity_in", -1, + times, tracks, 2, 2) == 2); + int type = -1; + assert(oakengine_node_keyframe_get_easing(opacity, "opacity_in", 0, NULL, + NULL, NULL, NULL, + &type) == OAKENGINE_OK); + assert(type == 2); + assert(oakengine_node_keyframe_get_easing(opacity, "opacity_in", 1, NULL, + NULL, NULL, NULL, + &type) == OAKENGINE_OK); + assert(type == 2); + // One undo restores each key's own previous easing (hold at 0, bezier + // at 15 -- left over from the earlier tests). + assert(oakengine_project_undo(project) == OAKENGINE_OK); + assert(oakengine_node_keyframe_get_easing(opacity, "opacity_in", 0, NULL, + NULL, NULL, NULL, + &type) == OAKENGINE_OK); + assert(type == 2); + assert(oakengine_node_keyframe_get_easing(opacity, "opacity_in", 1, NULL, + NULL, NULL, NULL, + &type) == OAKENGINE_OK); + assert(type == 1); + + // A bad address fails without side effects; zero count is a no-op. + const int64_t bad[1] = { 99 }; + assert(oakengine_node_keyframes_set_type_many(opacity, "opacity_in", -1, + bad, tracks, 1, + 1) == OAKENGINE_E_NOT_FOUND); + assert(oakengine_node_keyframes_set_type_many(opacity, "opacity_in", -1, + times, tracks, 0, 1) == 0); + assert(oakengine_node_keyframes_set_type_many(NULL, "opacity_in", -1, + times, tracks, 1, + 1) == OAKENGINE_E_INVALID); + assert(oakengine_node_keyframe_count(opacity, "opacity_in") == 3); +} + int main(void) { make_tmpdir(); @@ -295,6 +451,7 @@ int main(void) test_float_lifecycle(project, opacity); test_easing_and_remove(project, opacity); test_rational_and_color(project, timeremap, solid); + test_panel_paths(project, opacity, solid); oakengine_project_free(project); assert(oakengine_shutdown() == OAKENGINE_OK);