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:
@@ -301,4 +301,9 @@ if (BUILD_TESTS)
|
||||
if (TARGET olive-render-worker)
|
||||
add_dependencies(oakengine_export_test olive-render-worker)
|
||||
endif ()
|
||||
|
||||
make_oakengine_test(oakengine_node_test)
|
||||
target_compile_definitions(oakengine_node_test PRIVATE
|
||||
OAK_TEST_SOURCE_DIR="${CMAKE_SOURCE_DIR}"
|
||||
)
|
||||
endif ()
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
/***
|
||||
|
||||
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/>.
|
||||
|
||||
***/
|
||||
|
||||
#ifndef OAKENGINE_NODE_H
|
||||
#define OAKENGINE_NODE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "export.h"
|
||||
#include "init.h"
|
||||
#include "project.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @file node.h
|
||||
* @brief C ABI for the node graph: enumeration, metadata, input
|
||||
* introspection, parameter access and edge editing
|
||||
*
|
||||
* An OakEngineNode wraps the engine's olive::Node (engine/node/node.h).
|
||||
* Handles are borrowed views of nodes owned by their project (QObject
|
||||
* parent chain); they become invalid when the project is freed or the node
|
||||
* is removed (e.g. by undoing oakengine_project_add_node()).
|
||||
*
|
||||
* Parameter values cross the boundary as the small POD oak_node_value;
|
||||
* which of its fields are meaningful depends on the oak_node_value_type
|
||||
* (see the enum). String-typed inputs (NodeValue::k_file) do not fit the
|
||||
* POD and use the dedicated oakengine_node_get/set_input_string() pair
|
||||
* (buf/size convention).
|
||||
*
|
||||
* Every mutating call is undoable through the global undo stack, like the
|
||||
* timeline editing primitives (direct non-undoable application when the
|
||||
* engine is not initialized). Errors follow the family model: negative
|
||||
* OAKENGINE_E_* codes, NULL handles as no-ops, and a thread-local
|
||||
* human-readable reason via oakengine_node_last_error().
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Value type of an oak_node_value / a node input.
|
||||
*
|
||||
* Mirrors the data-carrying subset of olive::NodeValue::Type
|
||||
* (engine/node/value.h): combo maps to k_combo, STRING maps to k_file
|
||||
* (string-typed inputs handled by the dedicated string functions). Input
|
||||
* types without a POD representation (texture, samples, params, bezier,
|
||||
* binary, ...) report as OAK_NODE_VALUE_NONE.
|
||||
*/
|
||||
typedef enum oak_node_value_type {
|
||||
OAK_NODE_VALUE_NONE = 0,
|
||||
OAK_NODE_VALUE_INT, /**< num (olive k_int) */
|
||||
OAK_NODE_VALUE_FLOAT, /**< f[0] (olive k_float) */
|
||||
OAK_NODE_VALUE_BOOL, /**< num 0/1 (olive k_boolean) */
|
||||
OAK_NODE_VALUE_RATIONAL, /**< num/den (olive k_rational) */
|
||||
OAK_NODE_VALUE_COLOR, /**< f[0..3] = r,g,b,a (olive k_color) */
|
||||
OAK_NODE_VALUE_VEC2, /**< f[0..1] (olive k_vec2) */
|
||||
OAK_NODE_VALUE_VEC3, /**< f[0..2] (olive k_vec3) */
|
||||
OAK_NODE_VALUE_VEC4, /**< f[0..3] (olive k_vec4) */
|
||||
OAK_NODE_VALUE_COMBO, /**< num = selected index (olive k_combo) */
|
||||
OAK_NODE_VALUE_STRING /**< k_file; string APIs only, never in the POD */
|
||||
} oak_node_value_type;
|
||||
|
||||
/**
|
||||
* @brief POD parameter value. Only the fields documented for the value's
|
||||
* `type` are meaningful.
|
||||
*/
|
||||
typedef struct oak_node_value {
|
||||
int type; /**< oak_node_value_type. */
|
||||
int64_t num; /**< INT/COMBO value, BOOL 0/1, RATIONAL numerator. */
|
||||
int64_t den; /**< RATIONAL denominator. */
|
||||
double f[4]; /**< FLOAT f[0]; VEC2/3/4 f[0..n-1]; COLOR r,g,b,a. */
|
||||
} oak_node_value;
|
||||
|
||||
/**
|
||||
* @brief Opaque node handle (borrowed from the owning project).
|
||||
*/
|
||||
typedef struct OakEngineNode OakEngineNode;
|
||||
|
||||
/**
|
||||
* @brief Human-readable reason for the last failed node call on this
|
||||
* thread (buf/size convention).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_node_last_error(char *buf, int buf_size);
|
||||
|
||||
/* ---- Enumeration --------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @brief Number of nodes in the project's graph (Project::nodes()).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_project_node_count(const OakEngineProject *self);
|
||||
|
||||
/**
|
||||
* @brief Borrowed handle of the node at `index`, or NULL when out of range.
|
||||
*/
|
||||
OAKENGINE_API OakEngineNode *
|
||||
oakengine_project_node_at(const OakEngineProject *self, int index);
|
||||
|
||||
/* ---- Metadata -------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @brief The node's type id (Node::id(), e.g.
|
||||
* "org.olivevideoeditor.Olive.solidgenerator"). buf/size convention.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_node_get_type_id(const OakEngineNode *self,
|
||||
char *buf, int buf_size);
|
||||
|
||||
/**
|
||||
* @brief The node's display name (Node::name(), translated).
|
||||
* buf/size convention.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_node_get_name(const OakEngineNode *self,
|
||||
char *buf, int buf_size);
|
||||
|
||||
/**
|
||||
* @brief The node's user label (Node::get_label()). buf/size convention.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_node_get_label(const OakEngineNode *self,
|
||||
char *buf, int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Set the node's user label (undoable, olive::NodeRenameCommand).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_node_set_label(OakEngineNode *self,
|
||||
const char *label);
|
||||
|
||||
/* ---- Input introspection ---------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @brief Number of declared inputs (Node::inputs(); array elements are not
|
||||
* counted separately).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_node_input_count(const OakEngineNode *self);
|
||||
|
||||
/**
|
||||
* @brief The input id at `index` (Node::inputs()). buf/size convention;
|
||||
* returns OAKENGINE_E_NOT_FOUND for an out-of-range index.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_node_input_id(const OakEngineNode *self,
|
||||
int index, char *buf, int buf_size);
|
||||
|
||||
/**
|
||||
* @brief The input's value type as oak_node_value_type.
|
||||
*
|
||||
* Unknown ids and inputs whose NodeValue::Type has no POD representation
|
||||
* (texture, samples, params, ...) report OAK_NODE_VALUE_NONE; k_file
|
||||
* reports OAK_NODE_VALUE_STRING.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_node_input_get_type(const OakEngineNode *self,
|
||||
const char *input_id);
|
||||
|
||||
/**
|
||||
* @brief 1 if the input currently has a connected edge
|
||||
* (Node::is_input_connected()).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_node_input_is_connected(
|
||||
const OakEngineNode *self, const char *input_id);
|
||||
|
||||
/* ---- Parameter access -------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @brief Read an input's standard value (Node::get_standard_value())
|
||||
* mapped into `out`.
|
||||
*
|
||||
* String (k_file) inputs fail with OAKENGINE_E_INVALID -- use
|
||||
* oakengine_node_get_input_string(). Types without a POD representation
|
||||
* fail with OAKENGINE_E_NOT_FOUND; a missing input id fails with
|
||||
* OAKENGINE_E_NOT_FOUND as well.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_node_get_input(const OakEngineNode *self,
|
||||
const char *input_id,
|
||||
oak_node_value *out);
|
||||
|
||||
/**
|
||||
* @brief Write an input's standard value (undoable,
|
||||
* olive::NodeParamSetStandardValueCommand).
|
||||
*
|
||||
* `v->type` must equal the input's declared type (STRING is rejected --
|
||||
* use oakengine_node_set_input_string()); a type mismatch or an unknown
|
||||
* input id returns OAKENGINE_E_INVALID / OAKENGINE_E_NOT_FOUND.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_node_set_input(OakEngineNode *self,
|
||||
const char *input_id,
|
||||
const oak_node_value *v);
|
||||
|
||||
/**
|
||||
* @brief Read a string-typed (k_file) input's value (buf/size convention).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_node_get_input_string(const OakEngineNode *self,
|
||||
const char *input_id,
|
||||
char *buf, int buf_size);
|
||||
|
||||
/**
|
||||
* @brief Write a string-typed (k_file) input's value (undoable).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_node_set_input_string(OakEngineNode *self,
|
||||
const char *input_id,
|
||||
const char *s);
|
||||
|
||||
/* ---- Graph editing ------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* @brief Create a node of `type_id` in the project (undoable:
|
||||
* NodeFactory::create_from_id() + olive::NodeAddCommand).
|
||||
*
|
||||
* Returns the borrowed node handle, or NULL when `type_id` is not a
|
||||
* registered node id (see oakengine_node_last_error()).
|
||||
*/
|
||||
OAKENGINE_API OakEngineNode *
|
||||
oakengine_project_add_node(OakEngineProject *project, const char *type_id);
|
||||
|
||||
/**
|
||||
* @brief Remove a node from the project, disconnecting its edges
|
||||
* (undoable, olive::NodeRemoveAndDisconnectCommand).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_project_remove_node(OakEngineProject *project,
|
||||
OakEngineNode *node);
|
||||
|
||||
/**
|
||||
* @brief Connect `output_node`'s output into `input_node`'s `input_id`
|
||||
* (undoable, olive::NodeEdgeAddCommand).
|
||||
*
|
||||
* Fails with OAKENGINE_E_INVALID when the input is not connectable or the
|
||||
* id is unknown, and with OAKENGINE_E_STATE when the input is already
|
||||
* connected (disconnect first).
|
||||
*/
|
||||
OAKENGINE_API int oakengine_node_connect(OakEngineNode *output_node,
|
||||
OakEngineNode *input_node,
|
||||
const char *input_id);
|
||||
|
||||
/**
|
||||
* @brief Remove the edge feeding `input_node`'s `input_id` (undoable,
|
||||
* olive::NodeEdgeRemoveCommand). OAKENGINE_E_NOT_FOUND when not connected.
|
||||
*/
|
||||
OAKENGINE_API int oakengine_node_disconnect(OakEngineNode *input_node,
|
||||
const char *input_id);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* OAKENGINE_NODE_H */
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
@@ -0,0 +1,374 @@
|
||||
/***
|
||||
|
||||
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/>.
|
||||
|
||||
***/
|
||||
|
||||
// Pure C ABI test for the liboakengine node-graph facade: enumeration,
|
||||
// metadata, input introspection, parameter get/set (POD + string), edge
|
||||
// connect/disconnect and node add/remove -- all through their undo/redo
|
||||
// behavior too. Uses the Solid generator and the OCIO LUT nodes as
|
||||
// fixtures. No GL required.
|
||||
|
||||
#include <assert.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <direct.h>
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include "oakengine/init.h"
|
||||
#include "oakengine/node.h"
|
||||
#include "oakengine/project.h"
|
||||
#include "oakengine/timeline.h"
|
||||
|
||||
#ifndef OAK_TEST_SOURCE_DIR
|
||||
#define OAK_TEST_SOURCE_DIR "."
|
||||
#endif
|
||||
|
||||
static char g_tmpdir[4096];
|
||||
|
||||
static void make_tmpdir(void)
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
char base[MAX_PATH];
|
||||
const DWORD len = GetTempPathA(MAX_PATH, base);
|
||||
assert(len > 0 && len < MAX_PATH);
|
||||
snprintf(g_tmpdir, sizeof(g_tmpdir), "%soakengine_node_test_%lu", base,
|
||||
(unsigned long)GetCurrentProcessId());
|
||||
assert(_mkdir(g_tmpdir) == 0);
|
||||
#else
|
||||
strcpy(g_tmpdir, "/tmp/oakengine_node_test_XXXXXX");
|
||||
assert(mkdtemp(g_tmpdir) != NULL);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void test_enumeration(OakEngineProject *project)
|
||||
{
|
||||
// A fresh project holds at least its root folder node.
|
||||
const int count = oakengine_project_node_count(project);
|
||||
assert(count >= 1);
|
||||
assert(oakengine_project_node_at(project, 0) != NULL);
|
||||
assert(oakengine_project_node_at(project, count - 1) != NULL);
|
||||
assert(oakengine_project_node_at(project, count) == NULL);
|
||||
assert(oakengine_project_node_at(project, -1) == NULL);
|
||||
|
||||
// NULL safety.
|
||||
assert(oakengine_project_node_count(NULL) == 0);
|
||||
assert(oakengine_project_node_at(NULL, 0) == NULL);
|
||||
assert(oakengine_node_get_type_id(NULL, NULL, 0) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_node_get_name(NULL, NULL, 0) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_node_get_label(NULL, NULL, 0) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_node_set_label(NULL, "x") == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_node_input_count(NULL) == 0);
|
||||
assert(oakengine_node_input_id(NULL, 0, NULL, 0) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_node_input_get_type(NULL, "x") == OAK_NODE_VALUE_NONE);
|
||||
assert(oakengine_node_input_is_connected(NULL, "x") == 0);
|
||||
oak_node_value v;
|
||||
memset(&v, 0, sizeof(v));
|
||||
assert(oakengine_node_get_input(NULL, "x", &v) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_node_set_input(NULL, "x", &v) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_node_get_input_string(NULL, "x", NULL, 0) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_node_set_input_string(NULL, "x", "y") ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_project_add_node(NULL, "x") == NULL);
|
||||
assert(oakengine_project_remove_node(NULL, NULL) == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_node_connect(NULL, NULL, "x") == OAKENGINE_E_INVALID);
|
||||
assert(oakengine_node_disconnect(NULL, "x") == OAKENGINE_E_INVALID);
|
||||
}
|
||||
|
||||
static void test_add_and_metadata(OakEngineProject *project,
|
||||
OakEngineNode **solid_out,
|
||||
OakEngineNode **lut_out)
|
||||
{
|
||||
const int before = oakengine_project_node_count(project);
|
||||
char buf[256];
|
||||
|
||||
OakEngineNode *solid = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.solidgenerator");
|
||||
assert(solid != NULL);
|
||||
assert(oakengine_project_node_count(project) == before + 1);
|
||||
|
||||
assert(oakengine_node_get_type_id(solid, buf, sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "org.olivevideoeditor.Olive.solidgenerator") == 0);
|
||||
assert(oakengine_node_get_name(solid, buf, sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "Solid") == 0);
|
||||
|
||||
// Unknown type id fails with a reason.
|
||||
assert(oakengine_project_add_node(project, "org.example.nonexistent") ==
|
||||
NULL);
|
||||
char err[256];
|
||||
assert(oakengine_node_last_error(err, sizeof(err)) > 0);
|
||||
|
||||
// Label round-trip with undo/redo.
|
||||
assert(oakengine_node_get_label(solid, buf, sizeof(buf)) >= 0);
|
||||
assert(oakengine_node_set_label(solid, "MySolid") == OAKENGINE_OK);
|
||||
assert(oakengine_node_get_label(solid, buf, sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "MySolid") == 0);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_node_get_label(solid, buf, sizeof(buf)) >= 0);
|
||||
assert(strcmp(buf, "MySolid") != 0);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_node_get_label(solid, buf, sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "MySolid") == 0);
|
||||
|
||||
OakEngineNode *lut = oakengine_project_add_node(
|
||||
project, "org.olivevideoeditor.Olive.ociolut");
|
||||
assert(lut != NULL);
|
||||
|
||||
*solid_out = solid;
|
||||
*lut_out = lut;
|
||||
}
|
||||
|
||||
static void test_inputs_and_params(OakEngineProject *project,
|
||||
OakEngineNode *solid, OakEngineNode *lut)
|
||||
{
|
||||
char buf[256];
|
||||
|
||||
// Introspection on the Solid generator: the Node base class provides
|
||||
// "enabled_in" (BOOL), the generator itself adds "color_in" (COLOR).
|
||||
const int inputs = oakengine_node_input_count(solid);
|
||||
assert(inputs >= 2);
|
||||
assert(oakengine_node_input_id(solid, 0, buf, sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "enabled_in") == 0);
|
||||
assert(oakengine_node_input_id(solid, 1, buf, sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "color_in") == 0);
|
||||
assert(oakengine_node_input_id(solid, inputs, buf, sizeof(buf)) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
assert(oakengine_node_input_get_type(solid, "enabled_in") ==
|
||||
OAK_NODE_VALUE_BOOL);
|
||||
assert(oakengine_node_input_get_type(solid, "color_in") ==
|
||||
OAK_NODE_VALUE_COLOR);
|
||||
assert(oakengine_node_input_get_type(solid, "no_such_input") ==
|
||||
OAK_NODE_VALUE_NONE);
|
||||
assert(oakengine_node_input_is_connected(solid, "color_in") == 0);
|
||||
|
||||
// BOOL get/set on the base-class input (defaults to true).
|
||||
oak_node_value v;
|
||||
memset(&v, 0, sizeof(v));
|
||||
assert(oakengine_node_get_input(solid, "enabled_in", &v) ==
|
||||
OAKENGINE_OK);
|
||||
assert(v.type == OAK_NODE_VALUE_BOOL && v.num == 1);
|
||||
v.num = 0;
|
||||
assert(oakengine_node_set_input(solid, "enabled_in", &v) ==
|
||||
OAKENGINE_OK);
|
||||
memset(&v, 0, sizeof(v));
|
||||
assert(oakengine_node_get_input(solid, "enabled_in", &v) ==
|
||||
OAKENGINE_OK);
|
||||
assert(v.num == 0);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_node_get_input(solid, "enabled_in", &v) ==
|
||||
OAKENGINE_OK);
|
||||
assert(v.num == 1);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
|
||||
// COLOR get/set round-trip (Solid defaults to opaque red).
|
||||
memset(&v, 0, sizeof(v));
|
||||
assert(oakengine_node_get_input(solid, "color_in", &v) == OAKENGINE_OK);
|
||||
assert(v.type == OAK_NODE_VALUE_COLOR);
|
||||
assert(fabs(v.f[0] - 1.0) < 1e-6 && fabs(v.f[3] - 1.0) < 1e-6);
|
||||
|
||||
v.f[0] = 0.2;
|
||||
v.f[1] = 0.4;
|
||||
v.f[2] = 0.6;
|
||||
v.f[3] = 1.0;
|
||||
assert(oakengine_node_set_input(solid, "color_in", &v) == OAKENGINE_OK);
|
||||
memset(&v, 0, sizeof(v));
|
||||
assert(oakengine_node_get_input(solid, "color_in", &v) == OAKENGINE_OK);
|
||||
assert(fabs(v.f[0] - 0.2) < 1e-6 && fabs(v.f[1] - 0.4) < 1e-6 &&
|
||||
fabs(v.f[2] - 0.6) < 1e-6);
|
||||
|
||||
// Undo restores the default, redo applies it again.
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_node_get_input(solid, "color_in", &v) == OAKENGINE_OK);
|
||||
assert(fabs(v.f[0] - 1.0) < 1e-6);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_node_get_input(solid, "color_in", &v) == OAKENGINE_OK);
|
||||
assert(fabs(v.f[0] - 0.2) < 1e-6);
|
||||
|
||||
// Type errors: INT into a COLOR input, POD into a string input, POD and
|
||||
// string APIs on an unknown id.
|
||||
memset(&v, 0, sizeof(v));
|
||||
v.type = OAK_NODE_VALUE_INT;
|
||||
v.num = 3;
|
||||
assert(oakengine_node_set_input(solid, "color_in", &v) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
v.type = OAK_NODE_VALUE_COLOR;
|
||||
assert(oakengine_node_set_input(solid, "no_such_input", &v) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
assert(oakengine_node_get_input(solid, "no_such_input", &v) ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
assert(oakengine_node_set_input_string(solid, "color_in", "x") ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_node_get_input_string(solid, "color_in", buf,
|
||||
sizeof(buf)) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// STRING (k_file) on the LUT node: empty by default, round-trip + undo.
|
||||
assert(oakengine_node_input_get_type(lut, "lut_file_in") ==
|
||||
OAK_NODE_VALUE_STRING);
|
||||
assert(oakengine_node_get_input_string(lut, "lut_file_in", buf,
|
||||
sizeof(buf)) == 0);
|
||||
assert(oakengine_node_set_input_string(lut, "lut_file_in",
|
||||
"/tmp/x.cube") == OAKENGINE_OK);
|
||||
assert(oakengine_node_get_input_string(lut, "lut_file_in", buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "/tmp/x.cube") == 0);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_node_get_input_string(lut, "lut_file_in", buf,
|
||||
sizeof(buf)) == 0);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_node_get_input_string(lut, "lut_file_in", buf,
|
||||
sizeof(buf)) > 0);
|
||||
assert(strcmp(buf, "/tmp/x.cube") == 0);
|
||||
|
||||
// String typed inputs are rejected by the POD pair.
|
||||
memset(&v, 0, sizeof(v));
|
||||
assert(oakengine_node_get_input(lut, "lut_file_in", &v) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
v.type = OAK_NODE_VALUE_STRING;
|
||||
assert(oakengine_node_set_input(lut, "lut_file_in", &v) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
|
||||
// COMBO on the LUT direction input: 0 by default, set 1, undo.
|
||||
assert(oakengine_node_input_get_type(lut, "lut_dir_in") ==
|
||||
OAK_NODE_VALUE_COMBO);
|
||||
memset(&v, 0, sizeof(v));
|
||||
assert(oakengine_node_get_input(lut, "lut_dir_in", &v) == OAKENGINE_OK);
|
||||
assert(v.type == OAK_NODE_VALUE_COMBO && v.num == 0);
|
||||
v.num = 1;
|
||||
assert(oakengine_node_set_input(lut, "lut_dir_in", &v) == OAKENGINE_OK);
|
||||
memset(&v, 0, sizeof(v));
|
||||
assert(oakengine_node_get_input(lut, "lut_dir_in", &v) == OAKENGINE_OK);
|
||||
assert(v.num == 1);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_node_get_input(lut, "lut_dir_in", &v) == OAKENGINE_OK);
|
||||
assert(v.num == 0);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
}
|
||||
|
||||
static void test_edges(OakEngineProject *project, OakEngineNode *solid,
|
||||
OakEngineNode *lut)
|
||||
{
|
||||
char err[256];
|
||||
|
||||
// Solid's texture output -> LUT's "tex_in".
|
||||
assert(oakengine_node_input_is_connected(lut, "tex_in") == 0);
|
||||
assert(oakengine_node_connect(solid, lut, "tex_in") == OAKENGINE_OK);
|
||||
assert(oakengine_node_input_is_connected(lut, "tex_in") == 1);
|
||||
|
||||
// Already-connected input is refused; unknown ids and unconnectable
|
||||
// inputs fail.
|
||||
assert(oakengine_node_connect(solid, lut, "tex_in") ==
|
||||
OAKENGINE_E_STATE);
|
||||
assert(oakengine_node_connect(solid, lut, "no_such_input") ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
assert(oakengine_node_connect(solid, lut, "lut_file_in") ==
|
||||
OAKENGINE_E_INVALID);
|
||||
assert(oakengine_node_last_error(err, sizeof(err)) > 0);
|
||||
|
||||
// Disconnect restores the unconnected state; a second disconnect fails.
|
||||
assert(oakengine_node_disconnect(lut, "tex_in") == OAKENGINE_OK);
|
||||
assert(oakengine_node_input_is_connected(lut, "tex_in") == 0);
|
||||
assert(oakengine_node_disconnect(lut, "tex_in") ==
|
||||
OAKENGINE_E_NOT_FOUND);
|
||||
|
||||
// Undo/redo the disconnect and the connect: undo brings the connection
|
||||
// back, undo again removes it; redoing both replays connect then
|
||||
// disconnect, so the end state is disconnected.
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_node_input_is_connected(lut, "tex_in") == 1);
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_node_input_is_connected(lut, "tex_in") == 0);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_node_input_is_connected(lut, "tex_in") == 1);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_node_input_is_connected(lut, "tex_in") == 0);
|
||||
}
|
||||
|
||||
static void test_remove(OakEngineProject *project, OakEngineNode *solid,
|
||||
OakEngineNode *lut)
|
||||
{
|
||||
const int before = oakengine_project_node_count(project);
|
||||
|
||||
// A node from another project is refused.
|
||||
OakEngineProject *other = oakengine_project_create();
|
||||
assert(other != NULL);
|
||||
assert(oakengine_project_new(other) == OAKENGINE_OK);
|
||||
assert(oakengine_project_remove_node(other, solid) ==
|
||||
OAKENGINE_E_INVALID);
|
||||
oakengine_project_free(other);
|
||||
|
||||
assert(oakengine_project_remove_node(project, lut) == OAKENGINE_OK);
|
||||
assert(oakengine_project_node_count(project) == before - 1);
|
||||
|
||||
// Undo brings the node back, redo removes it again.
|
||||
assert(oakengine_project_undo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_project_node_count(project) == before);
|
||||
assert(oakengine_project_redo(project) == OAKENGINE_OK);
|
||||
assert(oakengine_project_node_count(project) == before - 1);
|
||||
|
||||
(void)solid;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
make_tmpdir();
|
||||
|
||||
// Sandbox the config/cache/data locations (see oakengine_init_test).
|
||||
#if !defined(_WIN32)
|
||||
assert(setenv("XDG_CONFIG_HOME", g_tmpdir, 1) == 0);
|
||||
assert(setenv("XDG_CACHE_HOME", g_tmpdir, 1) == 0);
|
||||
assert(setenv("XDG_DATA_HOME", g_tmpdir, 1) == 0);
|
||||
#endif
|
||||
|
||||
assert(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);
|
||||
|
||||
OakEngineProject *project = oakengine_project_create();
|
||||
assert(project != NULL);
|
||||
assert(oakengine_project_new(project) == OAKENGINE_OK);
|
||||
|
||||
test_enumeration(project);
|
||||
|
||||
OakEngineNode *solid = NULL, *lut = NULL;
|
||||
test_add_and_metadata(project, &solid, &lut);
|
||||
test_inputs_and_params(project, solid, lut);
|
||||
test_edges(project, solid, lut);
|
||||
test_remove(project, solid, lut);
|
||||
|
||||
// Graph nodes are not timeline clips: a sequence's track list stays
|
||||
// empty no matter what the project graph holds.
|
||||
OakEngineSequence *seq = oakengine_sequence_new(project, "Seq");
|
||||
assert(seq != NULL);
|
||||
int video = -1, audio = -1, subtitle = -1;
|
||||
assert(oakengine_sequence_track_count(seq, &video, &audio, &subtitle) ==
|
||||
OAKENGINE_OK);
|
||||
assert(video == 0 && audio == 0 && subtitle == 0);
|
||||
|
||||
oakengine_project_free(project);
|
||||
assert(oakengine_shutdown() == OAKENGINE_OK);
|
||||
|
||||
printf("oakengine_node_test: all assertions passed\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user