Files
oak-editor/engine/src/capi/node.cpp
T
Mike-Solar e9f173916f engine: complete C ABI facade (liboakengine oakengine_* surface)
The full pure-C facade used by the app: node/project/timeline/viewer/
undo/task/events/serializer/playback/preview/renderer/gizmo/color/
audio/footage/proxy/encoding/exporter/config/disk/ipc/plugin/worker
families, plus undo-group semantics, display renderer handles,
NodeFactory accessors, and per-family pure-C engine tests.
2026-07-26 22:43:00 +08:00

4571 lines
138 KiB
C++

/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
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 <http://www.gnu.org/licenses/>.
***/
#include "oakengine/node.h"
#include <cstdio>
#include <cstring>
#include <QByteArray>
#include <QString>
#include <QVariant>
#include <QVector2D>
#include <QVector3D>
#include <QVector4D>
#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 "node/group/group.h"
#include "node/input/multicam/multicamnode.h"
#include "node/audio/volume/volume.h"
#include "node/distort/transform/transformdistortnode.h"
#include "node/block/transition/transition.h"
#include "node/block/subtitle/subtitle.h"
#include "node/generator/shape/shapenodebase.h"
#include "audio/audiovisualwaveform.h"
#include "node/inputimmediate.h"
#include "undo/undocommand.h"
#include "undo/undostack.h"
#include "undointernal.h"
namespace
{
// Last node error per thread.
thread_local QString g_last_error;
void set_error(const QString &error)
{
g_last_error = error;
}
olive::Node *impl(OakEngineNode *h)
{
return reinterpret_cast<olive::Node *>(h);
}
const olive::Node *impl(const OakEngineNode *h)
{
return reinterpret_cast<const olive::Node *>(h);
}
OakEngineNode *wrap(olive::Node *n)
{
return reinterpret_cast<OakEngineNode *>(n);
}
olive::Project *impl(OakEngineProject *h)
{
return reinterpret_cast<olive::Project *>(h);
}
const olive::Project *impl(const OakEngineProject *h)
{
return reinterpret_cast<const olive::Project *>(h);
}
// buf/size convention: returns the would-be length excluding the NUL.
int string_to_buf(const QString &s, char *buf, int buf_size)
{
const QByteArray utf = s.toUtf8();
if (buf && buf_size > 0) {
snprintf(buf, size_t(buf_size), "%s", utf.constData());
}
return int(utf.size());
}
// Push an undoable command onto the global undo stack when the engine is
// initialized, otherwise execute it directly.
void push_or_run(olive::UndoCommand *command, const QString &name)
{
oakengine_undo_push_or_run(command, name);
}
// NodeValue::Type -> facade value type; types without a POD representation
// map to OAK_NODE_VALUE_NONE.
oak_node_value_type to_c_type(olive::NodeValue::Type t)
{
switch (t) {
case olive::NodeValue::k_int:
return OAK_NODE_VALUE_INT;
case olive::NodeValue::k_float:
return OAK_NODE_VALUE_FLOAT;
case olive::NodeValue::k_boolean:
return OAK_NODE_VALUE_BOOL;
case olive::NodeValue::k_rational:
return OAK_NODE_VALUE_RATIONAL;
case olive::NodeValue::k_color:
return OAK_NODE_VALUE_COLOR;
case olive::NodeValue::k_vec2:
return OAK_NODE_VALUE_VEC2;
case olive::NodeValue::k_vec3:
return OAK_NODE_VALUE_VEC3;
case olive::NodeValue::k_vec4:
return OAK_NODE_VALUE_VEC4;
case olive::NodeValue::k_combo:
return OAK_NODE_VALUE_COMBO;
case olive::NodeValue::k_file:
return OAK_NODE_VALUE_STRING;
case olive::NodeValue::k_text:
return OAK_NODE_VALUE_TEXT;
case olive::NodeValue::k_font:
return OAK_NODE_VALUE_FONT;
case olive::NodeValue::k_str_combo:
return OAK_NODE_VALUE_STR_COMBO;
case olive::NodeValue::k_binary:
return OAK_NODE_VALUE_BINARY;
case olive::NodeValue::k_bezier:
return OAK_NODE_VALUE_BEZIER;
case olive::NodeValue::k_texture:
return OAK_NODE_VALUE_TEXTURE;
case olive::NodeValue::k_samples:
return OAK_NODE_VALUE_SAMPLES;
case olive::NodeValue::k_video_params:
return OAK_NODE_VALUE_VIDEO_PARAMS;
case olive::NodeValue::k_audio_params:
return OAK_NODE_VALUE_AUDIO_PARAMS;
default:
return OAK_NODE_VALUE_NONE;
}
}
// Convert a raw QVariant (not wrapped in NodeValue) to C POD based on type.
// Used for default values where the QVariant holds the native type directly.
static bool qvariant_to_pod(olive::NodeValue::Type type, const QVariant &qv,
oak_node_value *out)
{
switch (type) {
case olive::NodeValue::k_int:
case olive::NodeValue::k_combo:
out->num = qv.toLongLong();
return true;
case olive::NodeValue::k_float:
out->f[0] = qv.toDouble();
return true;
case olive::NodeValue::k_boolean:
out->num = qv.toBool() ? 1 : 0;
return true;
case olive::NodeValue::k_rational: {
const olive::Rational r = qv.value<olive::Rational>();
out->num = r.numerator();
out->den = r.denominator();
return true;
}
case olive::NodeValue::k_color: {
const olive::core::Color c = qv.value<olive::core::Color>();
out->f[0] = c.red();
out->f[1] = c.green();
out->f[2] = c.blue();
out->f[3] = c.alpha();
return true;
}
case olive::NodeValue::k_vec2:
if (qv.canConvert<QVector2D>()) {
const QVector2D v2 = qv.value<QVector2D>();
out->f[0] = v2.x();
out->f[1] = v2.y();
return true;
}
return false;
case olive::NodeValue::k_vec3:
if (qv.canConvert<QVector3D>()) {
const QVector3D v3 = qv.value<QVector3D>();
out->f[0] = v3.x();
out->f[1] = v3.y();
out->f[2] = v3.z();
return true;
}
return false;
case olive::NodeValue::k_vec4:
if (qv.canConvert<QVector4D>()) {
const QVector4D v4 = qv.value<QVector4D>();
out->f[0] = v4.x();
out->f[1] = v4.y();
out->f[2] = v4.z();
out->f[3] = v4.w();
return true;
}
return false;
default:
return false;
}
}
// Map an engine standard value into the POD. Returns false when the type
// has no POD representation (including STRING, which uses dedicated APIs).
bool value_to_c(const olive::NodeValue &v, oak_node_value *out)
{
memset(out, 0, sizeof(*out));
out->type = to_c_type(v.type());
switch (v.type()) {
case olive::NodeValue::k_int:
case olive::NodeValue::k_combo:
out->num = v.to_int();
return true;
case olive::NodeValue::k_float:
out->f[0] = v.to_double();
return true;
case olive::NodeValue::k_boolean:
out->num = v.to_bool() ? 1 : 0;
return true;
case olive::NodeValue::k_rational: {
const olive::Rational r = v.to_rational();
out->num = r.numerator();
out->den = r.denominator();
return true;
}
case olive::NodeValue::k_color: {
const olive::Color c = v.to_color();
out->f[0] = c.red();
out->f[1] = c.green();
out->f[2] = c.blue();
out->f[3] = c.alpha();
return true;
}
case olive::NodeValue::k_vec2: {
const QVector2D c = v.to_vec2();
out->f[0] = c.x();
out->f[1] = c.y();
return true;
}
case olive::NodeValue::k_vec3: {
const QVector3D c = v.to_vec3();
out->f[0] = c.x();
out->f[1] = c.y();
out->f[2] = c.z();
return true;
}
case olive::NodeValue::k_vec4: {
const QVector4D c = v.to_vec4();
out->f[0] = c.x();
out->f[1] = c.y();
out->f[2] = c.z();
out->f[3] = c.w();
return true;
}
default:
return false;
}
}
// Map a POD back into an engine QVariant, checking the type against the
// input's declared type. Returns false on a type mismatch.
bool value_from_c(const oak_node_value *v, olive::NodeValue::Type declared,
QVariant *out)
{
if (int(v->type) != int(to_c_type(declared))) {
return false;
}
switch (declared) {
case olive::NodeValue::k_int:
case olive::NodeValue::k_combo:
*out = QVariant::fromValue<qlonglong>(v->num);
return true;
case olive::NodeValue::k_float:
*out = QVariant::fromValue(v->f[0]);
return true;
case olive::NodeValue::k_boolean:
*out = QVariant::fromValue(v->num != 0);
return true;
case olive::NodeValue::k_rational:
*out = QVariant::fromValue(
olive::Rational(int(v->num), int(v->den)));
return true;
case olive::NodeValue::k_color:
*out = QVariant::fromValue(olive::Color(
float(v->f[0]), float(v->f[1]), float(v->f[2]), float(v->f[3])));
return true;
case olive::NodeValue::k_vec2:
*out = QVariant::fromValue(
QVector2D(float(v->f[0]), float(v->f[1])));
return true;
case olive::NodeValue::k_vec3:
*out = QVariant::fromValue(
QVector3D(float(v->f[0]), float(v->f[1]), float(v->f[2])));
return true;
case olive::NodeValue::k_vec4:
*out = QVariant::fromValue(QVector4D(
float(v->f[0]), float(v->f[1]), float(v->f[2]), float(v->f[3])));
return true;
default:
return false;
}
}
// Validate self + input id and return the declared type; reports the error.
olive::NodeValue::Type checked_input(const olive::Node *self,
const char *input_id)
{
if (!self || !input_id) {
return olive::NodeValue::k_none;
}
const QString id = QString::fromUtf8(input_id);
if (!self->inputs().contains(id)) {
return olive::NodeValue::k_none;
}
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 (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,
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)
{
}
// 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;
}
protected:
virtual void redo() override
{
if (!has_old_) {
old_point_ = key_->bezier_control(mode_);
has_old_ = true;
}
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_;
bool has_old_ = false;
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;
}
// 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<olive::NodeKeyframeTrack> &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"
{
int oakengine_node_last_error(char *buf, int buf_size)
{
return string_to_buf(g_last_error, buf, buf_size);
}
int oakengine_project_node_count(const OakEngineProject *self)
{
return self ? impl(self)->nodes().size() : 0;
}
OakEngineNode *oakengine_project_node_at(const OakEngineProject *self,
int index)
{
if (!self || index < 0 || index >= impl(self)->nodes().size()) {
return nullptr;
}
return wrap(impl(self)->nodes().at(index));
}
int oakengine_node_factory_id_count(void)
{
return olive::NodeFactory::get_library().size();
}
OakEngineNode *oakengine_node_factory_create_from_id(const char *type_id)
{
if (!type_id) {
return nullptr;
}
return wrap(olive::NodeFactory::create_from_id(QString::fromUtf8(type_id)));
}
int oakengine_node_factory_name_from_id(const char *type_id, char *buf,
int buf_size)
{
if (!type_id) {
if (buf && buf_size > 0) buf[0] = '\0';
return 0;
}
const QString name = olive::NodeFactory::get_name_from_id(
QString::fromUtf8(type_id));
return string_to_buf(name, buf, buf_size);
}
OakEngineNode *oakengine_node_factory_node_at(int index)
{
const QList<olive::Node *> &lib = olive::NodeFactory::get_library();
if (index < 0 || index >= lib.size()) {
return nullptr;
}
return wrap(lib.at(index));
}
int oakengine_node_get_type_id(const OakEngineNode *self, char *buf,
int buf_size)
{
if (!self) {
return OAKENGINE_E_INVALID;
}
return string_to_buf(impl(self)->id(), buf, buf_size);
}
int oakengine_node_get_name(const OakEngineNode *self, char *buf,
int buf_size)
{
if (!self) {
return OAKENGINE_E_INVALID;
}
return string_to_buf(impl(self)->name(), buf, buf_size);
}
int oakengine_node_get_label(const OakEngineNode *self, char *buf,
int buf_size)
{
if (!self) {
return OAKENGINE_E_INVALID;
}
return string_to_buf(impl(self)->get_label(), buf, buf_size);
}
int oakengine_node_set_label(OakEngineNode *self, const char *label)
{
return oakengine_node_set_label_ex(self, label, 1);
}
int oakengine_node_set_label_ex(OakEngineNode *self, const char *label,
int undoable)
{
set_error(QString());
if (!self) {
set_error(QStringLiteral("invalid node"));
return OAKENGINE_E_INVALID;
}
olive::UndoCommand *command = new olive::NodeRenameCommand(
impl(self), QString::fromUtf8(label ? label : ""));
if (undoable) {
push_or_run(command, QStringLiteral("Rename Node"));
} else {
command->redo_now();
delete command;
}
return OAKENGINE_OK;
}
int oakengine_node_rename_many(OakEngineNode **nodes, int count,
const char *label,
void *parent_multi_or_NULL)
{
set_error(QString());
if (count < 0 || (count > 0 && !nodes)) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
if (count == 0) {
return OAKENGINE_OK;
}
// One multi-node rename command, like Core::label_nodes().
auto *command = new olive::NodeRenameCommand();
const QString text = QString::fromUtf8(label ? label : "");
for (int i = 0; i < count; i++) {
if (!nodes[i]) {
set_error(QStringLiteral("invalid node at index %1").arg(i));
delete command;
return OAKENGINE_E_INVALID;
}
command->add_node(impl(nodes[i]), text);
}
if (parent_multi_or_NULL) {
static_cast<olive::MultiUndoCommand *>(parent_multi_or_NULL)->add_child(
command);
} else {
push_or_run(command, QStringLiteral("Rename Nodes"));
}
return OAKENGINE_OK;
}
extern "C" void *oakengine_node_rename_command(OakEngineNode *node,
const char *label)
{
if (!node) {
return nullptr;
}
return new olive::NodeRenameCommand(impl(node),
QString::fromUtf8(label ? label : ""));
}
int oakengine_node_set_label_many(OakEngineNode **nodes, int count,
const char *label)
{
return oakengine_node_rename_many(nodes, count, label, nullptr);
}
int oakengine_node_set_color_label(OakEngineNode **nodes, int count,
int color_index)
{
set_error(QString());
if (count < 0 || (count > 0 && !nodes)) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
if (count == 0) {
return OAKENGINE_OK;
}
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
for (int i = 0; i < count; i++) {
if (!nodes[i]) {
set_error(QStringLiteral("invalid node at index %1").arg(i));
delete command;
return OAKENGINE_E_INVALID;
}
command->add_child(
new olive::NodeOverrideColorCommand(impl(nodes[i]),
color_index));
}
push_or_run(command, QStringLiteral("Set Node Color Labels"));
return OAKENGINE_OK;
}
extern "C" void *oakengine_node_set_color_label_command(OakEngineNode *node,
int color_index)
{
olive::Node *n = impl(node);
if (!n) {
return nullptr;
}
return new olive::NodeOverrideColorCommand(n, color_index);
}
int oakengine_node_get_color_label(const OakEngineNode *self)
{
if (!self) {
return -1;
}
return impl(self)->get_override_color();
}
int oakengine_node_input_count(const OakEngineNode *self)
{
return self ? impl(self)->inputs().size() : 0;
}
int oakengine_node_input_id(const OakEngineNode *self, int index, char *buf,
int buf_size)
{
if (!self) {
return OAKENGINE_E_INVALID;
}
const QVector<QString> &ids = impl(self)->inputs();
if (index < 0 || index >= ids.size()) {
return OAKENGINE_E_NOT_FOUND;
}
return string_to_buf(ids.at(index), buf, buf_size);
}
int oakengine_node_input_get_type(const OakEngineNode *self,
const char *input_id)
{
if (!self || !input_id) {
return OAK_NODE_VALUE_NONE;
}
return to_c_type(checked_input(impl(self), input_id));
}
int oakengine_node_input_is_connected(const OakEngineNode *self,
const char *input_id)
{
if (!self || !input_id) {
return 0;
}
return impl(self)->is_input_connected(QString::fromUtf8(input_id)) ? 1 :
0;
}
int oakengine_node_get_input(const OakEngineNode *self, const char *input_id,
oak_node_value *out)
{
set_error(QString());
if (!self || !input_id || !out) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
const olive::Node *node = impl(self);
const QString id = QString::fromUtf8(input_id);
if (!node->inputs().contains(id)) {
set_error(QStringLiteral("unknown input id \"%1\"").arg(id));
return OAKENGINE_E_NOT_FOUND;
}
const olive::NodeValue::Type type = node->get_input_data_type(id);
if (type == olive::NodeValue::k_file) {
set_error(QStringLiteral(
"\"%1\" is a string input; use oakengine_node_get_input_string()")
.arg(id));
return OAKENGINE_E_INVALID;
}
const olive::NodeValue v(type, node->get_standard_value(id));
if (!value_to_c(v, out)) {
set_error(QStringLiteral(
"input \"%1\" has no POD value representation")
.arg(id));
return OAKENGINE_E_NOT_FOUND;
}
return OAKENGINE_OK;
}
int oakengine_node_set_input(OakEngineNode *self, const char *input_id,
const oak_node_value *v)
{
set_error(QString());
if (!self || !input_id || !v) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
olive::Node *node = impl(self);
const QString id = QString::fromUtf8(input_id);
if (!node->inputs().contains(id)) {
set_error(QStringLiteral("unknown input id \"%1\"").arg(id));
return OAKENGINE_E_NOT_FOUND;
}
const olive::NodeValue::Type type = node->get_input_data_type(id);
if (type == olive::NodeValue::k_file) {
set_error(QStringLiteral(
"\"%1\" is a string input; use oakengine_node_set_input_string()")
.arg(id));
return OAKENGINE_E_INVALID;
}
QVariant value;
if (!value_from_c(v, type, &value)) {
set_error(QStringLiteral("value type %1 does not match the declared "
"type of \"%2\"")
.arg(v->type)
.arg(id));
return OAKENGINE_E_INVALID;
}
// The undo stack's set-value commands work on split (per-component
// track) values; split_normal_value_into_track_values() is the same
// conversion Node::set_standard_value() performs.
push_or_run(new olive::NodeParamSetSplitStandardValueCommand(
olive::NodeInput(node, id),
olive::NodeValue::split_normal_value_into_track_values(
type, value)),
QStringLiteral("Set Node Value"));
return OAKENGINE_OK;
}
int oakengine_node_get_input_string(const OakEngineNode *self,
const char *input_id, char *buf,
int buf_size)
{
set_error(QString());
if (!self || !input_id) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
const olive::Node *node = impl(self);
const QString id = QString::fromUtf8(input_id);
if (!node->inputs().contains(id)) {
set_error(QStringLiteral("unknown input id \"%1\"").arg(id));
return OAKENGINE_E_NOT_FOUND;
}
if (node->get_input_data_type(id) != olive::NodeValue::k_file) {
set_error(QStringLiteral("\"%1\" is not a string input").arg(id));
return OAKENGINE_E_INVALID;
}
return string_to_buf(node->get_standard_value(id).toString(), buf,
buf_size);
}
int oakengine_node_set_input_string(OakEngineNode *self,
const char *input_id, const char *s)
{
set_error(QString());
if (!self || !input_id) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
olive::Node *node = impl(self);
const QString id = QString::fromUtf8(input_id);
if (!node->inputs().contains(id)) {
set_error(QStringLiteral("unknown input id \"%1\"").arg(id));
return OAKENGINE_E_NOT_FOUND;
}
if (node->get_input_data_type(id) != olive::NodeValue::k_file) {
set_error(QStringLiteral("\"%1\" is not a string input").arg(id));
return OAKENGINE_E_INVALID;
}
const QVariant value = QVariant::fromValue(QString::fromUtf8(s ? s : ""));
push_or_run(new olive::NodeParamSetSplitStandardValueCommand(
olive::NodeInput(node, id),
olive::NodeValue::split_normal_value_into_track_values(
olive::NodeValue::k_file, value)),
QStringLiteral("Set Node Value"));
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<qlonglong>(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;
}
}
extern "C" void *oakengine_node_set_standard_value_command(
OakEngineNode *self, const char *input_id, int element, int track,
const oak_node_value *v)
{
if (!self || !input_id || !v) {
return nullptr;
}
olive::Node *node = impl(self);
const QString id = QString::fromUtf8(input_id);
if (!node->inputs().contains(id)) {
return nullptr;
}
const olive::NodeValue::Type declared = node->get_input_data_type(id);
if (to_c_type(declared) == OAK_NODE_VALUE_NONE) {
return nullptr;
}
QVariant value;
if (track < 0) {
// Track -1 writes the whole single-track value.
if (!value_from_c(v, declared, &value)) {
return nullptr;
}
} else {
// Per-track component (the command stores the value on one track).
if (!component_from_c(v, declared, 0, &value)) {
return nullptr;
}
}
return new olive::NodeParamSetStandardValueCommand(
olive::NodeKeyframeTrackReference(olive::NodeInput(node, id, element),
track),
value);
}
extern "C" void *oakengine_node_set_input_video_params_command(
OakEngineNode *self, const char *input_id, const oak_video_params *params)
{
if (!self || !input_id || !params) {
return nullptr;
}
olive::Node *node = impl(self);
const QString id = QString::fromUtf8(input_id);
if (!node->inputs().contains(id)) {
return nullptr;
}
if (node->get_input_data_type(id) != olive::NodeValue::k_video_params) {
return nullptr;
}
const olive::VideoParams params_cpp(
params->width, params->height, olive::Rational(params->time_base_num,
params->time_base_den),
static_cast<olive::PixelFormat::Format>(params->format),
olive::VideoParams::k_internal_channel_count,
olive::Rational(params->pixel_aspect_num, params->pixel_aspect_den),
static_cast<olive::VideoParams::Interlacing>(params->interlacing),
params->divider);
return new olive::NodeParamSetStandardValueCommand(
olive::NodeKeyframeTrackReference(olive::NodeInput(node, id)),
QVariant::fromValue(params_cpp));
}
extern "C" void *oakengine_node_set_value_at_time_command(
void *node, const char *input, int element, int64_t time_num,
int64_t time_den, const oak_node_value *value, int track,
int insert_on_all_tracks_if_no_key)
{
if (!node || !input || !value || time_den == 0) {
return nullptr;
}
olive::Node *n = reinterpret_cast<olive::Node *>(node);
const QString id = QString::fromUtf8(input);
if (!n->inputs().contains(id)) {
return nullptr;
}
const olive::NodeValue::Type declared = n->get_input_data_type(id);
const int nb_tracks =
olive::NodeValue::get_number_of_keyframe_tracks(declared);
if (track < -1 || track >= nb_tracks || nb_tracks == 0) {
return nullptr;
}
if (declared == olive::NodeValue::k_file ||
declared == olive::NodeValue::k_text ||
declared == olive::NodeValue::k_font ||
declared == olive::NodeValue::k_str_combo) {
return nullptr;
}
const olive::Rational time(time_num, time_den);
const olive::NodeInput node_input(n, id, element);
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
if (track == -1) {
for (int i = 0; i < nb_tracks; i++) {
QVariant component;
if (!component_from_c(value, declared, i, &component)) {
delete command;
return nullptr;
}
olive::Node::set_value_at_time(node_input, time, component, i,
command, false);
}
} else {
QVariant component;
if (!component_from_c(value, declared, 0, &component)) {
delete command;
return nullptr;
}
olive::Node::set_value_at_time(
node_input, time, component, track, command,
insert_on_all_tracks_if_no_key != 0);
}
return command;
}
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;
}
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)
{
set_error(QString());
olive::Project *p = reinterpret_cast<olive::Project *>(project);
if (!p || !type_id) {
set_error(QStringLiteral("invalid project or type id"));
return nullptr;
}
const QString id = QString::fromUtf8(type_id);
olive::Node *node = olive::NodeFactory::create_from_id(id);
if (!node) {
set_error(QStringLiteral("unknown node type id \"%1\"").arg(id));
return nullptr;
}
push_or_run(new olive::NodeAddCommand(p, node),
QStringLiteral("Add Node"));
return wrap(node);
}
int oakengine_project_remove_node(OakEngineProject *project,
OakEngineNode *node)
{
set_error(QString());
olive::Project *p = reinterpret_cast<olive::Project *>(project);
olive::Node *n = impl(node);
if (!p || !n) {
set_error(QStringLiteral("invalid project or node"));
return OAKENGINE_E_INVALID;
}
if (olive::Project::get_project_from_object(n) != p) {
set_error(QStringLiteral("node does not belong to this project"));
return OAKENGINE_E_INVALID;
}
push_or_run(new olive::NodeRemoveAndDisconnectCommand(n),
QStringLiteral("Remove Node"));
return OAKENGINE_OK;
}
int oakengine_node_connect(OakEngineNode *output_node,
OakEngineNode *input_node, const char *input_id)
{
set_error(QString());
olive::Node *out_node = impl(output_node);
olive::Node *in_node = impl(input_node);
if (!out_node || !in_node || !input_id) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
const QString id = QString::fromUtf8(input_id);
if (!in_node->inputs().contains(id)) {
set_error(QStringLiteral("unknown input id \"%1\"").arg(id));
return OAKENGINE_E_NOT_FOUND;
}
if (!in_node->is_input_connectable(id)) {
set_error(QStringLiteral("input \"%1\" is not connectable").arg(id));
return OAKENGINE_E_INVALID;
}
if (in_node->is_input_connected(id)) {
set_error(QStringLiteral(
"input \"%1\" is already connected; disconnect first")
.arg(id));
return OAKENGINE_E_STATE;
}
push_or_run(new olive::NodeEdgeAddCommand(
out_node, olive::NodeInput(in_node, id)),
QStringLiteral("Connect Nodes"));
return OAKENGINE_OK;
}
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);
if (!in_node || !input_id) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
const QString id = QString::fromUtf8(input_id);
if (!in_node->inputs().contains(id)) {
set_error(QStringLiteral("unknown input id \"%1\"").arg(id));
return OAKENGINE_E_NOT_FOUND;
}
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, input),
QStringLiteral("Disconnect Nodes"));
return OAKENGINE_OK;
}
extern "C" void *oakengine_node_connect_command(OakEngineNode *output_node,
OakEngineNode *input_node,
const char *input_id,
int element)
{
olive::Node *out_node = impl(output_node);
olive::Node *in_node = impl(input_node);
if (!out_node || !in_node || !input_id) {
return nullptr;
}
const QString id = QString::fromUtf8(input_id);
if (!in_node->inputs().contains(id)) {
return nullptr;
}
return new olive::NodeEdgeAddCommand(
out_node, olive::NodeInput(in_node, id, element));
}
extern "C" void *oakengine_node_disconnect_command(OakEngineNode *input_node,
const char *input_id,
int element)
{
olive::Node *in_node = impl(input_node);
if (!in_node || !input_id) {
return nullptr;
}
const QString id = QString::fromUtf8(input_id);
if (!in_node->inputs().contains(id)) {
return nullptr;
}
const olive::NodeInput input(in_node, id, element);
olive::Node *connected = in_node->get_connected_output(input);
if (!connected) {
return nullptr;
}
return new olive::NodeEdgeRemoveCommand(connected, input);
}
extern "C" int oakengine_block_link(void *a, void *b, int linked)
{
if (!a || !b) {
return OAKENGINE_E_INVALID;
}
olive::Node *na = reinterpret_cast<olive::Node *>(a);
olive::Node *nb = reinterpret_cast<olive::Node *>(b);
const bool ok = linked ? olive::Node::link(na, nb) :
olive::Node::unlink(na, nb);
return ok ? 1 : 0;
}
extern "C" void *oakengine_node_add_to_project_command(OakEngineProject *project,
OakEngineNode *node)
{
olive::Project *p = impl(project);
olive::Node *n = impl(node);
if (!p || !n) {
return nullptr;
}
return new olive::NodeAddCommand(p, n);
}
/* ---- 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;
}
extern "C" void *oakengine_node_insert_keyframe_command(
OakEngineNode *self, const char *input_id, int element, int track,
int64_t time_ts, const oak_node_value *value, int type, float x1, float y1,
float x2, float y2)
{
if (!self || !input_id || !value || type < 0 || type > 2) {
return nullptr;
}
olive::Node *node = impl(self);
const olive::NodeValue::Type declared =
checked_keyframe_input(node, input_id);
if (declared == olive::NodeValue::k_none) {
return nullptr;
}
QVariant normal;
if (!value_from_c(value, declared, &normal)) {
return nullptr;
}
const olive::SplitValue split =
olive::NodeValue::split_normal_value_into_track_values(declared,
normal);
if (track < 0 || track >= split.size()) {
return nullptr;
}
const QString id = QString::fromUtf8(input_id);
const olive::Rational time = olive::core::Timecode::timestamp_to_time(
time_ts, project_time_base(node));
auto *key = new olive::NodeKeyframe(time, split.at(track),
to_engine_easing(type), track, element,
id);
if (type == 1) {
key->set_bezier_control_in(QPointF(x1, y1));
key->set_bezier_control_out(QPointF(x2, y2));
}
return new olive::NodeParamInsertKeyframeCommand(node, key);
}
extern "C" void *oakengine_node_remove_keyframe_command(
OakEngineKeyframe *keyframe)
{
auto *key = reinterpret_cast<olive::NodeKeyframe *>(keyframe);
if (!key) {
return nullptr;
}
return new olive::NodeParamRemoveKeyframeCommand(key);
}
extern "C" void *oakengine_keyframe_set_time_command(
OakEngineKeyframe *keyframe, int64_t new_time_ts)
{
auto *key = reinterpret_cast<olive::NodeKeyframe *>(keyframe);
if (!key || !key->parent()) {
return nullptr;
}
const olive::Rational tb = project_time_base(key->parent());
const olive::Rational new_time =
olive::core::Timecode::timestamp_to_time(new_time_ts, tb);
return new olive::NodeParamSetKeyframeTimeCommand(key, new_time);
}
extern "C" void *oakengine_keyframe_set_value_command(
OakEngineKeyframe *keyframe, const oak_node_value *value)
{
auto *key = reinterpret_cast<olive::NodeKeyframe *>(keyframe);
if (!key || !value || !key->parent()) {
return nullptr;
}
const olive::NodeValue::Type declared =
key->parent()->get_input_data_type(key->input());
QVariant v;
if (!component_from_c(value, declared, key->track(), &v)) {
return nullptr;
}
return new olive::NodeParamSetKeyframeValueCommand(key, v);
}
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_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 =
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 KeyframeSetTypeCommand(key, to_engine_easing(type)));
}
push_or_run(command, QStringLiteral("Set Keyframe Type"));
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());
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;
}
/* ---- Extended input introspection ----------------------------------------- */
int oakengine_node_input_is_array(const OakEngineNode *self,
const char *input_id)
{
if (!self || !input_id) {
return 0;
}
return impl(self)->input_is_array(QString::fromUtf8(input_id)) ? 1 : 0;
}
int oakengine_node_input_array_size(const OakEngineNode *self,
const char *input_id)
{
if (!self || !input_id) {
return 0;
}
return impl(self)->input_array_size(QString::fromUtf8(input_id));
}
int oakengine_node_input_get_flags(const OakEngineNode *self,
const char *input_id)
{
if (!self || !input_id) {
return 0;
}
return int(impl(self)->get_input_flags(QString::fromUtf8(input_id)));
}
int oakengine_node_input_is_connectable(const OakEngineNode *self,
const char *input_id)
{
if (!self || !input_id) {
return 0;
}
return impl(self)->is_input_connectable(QString::fromUtf8(input_id)) ? 1 : 0;
}
int oakengine_node_input_is_keyframable(const OakEngineNode *self,
const char *input_id)
{
if (!self || !input_id) {
return 0;
}
return impl(self)->is_input_keyframable(QString::fromUtf8(input_id)) ? 1 : 0;
}
int oakengine_node_input_is_keyframed_ex(const OakEngineNode *self,
const char *input_id, int track)
{
if (!self || !input_id) {
return 0;
}
(void)track;
return impl(self)->is_input_keyframing(QString::fromUtf8(input_id)) ? 1 : 0;
}
int oakengine_node_get_label_and_name(const OakEngineNode *self, char *buf,
int buf_size)
{
if (!self) {
return OAKENGINE_E_INVALID;
}
return string_to_buf(impl(self)->get_label_and_name(), buf, buf_size);
}
int oakengine_node_get_input_name(const OakEngineNode *self,
const char *input_id, char *buf,
int buf_size)
{
if (!self || !input_id) {
return OAKENGINE_E_INVALID;
}
const olive::NodeInput input(const_cast<olive::Node *>(impl(self)),
QString::fromUtf8(input_id));
return string_to_buf(input.get_input_name(), buf, buf_size);
}
int oakengine_node_input_get_default_value(const OakEngineNode *self,
const char *input_id, int track,
oak_node_value *out)
{
set_error(QString());
if (!self || !input_id || !out) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
const olive::NodeInput input(const_cast<olive::Node *>(impl(self)),
QString::fromUtf8(input_id));
const olive::NodeValue::Type type = input.get_data_type();
if (type == olive::NodeValue::k_none) {
set_error(QStringLiteral("unknown input id \"%1\"")
.arg(QString::fromUtf8(input_id)));
return OAKENGINE_E_NOT_FOUND;
}
if (track >= 0) {
// Validate track count: inputs may have split tracks (Color→4, Vec2→2, etc.)
// or a single whole-value track (track 0).
int num_tracks = impl(self)->get_number_of_keyframe_tracks(
QString::fromUtf8(input_id));
if (track >= qMax(1, num_tracks)) {
set_error(QStringLiteral("track index out of range"));
return OAKENGINE_E_NOT_FOUND;
}
}
const QVariant def = (track >= 0) ?
input.get_split_default_value_for_track(track) :
input.get_default_value();
// Convert the default QVariant directly to C POD. Bypass NodeValue
// constructor because passing QVariant to the template constructor
// nests it inside another QVariant, breaking value_to_c extraction.
// Also handle split defaults: for k_color the QVariant may be a float
// (single channel) instead of a full Color.
memset(out, 0, sizeof(*out));
out->type = to_c_type(type);
if (type == olive::NodeValue::k_color && def.typeId() == QMetaType::Float) {
out->f[0] = def.toFloat();
out->f[1] = 0.0;
out->f[2] = 0.0;
out->f[3] = 1.0;
} else if (!qvariant_to_pod(type, def, out)) {
set_error(QStringLiteral("default value has no POD representation"));
return OAKENGINE_E_NOT_FOUND;
}
return OAKENGINE_OK;
}
OakEngineProject *oakengine_node_get_project(const OakEngineNode *self)
{
if (!self) {
return nullptr;
}
olive::Project *p = impl(self)->project();
return reinterpret_cast<OakEngineProject *>(p);
}
OakEngineNode *oakengine_node_input_get_connected_node(
const OakEngineNode *self, const char *input_id, int element)
{
if (!self || !input_id) {
return nullptr;
}
olive::Node *conn = const_cast<olive::Node *>(impl(self))
->get_connected_output(QString::fromUtf8(input_id),
element);
return wrap(conn);
}
int oakengine_node_copy_inputs(OakEngineNode *dest, const OakEngineNode *src)
{
set_error(QString());
if (!dest || !src) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
olive::Node::copy_inputs(impl(src), impl(dest), false, command);
push_or_run(command, QStringLiteral("Copy Inputs"));
return OAKENGINE_OK;
}
int oakengine_node_get_input_at_time(const OakEngineNode *self,
const char *input_id, int element,
int track, int64_t time_ts,
int track_for_time, oak_node_value *out)
{
set_error(QString());
// Facade contract: `track` is the 0-based component selector (-1 =
// whole value), time_ts is in SECONDS, track_for_time is the 1-based
// keyframe track selector (accepted for signature compatibility).
(void)track_for_time;
if (!self || !input_id || !out) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
const olive::Node *node = impl(self);
const QString id = QString::fromUtf8(input_id);
if (!node->inputs().contains(id)) {
set_error(QStringLiteral("unknown input id \"%1\"").arg(id));
return OAKENGINE_E_NOT_FOUND;
}
const olive::NodeValue::Type type = node->get_input_data_type(id);
if (type == olive::NodeValue::k_file || type == olive::NodeValue::k_text ||
type == olive::NodeValue::k_font ||
type == olive::NodeValue::k_str_combo) {
set_error(QStringLiteral(
"\"%1\" is a string input; use oakengine_node_get_input_string_at_time()")
.arg(id));
return OAKENGINE_E_INVALID;
}
// Facade contract: time_ts is a frame timestamp in the project's frame
// timebase (like the setter family and the timeline family).
const olive::Rational tb = project_time_base(node);
const olive::Rational time =
olive::core::Timecode::timestamp_to_time(time_ts, tb);
// When a specific track is requested (track >= 0) and element is not
// set (element == -1), use track as the element to get the per-track
// component from the engine's split-value API.
const int eff_element = (track >= 0 && element < 0) ? track : element;
// If we're requesting a single track on a multi-component type, use
// get_split_value_at_time to get the raw component value (float/int).
// Then construct the output POD directly, bypassing NodeValue which
// can't handle a scalar QVariant for a multi-component type.
bool direct_ok = false;
memset(out, 0, sizeof(*out));
out->type = to_c_type(type);
if (track >= 0) {
// Per-component read: the component is the keyframe track with the
// same 0-based index; the element is passed through unchanged (for
// non-array inputs it stays -1, so the keyed path is found).
const QVariant comp =
node->get_split_value_at_time_on_track(id, time, track, element);
if (comp.isValid()) {
// Scalar components are reported in f[0] for float-like types
// (color/vec) and in num for integer-like types (bool/int/
// combo/rational) -- see the at-time readers in
// oakengine_node_test/oakengine_keyframe_test.
switch (type) {
case olive::NodeValue::k_boolean:
case olive::NodeValue::k_int:
case olive::NodeValue::k_combo:
out->num = comp.toLongLong();
break;
case olive::NodeValue::k_rational:
out->num = comp.value<olive::core::Rational>().numerator();
out->den = comp.value<olive::core::Rational>().denominator();
break;
default:
out->f[0] = comp.toDouble();
if (type == olive::NodeValue::k_color) {
out->f[3] = 1.0;
}
break;
}
direct_ok = true;
}
}
if (!direct_ok) {
const QVariant sv = node->get_value_at_time(id, time, eff_element);
if (type == olive::NodeValue::k_color && sv.canConvert<olive::core::Color>()) {
olive::core::Color c = sv.value<olive::core::Color>();
}
olive::NodeValue nv(type, sv);
if (!value_to_c(nv, out)) {
set_error(QStringLiteral(
"input \"%1\" has no POD value at time").arg(id));
return OAKENGINE_E_NOT_FOUND;
}
}
return OAKENGINE_OK;
}
int oakengine_node_get_input_string_at_time(const OakEngineNode *self,
const char *input_id, int element,
int64_t time_ts, int track,
char *buf, int buf_size)
{
set_error(QString());
(void)track;
if (!self || !input_id) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
const olive::Node *node = impl(self);
const QString id = QString::fromUtf8(input_id);
if (!node->inputs().contains(id)) {
set_error(QStringLiteral("unknown input id \"%1\"").arg(id));
return OAKENGINE_E_NOT_FOUND;
}
const olive::NodeValue::Type type = node->get_input_data_type(id);
if (type != olive::NodeValue::k_file &&
type != olive::NodeValue::k_text &&
type != olive::NodeValue::k_font &&
type != olive::NodeValue::k_str_combo) {
set_error(QStringLiteral("\"%1\" is not a string input").arg(id));
return OAKENGINE_E_INVALID;
}
const olive::Rational tb = project_time_base(node);
const olive::Rational time =
olive::core::Timecode::timestamp_to_time(time_ts, tb);
const QVariant sv = node->get_value_at_time(id, time, element);
return string_to_buf(sv.toString(), buf, buf_size);
}
int oakengine_node_get_input_bezier_at_time(const OakEngineNode *self,
const char *input_id, int element,
int64_t time_ts, int track,
double *out_6)
{
set_error(QString());
if (!self || !input_id || !out_6) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
const olive::Node *node = impl(self);
const QString id = QString::fromUtf8(input_id);
if (!node->inputs().contains(id)) {
set_error(QStringLiteral("unknown input id \"%1\"").arg(id));
return OAKENGINE_E_NOT_FOUND;
}
const olive::NodeValue::Type type = node->get_input_data_type(id);
if (type != olive::NodeValue::k_bezier) {
set_error(QStringLiteral("\"%1\" is not a bezier input").arg(id));
return OAKENGINE_E_INVALID;
}
const olive::Rational tb = project_time_base(node);
const olive::Rational time =
olive::core::Timecode::timestamp_to_time(time_ts, tb);
const int nb_tracks =
olive::NodeValue::get_number_of_keyframe_tracks(type);
double vals[6] = { 0 };
const int n = qMin(6, nb_tracks);
for (int i = 0; i < n; ++i) {
const QVariant comp =
node->get_split_value_at_time_on_track(id, time, i, element);
vals[i] = comp.toDouble();
}
out_6[0] = vals[0];
out_6[1] = vals[1];
out_6[2] = vals[2];
out_6[3] = vals[3];
out_6[4] = vals[4];
out_6[5] = vals[5];
return OAKENGINE_OK;
}
int oakengine_node_get_input_binary_at_time(const OakEngineNode *self,
const char *input_id, int element,
int64_t time_ts, int track,
char *buf, int buf_size)
{
set_error(QString());
if (!self || !input_id) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
const olive::Node *node = impl(self);
const QString id = QString::fromUtf8(input_id);
if (!node->inputs().contains(id)) {
set_error(QStringLiteral("unknown input id \"%1\"").arg(id));
return OAKENGINE_E_NOT_FOUND;
}
const olive::NodeValue::Type type = node->get_input_data_type(id);
if (type != olive::NodeValue::k_binary) {
set_error(QStringLiteral("\"%1\" is not a binary input").arg(id));
return OAKENGINE_E_INVALID;
}
const olive::Rational tb = project_time_base(node);
const olive::Rational time =
olive::core::Timecode::timestamp_to_time(time_ts, tb);
const QVariant sv = node->get_value_at_time(id, time, element);
const QByteArray bytes = sv.toByteArray();
if (buf && buf_size > 0) {
const int n = qMin(buf_size, bytes.size());
if (n > 0) {
memcpy(buf, bytes.constData(), size_t(n));
}
}
return bytes.size();
}
/* ---- Input properties ----------------------------------------------------- */
int oakengine_node_input_has_property(const OakEngineNode *self,
const char *input_id, const char *key)
{
if (!self || !input_id || !key) {
return 0;
}
return impl(self)->has_input_property(QString::fromUtf8(input_id),
QString::fromUtf8(key)) ? 1 : 0;
}
int oakengine_node_set_input_property_string(OakEngineNode *self,
const char *input_id,
const char *key,
const char *value, int notify)
{
set_error(QString());
if (!self || !input_id || !key) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
// The engine's set_input_property is direct (no undo). We wrap it in
// the push_or_run pattern when notify != 0.
olive::Node *node = impl(self);
const QString id = QString::fromUtf8(input_id);
if (!node->inputs().contains(id)) {
set_error(QStringLiteral("unknown input id \"%1\"").arg(id));
return OAKENGINE_E_NOT_FOUND;
}
// Apply directly; property changes are typically not undoable at the
// engine level (they are UI hints). The engine's set_input_property
// always emits input_property_changed; the facade's `notify` flag
// controls whether that emission (and therefore the facade event)
// fires.
if (notify) {
node->set_input_property(id, QString::fromUtf8(key),
QVariant::fromValue(QString::fromUtf8(value ? value : "")));
} else {
const QSignalBlocker blocker(node);
node->set_input_property(id, QString::fromUtf8(key),
QVariant::fromValue(QString::fromUtf8(value ? value : "")));
}
return OAKENGINE_OK;
}
int oakengine_node_input_get_property_string(const OakEngineNode *self,
const char *input_id,
const char *key, char *buf,
int buf_size)
{
set_error(QString());
if (!self || !input_id || !key) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
const olive::Node *node = impl(self);
const QString id = QString::fromUtf8(input_id);
if (!node->has_input_property(id, QString::fromUtf8(key))) {
set_error(QStringLiteral("property \"%1\" not found on \"%2\"")
.arg(QString::fromUtf8(key)).arg(id));
return OAKENGINE_E_NOT_FOUND;
}
return string_to_buf(
node->get_input_property(id, QString::fromUtf8(key)).toString(),
buf, buf_size);
}
int oakengine_node_input_get_property_number(const OakEngineNode *self,
const char *input_id,
const char *key, int track,
double *out)
{
set_error(QString());
if (!self || !input_id || !key || !out) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
const olive::Node *node = impl(self);
const QString id = QString::fromUtf8(input_id);
if (!node->has_input_property(id, QString::fromUtf8(key))) {
set_error(QStringLiteral("property \"%1\" not found on \"%2\"")
.arg(QString::fromUtf8(key)).arg(id));
return OAKENGINE_E_NOT_FOUND;
}
const QVariant v = node->get_input_property(id, QString::fromUtf8(key));
bool ok = false;
const double d = v.toDouble(&ok);
if (!ok) {
set_error(QStringLiteral("property \"%1\" is not a number")
.arg(QString::fromUtf8(key)));
return OAKENGINE_E_INVALID;
}
*out = d;
return OAKENGINE_OK;
}
int oakengine_node_input_get_property_int(const OakEngineNode *self,
const char *input_id,
const char *key, int64_t *out)
{
set_error(QString());
if (!self || !input_id || !key || !out) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
const olive::Node *node = impl(self);
const QString id = QString::fromUtf8(input_id);
if (!node->has_input_property(id, QString::fromUtf8(key))) {
set_error(QStringLiteral("property \"%1\" not found on \"%2\"")
.arg(QString::fromUtf8(key)).arg(id));
return OAKENGINE_E_NOT_FOUND;
}
const QVariant v = node->get_input_property(id, QString::fromUtf8(key));
bool ok = false;
const qlonglong i = v.toLongLong(&ok);
if (!ok) {
set_error(QStringLiteral("property \"%1\" is not an integer")
.arg(QString::fromUtf8(key)));
return OAKENGINE_E_INVALID;
}
*out = int64_t(i);
return OAKENGINE_OK;
}
int oakengine_node_input_get_property_rational(const OakEngineNode *self,
const char *input_id,
const char *key, int *num,
int *den)
{
set_error(QString());
if (!self || !input_id || !key) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
const olive::Node *node = impl(self);
const QString id = QString::fromUtf8(input_id);
if (!node->has_input_property(id, QString::fromUtf8(key))) {
set_error(QStringLiteral("property \"%1\" not found on \"%2\"")
.arg(QString::fromUtf8(key)).arg(id));
return OAKENGINE_E_NOT_FOUND;
}
const QVariant v = node->get_input_property(id, QString::fromUtf8(key));
// Try rational first; fall back to converting a plain number.
if (v.canConvert<olive::Rational>()) {
const olive::Rational r = v.value<olive::Rational>();
if (num) *num = r.numerator();
if (den) *den = r.denominator();
return OAKENGINE_OK;
}
// Fallback: treat as double and return (value, 1).
bool ok = false;
const double d = v.toDouble(&ok);
if (ok) {
if (num) *num = int(d);
if (den) *den = 1;
return OAKENGINE_OK;
}
if (num) *num = 0;
if (den) *den = 1;
return OAKENGINE_OK;
}
int oakengine_node_input_get_property_count(const OakEngineNode *self,
const char *input_id)
{
if (!self || !input_id) {
return 0;
}
return impl(self)->get_input_properties(QString::fromUtf8(input_id)).size();
}
int oakengine_node_input_get_property_key(const OakEngineNode *self,
const char *input_id, int index,
char *buf, int buf_size)
{
set_error(QString());
if (!self || !input_id) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
const QHash<QString, QVariant> props =
impl(self)->get_input_properties(QString::fromUtf8(input_id));
const QList<QString> keys = props.keys();
if (index < 0 || index >= keys.size()) {
set_error(QStringLiteral("property index %1 out of range").arg(index));
return OAKENGINE_E_NOT_FOUND;
}
return string_to_buf(keys.at(index), buf, buf_size);
}
int oakengine_node_input_get_property_string_list_count(
const OakEngineNode *self, const char *input_id, const char *key)
{
if (!self || !input_id || !key) {
return 0;
}
const QVariant v = impl(self)->get_input_property(
QString::fromUtf8(input_id), QString::fromUtf8(key));
if (v.typeId() == QMetaType::QStringList) {
return v.toStringList().size();
}
if (v.typeId() == QMetaType::QString) {
return 1;
}
return 0;
}
int oakengine_node_input_get_property_string_list(
const OakEngineNode *self, const char *input_id, const char *key,
int index, char *buf, int buf_size)
{
set_error(QString());
if (!self || !input_id || !key) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
const QVariant v = impl(self)->get_input_property(
QString::fromUtf8(input_id), QString::fromUtf8(key));
QStringList list;
if (v.typeId() == QMetaType::QStringList) {
list = v.toStringList();
} else if (v.typeId() == QMetaType::QString) {
list = QStringList(v.toString());
} else {
set_error(QStringLiteral("property \"%1\" is not a string list")
.arg(QString::fromUtf8(key)));
return OAKENGINE_E_INVALID;
}
if (index < 0 || index >= list.size()) {
set_error(QStringLiteral("property \"%1\" index %2 out of range")
.arg(QString::fromUtf8(key)).arg(index));
return OAKENGINE_E_NOT_FOUND;
}
return string_to_buf(list.at(index), buf, buf_size);
}
/* ---- Node type queries ---------------------------------------------------- */
int oakengine_node_is_group(const OakEngineNode *self)
{
if (!self) {
return 0;
}
return dynamic_cast<const olive::NodeGroup *>(impl(self)) != nullptr ? 1 : 0;
}
int oakengine_node_is_multicam(const OakEngineNode *self)
{
if (!self) {
return 0;
}
return dynamic_cast<const olive::MultiCamNode *>(impl(self)) != nullptr ? 1 : 0;
}
/* ---- Context positions ---------------------------------------------------- */
int oakengine_node_context_node_count(const OakEngineNode *context)
{
if (!context) {
return OAKENGINE_E_INVALID;
}
return impl(context)->get_context_positions().size();
}
int oakengine_node_context_contains_node(const OakEngineNode *context,
const OakEngineNode *node)
{
if (!context || !node) {
return OAKENGINE_E_INVALID;
}
return impl(context)->context_contains_node(const_cast<olive::Node *>(impl(node))) ?
1 : 0;
}
OakEngineNode *oakengine_node_context_node_at(OakEngineNode *context,
int index, double *x,
double *y, int *expanded)
{
if (!context || index < 0) {
return nullptr;
}
const olive::Node::PositionMap &map = impl(context)->get_context_positions();
if (index >= map.size()) {
return nullptr;
}
auto it = map.constBegin();
for (int i = 0; i < index; i++) {
++it;
}
if (x) {
*x = it.value().position.x();
}
if (y) {
*y = it.value().position.y();
}
if (expanded) {
*expanded = it.value().expanded ? 1 : 0;
}
return wrap(it.key());
}
int oakengine_node_set_context_position(OakEngineNode *context,
OakEngineNode *node, double x,
double y)
{
set_error(QString());
if (!context || !node) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
olive::Node *ctx = impl(context);
olive::Node *n = impl(node);
olive::Node::Position pos(QPointF(x, y));
// A plain move must not reset the expanded flag (matches the C++
// QPointF overload of Node::set_node_position_in_context).
if (ctx->context_contains_node(n)) {
pos.expanded = ctx->get_node_position_data_in_context(n).expanded;
}
push_or_run(new olive::NodeSetPositionCommand(n, ctx, pos),
QStringLiteral("Set Position"));
return OAKENGINE_OK;
}
int oakengine_node_get_context_position(const OakEngineNode *context,
const OakEngineNode *node,
double *x, double *y, int *expanded)
{
set_error(QString());
if (!context || !node) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
const olive::Node *ctx = impl(context);
const olive::Node *n = impl(node);
if (!const_cast<olive::Node *>(ctx)->context_contains_node(const_cast<olive::Node *>(n))) {
set_error(QStringLiteral("node not found in context"));
return OAKENGINE_E_NOT_FOUND;
}
const olive::Node::Position pos =
const_cast<olive::Node *>(ctx)->get_node_position_data_in_context(
const_cast<olive::Node *>(n));
if (x) {
*x = pos.position.x();
}
if (y) {
*y = pos.position.y();
}
if (expanded) {
*expanded = pos.expanded ? 1 : 0;
}
return OAKENGINE_OK;
}
int oakengine_node_set_context_expanded(OakEngineNode *context,
OakEngineNode *node, int expanded)
{
set_error(QString());
if (!context || !node) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
olive::Node *ctx = impl(context);
olive::Node *n = impl(node);
ctx->set_node_expanded_in_context(n, expanded != 0);
return OAKENGINE_OK;
}
/* ---- Effect input --------------------------------------------------------- */
int oakengine_node_get_effect_input(const OakEngineNode *self,
char *input_id, int input_id_size,
int *element)
{
set_error(QString());
if (!self) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
const olive::NodeInput ei =
const_cast<olive::Node *>(impl(self))->get_effect_input();
if (!ei.is_valid()) {
set_error(QStringLiteral("node has no effect input"));
return OAKENGINE_E_NOT_FOUND;
}
const int len = string_to_buf(ei.input(), input_id, input_id_size);
if (element) {
*element = ei.element();
}
return len;
}
/* ---- Group passthrough ---------------------------------------------------- */
int oakengine_group_input_passthrough_count(const OakEngineNode *self)
{
if (!self) {
return OAKENGINE_E_INVALID;
}
const olive::NodeGroup *g =
dynamic_cast<const olive::NodeGroup *>(impl(self));
if (!g) {
return OAKENGINE_E_INVALID;
}
return g->get_input_passthroughs().size();
}
int oakengine_group_add_input_passthrough(OakEngineNode *self,
OakEngineNode *inner_node,
const char *inner_input,
int inner_element,
const char *preferred_id,
char *out_id, int out_id_size)
{
set_error(QString());
olive::NodeGroup *g = dynamic_cast<olive::NodeGroup *>(impl(self));
if (!g || !inner_node || !inner_input) {
set_error(QStringLiteral("invalid arguments or not a group"));
return OAKENGINE_E_INVALID;
}
const QString force_id = preferred_id ?
QString::fromUtf8(preferred_id) : QString();
const QString result = g->add_input_passthrough(
olive::NodeInput(impl(inner_node),
QString::fromUtf8(inner_input), inner_element),
force_id);
// buf/size convention: string_to_buf returns the id length even when
// out_id is NULL (NULL queries the length), so the return is > 0 for a
// successfully added passthrough either way.
return string_to_buf(result, out_id, out_id_size);
}
int oakengine_group_input_passthrough_at(const OakEngineNode *self,
int index, char *id, int id_size,
OakEngineNode **node,
char *input_id, int input_id_size,
int *element)
{
set_error(QString());
const olive::NodeGroup *g =
dynamic_cast<const olive::NodeGroup *>(impl(self));
if (!g) {
set_error(QStringLiteral("not a group"));
return OAKENGINE_E_INVALID;
}
const olive::NodeGroup::InputPassthroughs &pts = g->get_input_passthroughs();
if (index < 0 || index >= pts.size()) {
set_error(QStringLiteral("passthrough index %1 out of range").arg(index));
return OAKENGINE_E_INVALID;
}
const auto &pt = pts.at(index);
int total = string_to_buf(pt.first, id, id_size);
if (node) {
*node = wrap(pt.second.node());
}
if (input_id) {
total += string_to_buf(pt.second.input(), input_id, input_id_size);
}
if (element) {
*element = pt.second.element();
}
return total;
}
int oakengine_group_get_id_of_passthrough(const OakEngineNode *self,
OakEngineNode *inner_node,
const char *inner_input,
int inner_element, char *id,
int id_size)
{
set_error(QString());
const olive::NodeGroup *g =
dynamic_cast<const olive::NodeGroup *>(impl(self));
if (!g || !inner_node || !inner_input) {
set_error(QStringLiteral("invalid arguments or not a group"));
return OAKENGINE_E_INVALID;
}
const QString result = g->get_id_of_passthrough(
olive::NodeInput(impl(inner_node),
QString::fromUtf8(inner_input), inner_element));
if (result.isEmpty()) {
set_error(QStringLiteral("no passthrough for that node/input"));
return OAKENGINE_E_NOT_FOUND;
}
if (id) {
return string_to_buf(result, id, id_size);
}
return 0;
}
int oakengine_group_get_passthrough_from_id(const OakEngineNode *self,
const char *id,
OakEngineNode **out_node,
char *out_input,
int out_input_size,
int *out_element)
{
set_error(QString());
const olive::NodeGroup *g =
dynamic_cast<const olive::NodeGroup *>(impl(self));
if (!g || !id) {
set_error(QStringLiteral("invalid arguments or not a group"));
return OAKENGINE_E_INVALID;
}
const olive::NodeInput input = g->get_input_from_id(QString::fromUtf8(id));
if (!input.is_valid()) {
set_error(QStringLiteral("no passthrough with id \"%1\"").arg(id));
return OAKENGINE_E_NOT_FOUND;
}
if (out_node) {
*out_node = wrap(input.node());
}
if (out_input) {
string_to_buf(input.input(), out_input, out_input_size);
}
if (out_element) {
*out_element = input.element();
}
return OAKENGINE_OK;
}
OakEngineNode *oakengine_group_get_output_passthrough(
const OakEngineNode *self)
{
if (!self) {
return nullptr;
}
const olive::NodeGroup *g =
dynamic_cast<const olive::NodeGroup *>(impl(self));
if (!g) {
return nullptr;
}
return wrap(g->get_output_passthrough());
}
int oakengine_group_set_output_passthrough(OakEngineNode *self,
OakEngineNode *inner_node)
{
set_error(QString());
olive::NodeGroup *g = dynamic_cast<olive::NodeGroup *>(impl(self));
if (!g) {
set_error(QStringLiteral("not a group"));
return OAKENGINE_E_INVALID;
}
g->set_output_passthrough(impl(inner_node));
return OAKENGINE_OK;
}
OakEngineNode *oakengine_node_group_create(void)
{
return wrap(new olive::NodeGroup());
}
int oakengine_node_group_get_inner(OakEngineNode **inout_node,
char *inout_input,
int inout_input_size,
int *inout_element)
{
if (!inout_node || !*inout_node || !inout_input ||
inout_input_size <= 0 || !inout_element) {
return 0;
}
olive::Node *node = impl(*inout_node);
olive::NodeGroup *group = dynamic_cast<olive::NodeGroup *>(node);
if (!group) {
return 0;
}
olive::NodeInput input(node, QString::fromUtf8(inout_input),
*inout_element);
if (!olive::NodeGroup::get_inner(&input)) {
return 0;
}
*inout_node = wrap(input.node());
string_to_buf(input.input(), inout_input, inout_input_size);
*inout_element = input.element();
return 1;
}
int oakengine_group_resolve_input(const OakEngineNode *self, const char *id,
int element, OakEngineNode **out_node,
char *out_input, int out_input_size,
int *out_element)
{
set_error(QString());
if (!self || !id) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
const olive::Node *node = impl(self);
const QString pid = QString::fromUtf8(id);
// For a group, resolve through the group's chain.
if (dynamic_cast<const olive::NodeGroup *>(node)) {
const olive::NodeInput resolved =
olive::NodeGroup::resolve_input(
olive::NodeInput(const_cast<olive::Node *>(node), pid, element));
if (out_node) {
*out_node = wrap(resolved.node());
}
if (out_input) {
string_to_buf(resolved.input(), out_input, out_input_size);
}
if (out_element) {
*out_element = resolved.element();
}
return OAKENGINE_OK;
}
// For a plain node, pass through unchanged.
if (out_node) {
*out_node = const_cast<OakEngineNode *>(self);
}
if (out_input) {
string_to_buf(pid, out_input, out_input_size);
}
if (out_element) {
*out_element = element;
}
return OAKENGINE_OK;
}
int oakengine_group_remove_input_passthrough(OakEngineNode *self,
OakEngineNode *inner_node,
const char *inner_input,
int inner_element)
{
set_error(QString());
olive::NodeGroup *g = dynamic_cast<olive::NodeGroup *>(impl(self));
if (!g || !inner_node || !inner_input) {
set_error(QStringLiteral("invalid arguments or not a group"));
return OAKENGINE_E_INVALID;
}
const olive::NodeInput input(impl(inner_node),
QString::fromUtf8(inner_input),
inner_element);
if (!g->contains_input_passthrough(input)) {
set_error(QStringLiteral("passthrough not found"));
return OAKENGINE_E_NOT_FOUND;
}
g->remove_input_passthrough(input);
return OAKENGINE_OK;
}
extern "C" void *oakengine_group_add_input_passthrough_command(
OakEngineNode *self, OakEngineNode *inner_node,
const char *inner_input, int inner_element,
const char *preferred_id)
{
olive::NodeGroup *g = dynamic_cast<olive::NodeGroup *>(impl(self));
if (!g || !inner_node || !inner_input) {
return nullptr;
}
const QString force_id = preferred_id ?
QString::fromUtf8(preferred_id) : QString();
return new olive::NodeGroupAddInputPassthrough(
g, olive::NodeInput(impl(inner_node),
QString::fromUtf8(inner_input),
inner_element),
force_id);
}
extern "C" void *oakengine_group_set_output_passthrough_command(
OakEngineNode *self, OakEngineNode *inner_node)
{
olive::NodeGroup *g = dynamic_cast<olive::NodeGroup *>(impl(self));
if (!g || !inner_node) {
return nullptr;
}
return new olive::NodeGroupSetOutputPassthrough(g, impl(inner_node));
}
int oakengine_group_add_input_passthrough_undoable(
OakEngineNode *self, OakEngineNode *inner_node,
const char *inner_input, int inner_element,
const char *preferred_id)
{
set_error(QString());
void *cmd = oakengine_group_add_input_passthrough_command(
self, inner_node, inner_input, inner_element, preferred_id);
if (!cmd) {
set_error(QStringLiteral("invalid arguments or not a group"));
return OAKENGINE_E_INVALID;
}
push_or_run(static_cast<olive::UndoCommand *>(cmd),
QStringLiteral("Add Input Passthrough"));
return OAKENGINE_OK;
}
int oakengine_group_set_output_passthrough_undoable(
OakEngineNode *self, OakEngineNode *inner_node)
{
set_error(QString());
void *cmd = oakengine_group_set_output_passthrough_command(self, inner_node);
if (!cmd) {
set_error(QStringLiteral("not a group"));
return OAKENGINE_E_INVALID;
}
push_or_run(static_cast<olive::UndoCommand *>(cmd),
QStringLiteral("Set Output Passthrough"));
return OAKENGINE_OK;
}
/* ---- Multi-camera --------------------------------------------------------- */
const char *oakengine_multicam_input_current(void)
{
static const char *s = "current_in";
Q_UNUSED(olive::MultiCamNode::k_current_input);
return s;
}
const char *oakengine_multicam_input_sources(void)
{
static const char *s = "sources_in";
Q_UNUSED(olive::MultiCamNode::k_sources_input);
return s;
}
const char *oakengine_multicam_input_sequence(void)
{
static const char *s = "sequence_in";
Q_UNUSED(olive::MultiCamNode::k_sequence_input);
return s;
}
const char *oakengine_multicam_input_sequence_type(void)
{
static const char *s = "sequence_type_in";
Q_UNUSED(olive::MultiCamNode::k_sequence_type_input);
return s;
}
int oakengine_multicam_get_source_count(const OakEngineNode *self)
{
if (!self) {
return OAKENGINE_E_INVALID;
}
const olive::MultiCamNode *m =
dynamic_cast<const olive::MultiCamNode *>(impl(self));
if (!m) {
return OAKENGINE_E_INVALID;
}
return m->get_source_count();
}
int oakengine_multicam_get_rows_and_columns(int source_count, int *rows,
int *cols)
{
if (source_count < 0 || !rows || !cols) {
return OAKENGINE_E_INVALID;
}
olive::MultiCamNode::get_rows_and_columns(source_count, rows, cols);
return OAKENGINE_OK;
}
int oakengine_multicam_index_to_row_cols(int index, int rows, int cols,
int *out_row, int *out_col)
{
if (index < 0 || rows < 1 || cols < 1 || !out_row || !out_col) {
return OAKENGINE_E_INVALID;
}
olive::MultiCamNode::index_to_row_cols(index, rows, cols, out_row,
out_col);
return OAKENGINE_OK;
}
int oakengine_multicam_rows_cols_to_index(int row, int col, int rows,
int cols)
{
if (row < 0 || col < 0 || rows < 1 || cols < 1 ||
row >= rows || col >= cols) {
return OAKENGINE_E_INVALID;
}
return olive::MultiCamNode::rows_cols_to_index(row, col, rows, cols);
}
int oakengine_multicam_get_current_source(const OakEngineNode *self)
{
if (!self) {
return OAKENGINE_E_INVALID;
}
const olive::MultiCamNode *m =
dynamic_cast<const olive::MultiCamNode *>(impl(self));
if (!m) {
return OAKENGINE_E_INVALID;
}
return m->get_current_source();
}
int oakengine_shape_set_rect_undoable(OakEngineNode *node, double x, double y,
double w, double h,
const oak_video_params *video_params,
void *command)
{
if (!node || !video_params || !command) {
return OAKENGINE_E_INVALID;
}
olive::ShapeNodeBase *shape =
dynamic_cast<olive::ShapeNodeBase *>(impl(node));
if (!shape) {
return OAKENGINE_E_INVALID;
}
olive::VideoParams params(
video_params->width, video_params->height,
olive::Rational(video_params->time_base_num, video_params->time_base_den),
olive::PixelFormat::Format(video_params->format),
olive::VideoParams::k_internal_channel_count,
olive::Rational(video_params->pixel_aspect_num,
video_params->pixel_aspect_den),
static_cast<olive::VideoParams::Interlacing>(video_params->interlacing),
video_params->divider > 0 ? video_params->divider : 1);
shape->set_rect(QRectF(x, y, w, h), params,
static_cast<olive::MultiUndoCommand *>(command));
return OAKENGINE_OK;
}
const char *oakengine_subtitle_text_input_id(void)
{
static const char *s = "text_in";
Q_UNUSED(olive::SubtitleBlock::k_text_in);
return s;
}
int oakengine_subtitle_get_text(const OakEngineNode *node, char *buf,
int buf_size)
{
if (!node) {
return OAKENGINE_E_INVALID;
}
const olive::SubtitleBlock *sub =
dynamic_cast<const olive::SubtitleBlock *>(impl(node));
if (!sub) {
return OAKENGINE_E_INVALID;
}
const QByteArray utf8 = sub->get_text().toUtf8();
const int len = int(utf8.size());
if (buf && buf_size > 0) {
const int n = qMin(len, buf_size - 1);
if (n > 0) {
std::memcpy(buf, utf8.constData(), size_t(n));
}
buf[n] = '\0';
}
return len;
}
int oakengine_subtitle_set_text(OakEngineNode *node, const char *text)
{
if (!node) {
return OAKENGINE_E_INVALID;
}
olive::SubtitleBlock *sub = dynamic_cast<olive::SubtitleBlock *>(impl(node));
if (!sub) {
return OAKENGINE_E_INVALID;
}
sub->set_text(QString::fromUtf8(text ? text : ""));
return OAKENGINE_OK;
}
/* ---- Bulk graph deletion -------------------------------------------------- */
int oakengine_nodes_delete_many(
OakEngineNode *const *nodes, OakEngineNode *const *contexts,
int node_count, OakEngineNode *const *edge_outputs,
OakEngineNode *const *edge_input_nodes,
const char *const *edge_input_ids,
const int *edge_input_elements, int edge_count)
{
return oakengine_nodes_delete_many_ex(
nodes, contexts, node_count, edge_outputs, edge_input_nodes,
edge_input_ids, edge_input_elements, edge_count, nullptr, nullptr,
nullptr, nullptr, 0);
}
int oakengine_nodes_delete_many_ex(
OakEngineNode *const *nodes, OakEngineNode *const *contexts,
int node_count, OakEngineNode *const *edge_outputs,
OakEngineNode *const *edge_input_nodes,
const char *const *edge_input_ids,
const int *edge_input_elements, int edge_count,
OakEngineNode *const *reconnect_outputs,
OakEngineNode *const *reconnect_input_nodes,
const char *const *reconnect_input_ids,
const int *reconnect_input_elements, int reconnect_count)
{
set_error(QString());
if (node_count <= 0 && edge_count <= 0) {
set_error(QStringLiteral("nothing to delete"));
return OAKENGINE_E_INVALID;
}
if (node_count > 0 && !nodes) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
auto *command = new olive::MultiUndoCommand();
auto *dc = new olive::NodeViewDeleteCommand();
command->add_child(dc);
for (int i = 0; i < node_count; i++) {
if (!nodes[i]) {
set_error(QStringLiteral("null node at index %1").arg(i));
delete command;
return OAKENGINE_E_INVALID;
}
olive::Node *ctx = nullptr;
if (contexts && contexts[i]) {
ctx = impl(contexts[i]);
}
dc->add_node(impl(nodes[i]), ctx);
}
for (int i = 0; i < edge_count; i++) {
if (!edge_outputs[i] || !edge_input_nodes[i] || !edge_input_ids[i]) {
set_error(QStringLiteral("invalid edge at index %1").arg(i));
delete command;
return OAKENGINE_E_INVALID;
}
dc->add_edge(
impl(edge_outputs[i]),
olive::NodeInput(impl(edge_input_nodes[i]),
QString::fromUtf8(edge_input_ids[i]),
edge_input_elements ? edge_input_elements[i] : -1));
}
// Reconnect edges run AFTER the deletion inside the same command, so
// they may target inputs that were occupied by the deleted nodes.
for (int i = 0; i < reconnect_count; i++) {
if (!reconnect_outputs[i] || !reconnect_input_nodes[i] ||
!reconnect_input_ids[i]) {
set_error(QStringLiteral("invalid reconnect edge at index %1").arg(i));
delete command;
return OAKENGINE_E_INVALID;
}
command->add_child(new olive::NodeEdgeAddCommand(
impl(reconnect_outputs[i]),
olive::NodeInput(impl(reconnect_input_nodes[i]),
QString::fromUtf8(reconnect_input_ids[i]),
reconnect_input_elements
? reconnect_input_elements[i]
: -1)));
}
push_or_run(command, QStringLiteral("Delete Nodes"));
return OAKENGINE_OK;
}
/* ---- Keyframe best type at time ------------------------------------------- */
int oakengine_node_keyframe_best_type_at_time(
const OakEngineNode *self, const char *input_id, int element,
int64_t time_ts, int track, int default_type)
{
set_error(QString());
if (!self || !input_id) {
return default_type;
}
const olive::Node *node = impl(self);
const QString id = QString::fromUtf8(input_id);
if (!node->inputs().contains(id)) {
return default_type;
}
olive::NodeInputImmediate *imm =
const_cast<olive::Node *>(node)->get_immediate(id, element);
if (!imm) {
return default_type;
}
const olive::Rational tb = project_time_base(node);
const olive::Rational time =
olive::core::Timecode::timestamp_to_time(time_ts, tb);
const olive::NodeKeyframe::Type best =
imm->get_best_keyframe_type_for_time(time, track);
return from_engine_easing(best);
}
/* ---- Handle-based keyframe API -------------------------------------------- */
// Keyframe handle wrappers.
namespace {
const olive::NodeKeyframe *impl_kf_const(const OakEngineKeyframe *h)
{
return reinterpret_cast<const olive::NodeKeyframe *>(h);
}
olive::NodeKeyframe *impl_kf(OakEngineKeyframe *h)
{
return reinterpret_cast<olive::NodeKeyframe *>(h);
}
OakEngineKeyframe *wrap_kf(olive::NodeKeyframe *k)
{
return reinterpret_cast<OakEngineKeyframe *>(k);
}
} // namespace
// Map oak_node_value_type -> olive::NodeValue::Type (reverse of to_c_type).
namespace {
olive::NodeValue::Type from_c_type(int t)
{
switch (t) {
case OAK_NODE_VALUE_NONE: return olive::NodeValue::k_none;
case OAK_NODE_VALUE_INT: return olive::NodeValue::k_int;
case OAK_NODE_VALUE_FLOAT: return olive::NodeValue::k_float;
case OAK_NODE_VALUE_BOOL: return olive::NodeValue::k_boolean;
case OAK_NODE_VALUE_RATIONAL: return olive::NodeValue::k_rational;
case OAK_NODE_VALUE_COLOR: return olive::NodeValue::k_color;
case OAK_NODE_VALUE_VEC2: return olive::NodeValue::k_vec2;
case OAK_NODE_VALUE_VEC3: return olive::NodeValue::k_vec3;
case OAK_NODE_VALUE_VEC4: return olive::NodeValue::k_vec4;
case OAK_NODE_VALUE_COMBO: return olive::NodeValue::k_combo;
case OAK_NODE_VALUE_STRING: return olive::NodeValue::k_file;
case OAK_NODE_VALUE_TEXT: return olive::NodeValue::k_text;
case OAK_NODE_VALUE_FONT: return olive::NodeValue::k_font;
case OAK_NODE_VALUE_STR_COMBO: return olive::NodeValue::k_str_combo;
case OAK_NODE_VALUE_BINARY: return olive::NodeValue::k_binary;
case OAK_NODE_VALUE_BEZIER: return olive::NodeValue::k_bezier;
case OAK_NODE_VALUE_TEXTURE: return olive::NodeValue::k_texture;
case OAK_NODE_VALUE_SAMPLES: return olive::NodeValue::k_samples;
case OAK_NODE_VALUE_VIDEO_PARAMS: return olive::NodeValue::k_video_params;
case OAK_NODE_VALUE_AUDIO_PARAMS: return olive::NodeValue::k_audio_params;
default: return olive::NodeValue::k_none;
}
}
} // namespace
int oakengine_node_keyframe_track_count(const OakEngineNode *self,
const char *input_id, int element)
{
if (!self || !input_id) {
return 0;
}
return impl(self)->get_number_of_keyframe_tracks(
QString::fromUtf8(input_id));
}
int oakengine_node_keyframe_count_on_track(const OakEngineNode *self,
const char *input_id, int element,
int track)
{
if (!self || !input_id) {
return 0;
}
const QVector<olive::NodeKeyframeTrack> &tracks =
impl(self)->get_keyframe_tracks(QString::fromUtf8(input_id), element);
if (track < 0 || track >= tracks.size()) {
return 0;
}
return tracks.at(track).size();
}
int oakengine_node_keyframes_toggle_at_time(OakEngineNode *self,
const char *input_id,
int element, int64_t time_ts,
int track, int on,
const char *undo_name)
{
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;
}
const QString id = QString::fromUtf8(input_id);
// Facade contract: time_ts is in SECONDS and track is 1-based
// (track=1 addresses the first track).
const olive::Rational time(time_ts);
const int track_index = track - 1;
const QString name = undo_name ?
QString::fromUtf8(undo_name) : QStringLiteral("Toggle Keyframe");
if (on) {
// Check if a keyframe already exists; if so, no-op.
olive::NodeKeyframe *existing =
node->get_keyframe_at_time_on_track(id, time, track_index,
element);
if (existing) {
return OAKENGINE_OK;
}
// Create keyframe with current value and best type.
const QVariant cv = node->get_value_at_time(id, time, element);
olive::NodeKeyframe *key = new olive::NodeKeyframe(
time, cv, olive::NodeKeyframe::k_default_type, track_index,
element, id);
olive::MultiUndoCommand *cmd = new olive::MultiUndoCommand();
if (!node->is_input_keyframing(id, element)) {
cmd->add_child(new olive::NodeParamSetKeyframingCommand(
olive::NodeInput(node, id, element), true));
}
cmd->add_child(new olive::NodeParamInsertKeyframeCommand(node, key));
push_or_run(cmd, name);
} else {
// Remove keyframe if it exists.
olive::NodeKeyframe *existing =
node->get_keyframe_at_time_on_track(id, time, track_index,
element);
if (!existing) {
return OAKENGINE_OK; // no-op
}
push_or_run(new olive::NodeParamRemoveKeyframeCommand(existing), name);
}
return OAKENGINE_OK;
}
int oakengine_node_has_keyframe_at_time(const OakEngineNode *self,
const char *input_id, int element,
int64_t time_ts, int track)
{
if (!self || !input_id) {
return 0;
}
const olive::Node *node = impl(self);
const QString id = QString::fromUtf8(input_id);
// Facade contract: time_ts is in SECONDS and track is 1-based.
return node->get_keyframe_at_time_on_track(id, olive::Rational(time_ts),
track - 1, element) ?
1 : 0;
}
int oakengine_node_keyframe_earliest_time(const OakEngineNode *self,
const char *input_id, int element,
int64_t *num, int64_t *den)
{
set_error(QString());
if (!self || !input_id) {
set_error(QStringLiteral("invalid arguments"));
return 0;
}
olive::NodeInputImmediate *imm =
const_cast<olive::Node *>(impl(self))->get_immediate(
QString::fromUtf8(input_id), element);
if (!imm) {
set_error(QStringLiteral("no keyframe tracks"));
return 0;
}
const olive::NodeKeyframe *earliest = imm->get_earliest_keyframe();
if (!earliest) {
if (num) *num = 0;
if (den) *den = 1;
return 0;
}
const olive::Rational &time = earliest->time();
if (num) *num = time.numerator();
if (den) *den = time.denominator();
return 1;
}
int oakengine_node_keyframe_latest_time(const OakEngineNode *self,
const char *input_id, int element,
int64_t *num, int64_t *den)
{
set_error(QString());
if (!self || !input_id) {
set_error(QStringLiteral("invalid arguments"));
return 0;
}
olive::NodeInputImmediate *imm =
const_cast<olive::Node *>(impl(self))->get_immediate(
QString::fromUtf8(input_id), element);
if (!imm) {
set_error(QStringLiteral("no keyframe tracks"));
return 0;
}
const olive::NodeKeyframe *latest = imm->get_latest_keyframe();
if (!latest) {
if (num) *num = 0;
if (den) *den = 1;
return 0;
}
const olive::Rational &time = latest->time();
if (num) *num = time.numerator();
if (den) *den = time.denominator();
return 1;
}
int oakengine_node_keyframe_closest_time_before(
const OakEngineNode *self, const char *input_id, int element,
int64_t time_ts, int track, int64_t *num, int64_t *den)
{
set_error(QString());
if (!self || !input_id) {
set_error(QStringLiteral("invalid arguments"));
return 0;
}
const olive::Node *node = impl(self);
const QString id = QString::fromUtf8(input_id);
// Facade contract: time_ts is in SECONDS and track is 1-based.
const olive::Rational time(time_ts);
const int track_index = track - 1;
olive::NodeInputImmediate *imm =
const_cast<olive::Node *>(node)->get_immediate(id, element);
if (!imm) {
return 0;
}
// Walk the track to find the closest keyframe before the given time.
const QVector<olive::NodeKeyframeTrack> &tracks =
node->get_keyframe_tracks(id, element);
if (track_index < 0 || track_index >= tracks.size()) {
return 0;
}
const olive::NodeKeyframe *found = nullptr;
for (const olive::NodeKeyframe *key : tracks.at(track_index)) {
if (key->time() < time) {
if (!found || key->time() > found->time()) {
found = key;
}
}
}
if (!found) {
return 0;
}
if (num) *num = found->time().numerator();
if (den) *den = found->time().denominator();
return 1;
}
int oakengine_node_keyframe_closest_time_after(
const OakEngineNode *self, const char *input_id, int element,
int64_t time_ts, int track, int64_t *num, int64_t *den)
{
set_error(QString());
if (!self || !input_id) {
set_error(QStringLiteral("invalid arguments"));
return 0;
}
const olive::Node *node = impl(self);
const QString id = QString::fromUtf8(input_id);
// Facade contract: time_ts is in SECONDS and track is 1-based.
const olive::Rational time(time_ts);
const int track_index = track - 1;
const QVector<olive::NodeKeyframeTrack> &tracks =
node->get_keyframe_tracks(id, element);
if (track_index < 0 || track_index >= tracks.size()) {
return 0;
}
const olive::NodeKeyframe *found = nullptr;
for (const olive::NodeKeyframe *key : tracks.at(track_index)) {
if (key->time() > time) {
if (!found || key->time() < found->time()) {
found = key;
}
}
}
if (!found) {
return 0;
}
if (num) *num = found->time().numerator();
if (den) *den = found->time().denominator();
return 1;
}
OakEngineKeyframe *oakengine_node_keyframe_handle_on_track(
const OakEngineNode *self, const char *input_id, int element,
int track, int index)
{
if (!self || !input_id) {
return nullptr;
}
const QVector<olive::NodeKeyframeTrack> &tracks =
impl(self)->get_keyframe_tracks(QString::fromUtf8(input_id), element);
if (track < 0 || track >= tracks.size()) {
return nullptr;
}
const olive::NodeKeyframeTrack &tr = tracks.at(track);
if (index < 0 || index >= tr.size()) {
return nullptr;
}
return wrap_kf(tr.at(index));
}
OakEngineKeyframe *oakengine_node_keyframe_handle_at_time(
const OakEngineNode *self, const char *input_id, int element,
int track, int64_t time_ts, int track_for_time)
{
if (!self || !input_id) {
return nullptr;
}
const olive::Node *node = impl(self);
const QString id = QString::fromUtf8(input_id);
// Facade contract: time_ts is in SECONDS and track_for_time is 1-based.
olive::NodeKeyframe *key = node->get_keyframe_at_time_on_track(
id, olive::Rational(time_ts), track_for_time - 1, element);
return wrap_kf(key);
}
int oakengine_node_keyframes_at_time(const OakEngineNode *self,
const char *input_id, int element,
int64_t time_ts, int track,
OakEngineKeyframe **out_handles,
int max_handles)
{
if (!self || !input_id || !out_handles || max_handles <= 0) {
return 0;
}
const olive::Node *node = impl(self);
const QString id = QString::fromUtf8(input_id);
// Facade contract: time_ts is in SECONDS.
const olive::Rational time(time_ts);
olive::NodeInputImmediate *imm =
const_cast<olive::Node *>(node)->get_immediate(id, element);
if (!imm) {
return 0;
}
const QVector<olive::NodeKeyframe *> at_time =
imm->get_keyframe_at_time(time);
const int n = qMin(at_time.size(), max_handles);
for (int i = 0; i < n; i++) {
out_handles[i] = wrap_kf(at_time[i]);
}
return n;
}
int oakengine_node_set_input_keyframing(OakEngineNode *self,
const char *input_id, int element,
int keyframing, int track,
int enable_all_tracks,
const char *undo_name)
{
set_error(QString());
(void)track;
(void)enable_all_tracks;
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;
}
const QString id = QString::fromUtf8(input_id);
const bool already = node->is_input_keyframing(id, element);
const QString name = undo_name ?
QString::fromUtf8(undo_name) : QStringLiteral("Set Keyframing");
if (keyframing && already) {
// Already enabled: redundant enable is a no-op success.
return OAKENGINE_OK;
}
// Facade contract (see oakengine/node.h): enabling keyframing also seeds
// one default-type keyframe per track (at t=0 with the current split
// standard value); disabling removes every keyframe on every track.
// Both are ONE undoable command.
auto *command = new olive::MultiUndoCommand();
const olive::NodeInput input(node, id, element);
if (keyframing) {
command->add_child(
new olive::NodeParamSetKeyframingCommand(input, true));
const int tracks =
olive::NodeValue::get_number_of_keyframe_tracks(declared);
const olive::SplitValue values =
node->get_split_standard_value(id, element);
for (int t = 0; t < tracks; t++) {
const QVariant v = t < values.size() ? values.at(t) : QVariant();
command->add_child(new olive::NodeParamInsertKeyframeCommand(
node, new olive::NodeKeyframe(
olive::Rational(0), v,
olive::NodeKeyframe::k_default_type, t, element,
id)));
}
} else {
const QVector<olive::NodeKeyframeTrack> &tracks =
node->get_keyframe_tracks(id, element);
for (const olive::NodeKeyframeTrack &tr : tracks) {
for (olive::NodeKeyframe *key : tr) {
command->add_child(
new olive::NodeParamRemoveKeyframeCommand(key));
}
}
command->add_child(
new olive::NodeParamSetKeyframingCommand(input, false));
}
push_or_run(command, name);
return OAKENGINE_OK;
}
extern "C" void *oakengine_node_set_input_keyframing_command(
OakEngineNode *self, const char *input_id, int element, int keyframing)
{
if (!self || !input_id) {
return nullptr;
}
olive::Node *node = impl(self);
if (!node->inputs().contains(QString::fromUtf8(input_id))) {
return nullptr;
}
return new olive::NodeParamSetKeyframingCommand(
olive::NodeInput(node, QString::fromUtf8(input_id), element),
keyframing != 0);
}
int oakengine_node_keyframes_paste(OakEngineNode *self,
OakEngineKeyframe *const *keyframes,
int count, const char *undo_name)
{
set_error(QString());
if (!self || !keyframes || count <= 0) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
olive::Node *node = impl(self);
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
const QString name = undo_name ?
QString::fromUtf8(undo_name) : QStringLiteral("Paste Keyframes");
for (int i = 0; i < count; i++) {
if (!keyframes[i]) {
set_error(QStringLiteral("null keyframe at index %1").arg(i));
delete command;
return OAKENGINE_E_INVALID;
}
olive::NodeKeyframe *src = impl_kf(keyframes[i]);
auto *clone = new olive::NodeKeyframe(src->time(), src->value(),
src->type(), src->track(),
src->element(), src->input());
clone->set_bezier_control_in(src->bezier_control_in());
clone->set_bezier_control_out(src->bezier_control_out());
if (!node->is_input_keyframing(src->input(), src->element())) {
command->add_child(new olive::NodeParamSetKeyframingCommand(
olive::NodeInput(node, src->input(), src->element()), true));
}
command->add_child(
new olive::NodeParamInsertKeyframeCommand(node, clone));
}
push_or_run(command, name);
return OAKENGINE_OK;
}
/* ---- OakEngineKeyframe accessors ------------------------------------------ */
int oakengine_keyframe_get_time(const OakEngineKeyframe *self, int64_t *num,
int64_t *den)
{
if (!self) {
return OAKENGINE_E_INVALID;
}
const olive::Rational &time = impl_kf_const(self)->time();
if (num) {
*num = time.numerator();
}
if (den) {
*den = time.denominator();
}
return OAKENGINE_OK;
}
int oakengine_keyframe_get_input_id(const OakEngineKeyframe *self, char *buf,
int buf_size)
{
if (!self) {
return OAKENGINE_E_INVALID;
}
return string_to_buf(impl_kf_const(self)->input(), buf, buf_size);
}
int oakengine_keyframe_get_track(const OakEngineKeyframe *self)
{
if (!self) {
return -1;
}
return impl_kf_const(self)->track();
}
int oakengine_keyframe_get_element(const OakEngineKeyframe *self)
{
if (!self) {
return -1;
}
return impl_kf_const(self)->element();
}
OakEngineNode *oakengine_keyframe_get_node(const OakEngineKeyframe *self)
{
if (!self) {
return nullptr;
}
return wrap(impl_kf_const(self)->parent());
}
int oakengine_keyframe_get_type(const OakEngineKeyframe *self)
{
if (!self) {
return -1;
}
return from_engine_easing(impl_kf_const(self)->type());
}
int oakengine_keyframe_default_type(void)
{
return from_engine_easing(olive::NodeKeyframe::k_default_type);
}
int oakengine_keyframe_opposing_bezier_type(int type)
{
return int(olive::NodeKeyframe::get_opposing_bezier_type(
olive::NodeKeyframe::BezierType(type)));
}
int oakengine_keyframe_get_value(const OakEngineKeyframe *self,
oak_node_value *out)
{
set_error(QString());
if (!self || !out) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
const olive::NodeKeyframe *key = impl_kf_const(self);
// Determine the type from the key's input on the parent node.
const olive::Node *parent = key->parent();
const QString &input_id = key->input();
olive::NodeValue::Type type = olive::NodeValue::k_float;
if (parent && parent->inputs().contains(input_id)) {
type = parent->get_input_data_type(input_id);
}
return kf_value_to_c(type, key->value(), out) ?
OAKENGINE_OK : OAKENGINE_E_INVALID;
}
int oakengine_keyframe_has_sibling_at_time(const OakEngineKeyframe *self,
int64_t time_ts, int track)
{
if (!self) {
return 0;
}
const olive::NodeKeyframe *key = impl_kf_const(self);
const olive::Rational time(track, 1); // fallback
return key->has_sibling_at_time(olive::Rational(time_ts, 1)) ? 1 : 0;
}
int oakengine_keyframe_set_bezier_point_live(OakEngineKeyframe *self,
int point_index, double x,
double y)
{
set_error(QString());
if (!self || point_index < 0 || point_index > 1) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
olive::NodeKeyframe *key = impl_kf(self);
const olive::NodeKeyframe::BezierType mode =
(point_index == 0) ? olive::NodeKeyframe::k_in_handle :
olive::NodeKeyframe::k_out_handle;
key->set_bezier_control(mode, QPointF(x, y));
return OAKENGINE_OK;
}
int oakengine_keyframe_get_bezier_point(const OakEngineKeyframe *self,
int point_index, double *x,
double *y)
{
set_error(QString());
if (!self || !x || !y || point_index < 0 || point_index > 1) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
const olive::NodeKeyframe *key = impl_kf_const(self);
const olive::NodeKeyframe::BezierType mode =
(point_index == 0) ? olive::NodeKeyframe::k_in_handle :
olive::NodeKeyframe::k_out_handle;
const QPointF p = key->bezier_control(mode);
*x = p.x();
*y = p.y();
return OAKENGINE_OK;
}
int oakengine_keyframe_get_valid_bezier_point(const OakEngineKeyframe *self,
int point_index, double *x,
double *y)
{
set_error(QString());
if (!self || !x || !y || point_index < 0 || point_index > 1) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
const olive::NodeKeyframe *key = impl_kf_const(self);
const QPointF p = (point_index == 0) ?
key->valid_bezier_control_in() :
key->valid_bezier_control_out();
*x = p.x();
*y = p.y();
return OAKENGINE_OK;
}
int oakengine_keyframe_set_value_live(OakEngineKeyframe *self,
const oak_node_value *value)
{
set_error(QString());
if (!self || !value) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
olive::NodeKeyframe *key = impl_kf(self);
// Map the POD to a QVariant. Use key's parent node to determine type.
const olive::Node *parent = key->parent();
const QString &input_id = key->input();
olive::NodeValue::Type type = olive::NodeValue::k_float;
if (parent && parent->inputs().contains(input_id)) {
type = parent->get_input_data_type(input_id);
}
QVariant qv;
if (!component_from_c(value, type, 0, &qv)) {
set_error(QStringLiteral("value type mismatch"));
return OAKENGINE_E_INVALID;
}
key->set_value(qv);
return OAKENGINE_OK;
}
int oakengine_keyframe_set_time_live(OakEngineKeyframe *self, int64_t num,
int64_t den)
{
set_error(QString());
if (!self) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
impl_kf(self)->set_time(olive::Rational(int(num), int(den)));
return OAKENGINE_OK;
}
int oakengine_keyframes_remove_many(OakEngineKeyframe *const *keyframes,
int count, const char *undo_name)
{
set_error(QString());
if (!keyframes || count <= 0) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
// Check for NULL entries first.
for (int i = 0; i < count; i++) {
if (!keyframes[i]) {
set_error(QStringLiteral("null keyframe at index %1").arg(i));
return OAKENGINE_E_INVALID;
}
}
const QString name = undo_name ?
QString::fromUtf8(undo_name) : QStringLiteral("Remove Keyframes");
olive::MultiUndoCommand *command = new olive::MultiUndoCommand();
for (int i = 0; i < count; i++) {
command->add_child(
new olive::NodeParamRemoveKeyframeCommand(impl_kf(keyframes[i])));
}
push_or_run(command, name);
return OAKENGINE_OK;
}
OakEngineKeyframe *oakengine_keyframe_create(
OakEngineNode *node, const char *input_id, int element, int track,
int64_t time_ts, int type, const oak_node_value *value,
int64_t duration_ts)
{
set_error(QString());
(void)duration_ts;
if (!node || !input_id || !value) {
set_error(QStringLiteral("invalid arguments"));
return nullptr;
}
olive::Node *n = impl(node);
const QString id = QString::fromUtf8(input_id);
if (!n->inputs().contains(id)) {
set_error(QStringLiteral("unknown input id \"%1\"").arg(id));
return nullptr;
}
const olive::Rational tb = project_time_base(impl(node));
const olive::Rational time =
olive::core::Timecode::timestamp_to_time(time_ts, tb);
const olive::NodeValue::Type declared = n->get_input_data_type(id);
QVariant engine_value;
if (!component_from_c(value, declared, 0, &engine_value)) {
set_error(QStringLiteral("value type mismatch for \"%1\"").arg(id));
return nullptr;
}
auto *key = new olive::NodeKeyframe(time, engine_value,
to_engine_easing(type),
track, element, id);
return wrap_kf(key);
}
void oakengine_keyframe_dispose(OakEngineKeyframe *keyframe)
{
if (!keyframe) {
return;
}
delete impl_kf(keyframe);
}
/* ---- Input dragger -------------------------------------------------------- */
struct OakEngineNodeDraggerImpl {
olive::Node *node;
QString input_id;
int element;
int track;
int64_t time_ts;
bool started;
int keys_before;
};
OakEngineNodeDragger *oakengine_dragger_create(OakEngineNode *node,
const char *input_id,
int element, int track)
{
set_error(QString());
if (!node || !input_id) {
set_error(QStringLiteral("invalid arguments"));
return nullptr;
}
olive::Node *n = impl(node);
const QString id = QString::fromUtf8(input_id);
if (!n->inputs().contains(id)) {
set_error(QStringLiteral("unknown input id \"%1\"").arg(id));
return nullptr;
}
auto *d = new OakEngineNodeDraggerImpl();
d->node = n;
d->input_id = id;
d->element = element;
d->track = track;
d->time_ts = 0;
d->started = false;
d->keys_before = 0;
return reinterpret_cast<OakEngineNodeDragger *>(d);
}
int oakengine_dragger_start(OakEngineNodeDragger *self, int64_t time_ts,
int track, int insert_on_all_tracks)
{
set_error(QString());
if (!self) {
set_error(QStringLiteral("invalid dragger"));
return OAKENGINE_E_INVALID;
}
auto *d = reinterpret_cast<OakEngineNodeDraggerImpl *>(self);
if (d->started) {
set_error(QStringLiteral("dragger already started"));
return OAKENGINE_E_STATE;
}
d->time_ts = time_ts;
d->track = track;
d->keys_before = olive::NodeInput(d->node, d->input_id, d->element)
.get_array_size();
// Create a keyframe at the drag time by calling set_value_at_time.
// We need a temporary value; the dragger will live-set it.
// Facade contract: time_ts is a frame timestamp and track is 1-based.
const olive::Rational tb = project_time_base(d->node);
const olive::Rational time =
olive::core::Timecode::timestamp_to_time(d->time_ts, tb);
const QVariant cv = d->node->get_value_at_time(d->input_id, time,
d->element);
olive::MultiUndoCommand *temp_cmd = new olive::MultiUndoCommand();
olive::Node::set_value_at_time(
olive::NodeInput(d->node, d->input_id, d->element),
time, cv, track - 1, temp_cmd, insert_on_all_tracks != 0);
// Execute the command immediately (we'll push the final command at end).
temp_cmd->redo_now();
delete temp_cmd;
d->started = true;
return OAKENGINE_OK;
}
int oakengine_dragger_drag(OakEngineNodeDragger *self,
const oak_node_value *value)
{
set_error(QString());
if (!self || !value) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
auto *d = reinterpret_cast<OakEngineNodeDraggerImpl *>(self);
if (!d->started) {
set_error(QStringLiteral("dragger not started"));
return OAKENGINE_E_STATE;
}
// Facade contract: the stored time_ts is a frame timestamp and track is
// 1-based.
const olive::Rational tb = project_time_base(d->node);
const olive::Rational time =
olive::core::Timecode::timestamp_to_time(d->time_ts, tb);
const olive::NodeValue::Type declared =
d->node->get_input_data_type(d->input_id);
QVariant qv;
if (!component_from_c(value, declared, 0, &qv)) {
set_error(QStringLiteral("value type mismatch"));
return OAKENGINE_E_INVALID;
}
// Live-set the keyframe value at the drag time.
olive::NodeKeyframe *key = d->node->get_keyframe_at_time_on_track(
d->input_id, time, d->track - 1, d->element);
if (key) {
key->set_value(qv);
}
return OAKENGINE_OK;
}
int oakengine_dragger_end(OakEngineNodeDragger *self, const char *undo_name)
{
set_error(QString());
if (!self) {
set_error(QStringLiteral("invalid dragger"));
return OAKENGINE_E_INVALID;
}
auto *d = reinterpret_cast<OakEngineNodeDraggerImpl *>(self);
if (!d->started) {
set_error(QStringLiteral("dragger not started"));
return OAKENGINE_E_STATE;
}
// Push the single undo command that captures the entire drag.
// Facade contract: the stored time_ts is a frame timestamp and track is
// 1-based.
const olive::Rational tb = project_time_base(d->node);
const olive::Rational time =
olive::core::Timecode::timestamp_to_time(d->time_ts, tb);
// Capture the dragged (final) value before touching the live key.
const QVariant final_value = d->node->get_value_at_time(
d->input_id, time, d->element);
olive::NodeKeyframe *key = d->node->get_keyframe_at_time_on_track(
d->input_id, time, d->track - 1, d->element);
if (key) {
// The start-created key lives on the node but NOT on the undo stack
// (dragger_start executed it with redo_now). Remove it live and push
// ONE undoable command that recreates it holding the final value, so
// the whole drag is a single undo entry: undo removes the key
// entirely (restoring the pre-drag keyframe count), redo re-creates
// it with the final value.
delete key;
const QString name = undo_name ?
QString::fromUtf8(undo_name) : QStringLiteral("Drag Input");
olive::MultiUndoCommand *cmd = new olive::MultiUndoCommand();
olive::Node::set_value_at_time(
olive::NodeInput(d->node, d->input_id, d->element),
time, final_value, d->track - 1, cmd, false);
push_or_run(cmd, name);
}
d->started = false;
return OAKENGINE_OK;
}
int oakengine_dragger_is_started(const OakEngineNodeDragger *self)
{
if (!self) {
return 0;
}
return reinterpret_cast<const OakEngineNodeDraggerImpl *>(self)->started ?
1 : 0;
}
void oakengine_dragger_free(OakEngineNodeDragger *self)
{
if (!self) {
return;
}
delete reinterpret_cast<OakEngineNodeDraggerImpl *>(self);
}
int oakengine_node_set_value_hint(OakEngineNode *self, const char *input_id,
int element, int type, int index,
const char *tag)
{
set_error(QString());
if (!self || !input_id) {
set_error(QStringLiteral("invalid arguments"));
return OAKENGINE_E_INVALID;
}
olive::Node *node = impl(self);
const QString id = QString::fromUtf8(input_id);
if (!node->inputs().contains(id)) {
set_error(QStringLiteral("unknown input id \"%1\"").arg(id));
return OAKENGINE_E_NOT_FOUND;
}
// Validate type: must be a valid oak_node_value_type or -1.
olive::NodeValue::Type nv_type = olive::NodeValue::k_none;
if (type >= 0) {
nv_type = from_c_type(type);
if (nv_type == olive::NodeValue::k_none && type != 0) {
set_error(QStringLiteral("invalid value type %1").arg(type));
return OAKENGINE_E_INVALID;
}
}
// An empty type list means "no preference" and falls back to the input's
// declared type (NodeTraverser::generate_row_value_element_index);
// OAK_NODE_VALUE_NONE (0) and -1 both leave the list empty.
QVector<olive::NodeValue::Type> types;
if (nv_type != olive::NodeValue::k_none) {
types.append(nv_type);
}
olive::Node::ValueHint hint(types, index,
QString::fromUtf8(tag ? tag : ""));
node->set_value_hint_for_input(id, hint, element);
return OAKENGINE_OK;
}
/* ---- Node static data and helpers ----------------------------------------- */
extern "C" const char *oakengine_node_enabled_input_id(void)
{
return olive::Node::k_enabled_input.toUtf8().constData();
}
extern "C" const char *oakengine_volume_samples_input_id(void)
{
return olive::VolumeNode::k_samples_input.toUtf8().constData();
}
extern "C" const char *oakengine_transform_texture_input_id(void)
{
return olive::TransformDistortNode::k_texture_input.toUtf8().constData();
}
extern "C" const char *oakengine_transition_in_block_input_id(void)
{
return olive::TransitionBlock::k_in_block_input.toUtf8().constData();
}
extern "C" const char *oakengine_transition_out_block_input_id(void)
{
return olive::TransitionBlock::k_out_block_input.toUtf8().constData();
}
extern "C" double oakengine_audio_waveform_max_sample_rate(void)
{
return olive::AudioVisualWaveform::k_maximum_sample_rate.to_double();
}
extern "C" int oakengine_node_category_name(int category_id, char *buf,
int buf_size)
{
QString name = olive::Node::get_category_name(
olive::Node::CategoryID(category_id));
QByteArray utf8 = name.toUtf8();
return string_to_buf(name, buf, buf_size);
}
extern "C" void *oakengine_node_link_command(OakEngineNode *a,
OakEngineNode *b, int link)
{
auto *na = reinterpret_cast<olive::Node *>(a);
auto *nb = reinterpret_cast<olive::Node *>(b);
if (!na || !nb) {
return nullptr;
}
return new olive::NodeLinkCommand(na, nb, link != 0);
}
extern "C" OakEngineNode *oakengine_node_copy_in_graph(
OakEngineNode *node, void *command)
{
auto *n = reinterpret_cast<olive::Node *>(node);
if (!n) {
return nullptr;
}
olive::MultiUndoCommand *cmd = command
? static_cast<olive::MultiUndoCommand *>(command) : nullptr;
olive::Node *copy = olive::Node::copy_node_in_graph(n, cmd);
if (!cmd && copy) {
// If no parent command, the copy was made directly.
}
return reinterpret_cast<OakEngineNode *>(copy);
}
extern "C" int oakengine_node_copy_dependency_graph(
OakEngineNode *const *nodes, OakEngineNode *const *copies, int count,
void *command)
{
if (!nodes || !copies || count <= 0) {
return OAKENGINE_E_INVALID;
}
QList<olive::Node *> node_list;
QList<olive::Node *> copy_list;
for (int i = 0; i < count; i++) {
node_list.append(reinterpret_cast<olive::Node *>(nodes[i]));
copy_list.append(reinterpret_cast<olive::Node *>(copies[i]));
}
olive::MultiUndoCommand *cmd = command
? static_cast<olive::MultiUndoCommand *>(command) : nullptr;
olive::Node::copy_dependency_graph(node_list, copy_list, cmd);
return OAKENGINE_OK;
}
extern "C" int oakengine_node_connect_command_string(
OakEngineNode *output, OakEngineNode *input_node,
const char *input_id, int element, char *buf, int buf_size)
{
auto *out = reinterpret_cast<olive::Node *>(output);
auto *in = reinterpret_cast<olive::Node *>(input_node);
if (!out || !in || !input_id) {
return OAKENGINE_E_INVALID;
}
olive::NodeInput ni(in, QString::fromUtf8(input_id), element);
QString name = olive::Node::get_connect_command_string(out, ni);
QByteArray utf8 = name.toUtf8();
return string_to_buf(name, buf, buf_size);
}
extern "C" int oakengine_node_transform_time_to(
OakEngineNode *from, OakEngineNode *to, int direction,
int path_index, int64_t in_num, int64_t in_den,
int64_t out_num, int64_t out_den,
int64_t *result_in_num, int64_t *result_in_den,
int64_t *result_out_num, int64_t *result_out_den)
{
auto *f = reinterpret_cast<olive::Node *>(from);
auto *t = reinterpret_cast<olive::Node *>(to);
if (!f || !t) {
return OAKENGINE_E_INVALID;
}
olive::TimeRange range(
olive::Rational(in_num, in_den),
olive::Rational(out_num, out_den));
olive::TimeRange result = f->transform_time_to(
range, t,
static_cast<olive::Node::TransformTimeDirection>(direction),
path_index);
if (result_in_num) *result_in_num = result.in().numerator();
if (result_in_den) *result_in_den = result.in().denominator();
if (result_out_num) *result_out_num = result.out().numerator();
if (result_out_den) *result_out_den = result.out().denominator();
return OAKENGINE_OK;
}
/* ---- P1.1: NodeValue static methods (F class: 4 symbols) ---------------- */
int oakengine_node_value_keyframe_track_count(int c_type)
{
olive::NodeValue::Type type = from_c_type(c_type);
return olive::NodeValue::get_number_of_keyframe_tracks(type);
}
int oakengine_node_value_pretty_type_name(int c_type, char *buf, int buf_size)
{
if (c_type <= OAK_NODE_VALUE_NONE ||
c_type > OAK_NODE_VALUE_AUDIO_PARAMS) {
return -1;
}
olive::NodeValue::Type type = from_c_type(c_type);
QString name = olive::NodeValue::get_pretty_data_type_name(type);
return string_to_buf(name, buf, buf_size);
}
int oakengine_node_value_split_to_tracks(int c_type,
const oak_node_value *normal, oak_node_value *tracks_out, int track_count)
{
if (!normal || !tracks_out || track_count <= 0) {
return OAKENGINE_E_INVALID;
}
olive::NodeValue::Type type = from_c_type(c_type);
QVariant v;
switch (type) {
case olive::NodeValue::k_int:
case olive::NodeValue::k_combo:
v = QVariant(static_cast<int>(normal->num));
break;
case olive::NodeValue::k_float:
v = normal->f[0];
break;
case olive::NodeValue::k_boolean:
v = normal->num != 0;
break;
case olive::NodeValue::k_rational:
v = QVariant::fromValue(olive::Rational(normal->num, normal->den));
break;
case olive::NodeValue::k_color:
v = QVariant::fromValue(olive::core::Color(
normal->f[0], normal->f[1], normal->f[2], normal->f[3]));
break;
case olive::NodeValue::k_vec2:
v = QVariant::fromValue(QVector2D(normal->f[0], normal->f[1]));
break;
case olive::NodeValue::k_vec3:
v = QVariant::fromValue(QVector3D(normal->f[0], normal->f[1], normal->f[2]));
break;
case olive::NodeValue::k_vec4:
v = QVariant::fromValue(QVector4D(normal->f[0], normal->f[1], normal->f[2], normal->f[3]));
break;
case olive::NodeValue::k_bezier:
v = QVariant::fromValue(
Bezier(normal->f[0], normal->f[1], normal->f[2], normal->f[3], normal->den, normal->num));
break;
default:
return OAKENGINE_E_INVALID;
}
QVector<QVariant> split = olive::NodeValue::split_normal_value_into_track_values(type, v);
int n = qMin(split.size(), track_count);
for (int i = 0; i < n; ++i) {
tracks_out[i].f[0] = 0;
tracks_out[i].num = 0;
tracks_out[i].den = 0;
switch (type) {
case olive::NodeValue::k_int:
tracks_out[i].type = OAK_NODE_VALUE_INT;
tracks_out[i].num = split[i].toLongLong();
break;
case olive::NodeValue::k_combo:
tracks_out[i].type = OAK_NODE_VALUE_COMBO;
tracks_out[i].num = split[i].toLongLong();
break;
case olive::NodeValue::k_boolean:
tracks_out[i].type = OAK_NODE_VALUE_BOOL;
tracks_out[i].num = split[i].toBool() ? 1 : 0;
break;
case olive::NodeValue::k_rational: {
olive::Rational r = split[i].value<olive::Rational>();
tracks_out[i].type = OAK_NODE_VALUE_RATIONAL;
tracks_out[i].num = r.numerator();
tracks_out[i].den = r.denominator();
break;
}
default:
tracks_out[i].type = OAK_NODE_VALUE_FLOAT;
tracks_out[i].f[0] = split[i].toDouble();
break;
}
}
return OAKENGINE_OK;
}
int oakengine_node_value_combine_tracks(int c_type,
const oak_node_value *tracks, int track_count, oak_node_value *normal_out)
{
if (!tracks || !normal_out || track_count <= 0) {
return OAKENGINE_E_INVALID;
}
olive::NodeValue::Type type = from_c_type(c_type);
QVector<QVariant> split;
split.reserve(track_count);
for (int i = 0; i < track_count; ++i) {
switch (type) {
case olive::NodeValue::k_int:
case olive::NodeValue::k_combo:
split.append(QVariant(static_cast<int>(tracks[i].num)));
break;
case olive::NodeValue::k_boolean:
split.append(tracks[i].num != 0);
break;
case olive::NodeValue::k_rational:
split.append(QVariant::fromValue(
olive::Rational(tracks[i].num, tracks[i].den)));
break;
default:
split.append(tracks[i].f[0]);
break;
}
}
QVariant combined = olive::NodeValue::combine_track_values_into_normal_value(type, split);
if (!normal_out) {
return OAKENGINE_OK;
}
switch (type) {
case olive::NodeValue::k_int:
case olive::NodeValue::k_combo:
normal_out->type = OAK_NODE_VALUE_INT;
normal_out->num = combined.toLongLong();
normal_out->f[0] = 0;
break;
case olive::NodeValue::k_float:
normal_out->type = OAK_NODE_VALUE_FLOAT;
normal_out->f[0] = combined.toFloat();
normal_out->num = 0;
break;
case olive::NodeValue::k_boolean:
normal_out->type = OAK_NODE_VALUE_BOOL;
normal_out->num = combined.toBool() ? 1 : 0;
normal_out->f[0] = 0;
break;
case olive::NodeValue::k_rational: {
olive::Rational r = combined.value<olive::Rational>();
normal_out->type = OAK_NODE_VALUE_RATIONAL;
normal_out->num = r.numerator();
normal_out->den = r.denominator();
break;
}
case olive::NodeValue::k_color: {
olive::core::Color c = combined.value<olive::core::Color>();
normal_out->type = OAK_NODE_VALUE_COLOR;
normal_out->f[0] = c.red();
normal_out->f[1] = c.green();
normal_out->f[2] = c.blue();
normal_out->f[3] = c.alpha();
break;
}
case olive::NodeValue::k_vec2: {
QVector2D v = combined.value<QVector2D>();
normal_out->type = OAK_NODE_VALUE_VEC2;
normal_out->f[0] = v.x();
normal_out->f[1] = v.y();
break;
}
case olive::NodeValue::k_vec3: {
QVector3D v = combined.value<QVector3D>();
normal_out->type = OAK_NODE_VALUE_VEC3;
normal_out->f[0] = v.x();
normal_out->f[1] = v.y();
normal_out->f[2] = v.z();
break;
}
case olive::NodeValue::k_vec4: {
QVector4D v = combined.value<QVector4D>();
normal_out->type = OAK_NODE_VALUE_VEC4;
normal_out->f[0] = v.x();
normal_out->f[1] = v.y();
normal_out->f[2] = v.z();
normal_out->f[3] = v.w();
break;
}
case olive::NodeValue::k_bezier: {
Bezier b = combined.value<Bezier>();
normal_out->type = OAK_NODE_VALUE_BEZIER;
normal_out->f[0] = b.x();
normal_out->f[1] = b.y();
normal_out->f[2] = b.cp1_x();
normal_out->f[3] = b.cp1_y();
break;
}
default:
normal_out->type = OAK_NODE_VALUE_NONE;
normal_out->num = 0;
normal_out->den = 0;
normal_out->f[0] = 0;
break;
}
return OAKENGINE_OK;
}
} // extern "C"