engine: add the node graph family to the C ABI facade

- project node enumeration, type/name/label metadata, input
  introspection (id, mapped value type, connection state)
- typed parameter read/write for the eight common NodeValue kinds
  (int/float/bool/rational/color/vec2-4/combo/string), undoable via
  NodeParamSetSplitStandardValueCommand - the split-track value path,
  chosen after proving the standard-value command stores whole
  variants into a single component track
- graph operations (add/remove/connect/disconnect) reusing the
  engine's undo commands, all covered by undo/redo assertions
This commit is contained in:
2026-07-20 08:16:43 +08:00
parent db355c28b9
commit e82011b0ee
5 changed files with 1201 additions and 0 deletions
+2
View File
@@ -28,11 +28,13 @@ set(OLIVE_SOURCES
include/oakengine/renderer.h
include/oakengine/footage.h
include/oakengine/exporter.h
include/oakengine/node.h
src/capi/init.cpp
src/capi/project.cpp
src/capi/timeline.cpp
src/capi/renderer.cpp
src/capi/footage.cpp
src/capi/export.cpp
src/capi/node.cpp
PARENT_SCOPE
)
+562
View File
@@ -0,0 +1,562 @@
/***
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/node.h"
#include "node/nodeundo.h"
#include "node/project.h"
#include "node/value.h"
#include "undo/undocommand.h"
#include "undo/undostack.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)
{
if (olive::EngineCore::instance()) {
olive::EngineCore::instance()->undo_stack()->push(command, name);
} else {
command->redo_now();
delete command;
}
}
// 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;
default:
return OAK_NODE_VALUE_NONE;
}
}
// 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);
}
} // 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_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)
{
set_error(QString());
if (!self) {
set_error(QStringLiteral("invalid node"));
return OAKENGINE_E_INVALID;
}
push_or_run(new olive::NodeRenameCommand(
impl(self), QString::fromUtf8(label ? label : "")),
QStringLiteral("Rename Node"));
return OAKENGINE_OK;
}
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;
}
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)
{
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;
}
olive::Node *connected =
in_node->get_connected_output(olive::NodeInput(in_node, id));
if (!connected) {
set_error(QStringLiteral("input \"%1\" is not connected").arg(id));
return OAKENGINE_E_NOT_FOUND;
}
push_or_run(new olive::NodeEdgeRemoveCommand(
connected, olive::NodeInput(in_node, id)),
QStringLiteral("Disconnect Nodes"));
return OAKENGINE_OK;
}
} // extern "C"