- app/ no longer includes engine C++ headers nor holds engine C++ types: engine access goes through the oakengine C ABI plus C++ wrappers (oakutil/oaknode.h, oakutil/oakvideo.h) and app-local mirror types (tooltypes, trackreferencehandle, timelinecommonapp, keyframetypes, subtitleapp, serializedlayoutinfoapp, nodevaluehandle, sliderdisplaytypeapp) - engine: new C ABI functions for block/track/clip/transition navigation and predicates, links, caches, waveform/playback, disk folder, sequence_track_list, node_free, footage_is_valid, block_get_track, get_brush; loadotio/saveotio ported to the current engine API - OTIO is now a required dependency: CI and CD build it on every platform, FindOpenTimelineIO fixed for OTIO 0.16/0.19 (the old deps include requirement silently disabled OTIO everywhere), runtime libraries are bundled into packages and copied next to macOS binaries (oak_copy_otio_runtime) - fix ProjectViewModel drag&drop mime read/write size mismatch (segfault) - unify color label naming (k_olive -> "Oak") in the app-side mirror - docs: OTIO required, FFmpeg minimum corrected to 6.0 (en/zh) - gtest suite: 1925 passed, 0 failed
1037 lines
32 KiB
C++
1037 lines
32 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/>.
|
|
|
|
***/
|
|
|
|
#ifndef OAKUTIL_OAKNODE_H
|
|
#define OAKUTIL_OAKNODE_H
|
|
|
|
#include <QBrush>
|
|
#include <QPointF>
|
|
#include <QString>
|
|
#include <QVariant>
|
|
#include <QVector>
|
|
#include <QHash>
|
|
|
|
#include "oakengine/node.h"
|
|
#include "oakengine/project.h"
|
|
#include "oakengine/viewer.h"
|
|
#include "oakengine/footage.h"
|
|
#include "oakengine/undo.h"
|
|
|
|
namespace oak
|
|
{
|
|
|
|
/**
|
|
* @file oaknode.h
|
|
* @brief C++ wrapper layer over the engine's pure C ABI (oakengine_*).
|
|
*
|
|
* The engine boundary is a flat C API so that it can be reimplemented in Rust
|
|
* and consumed through FFI. Calling that flat API directly from the UI layer
|
|
* is verbose, error-prone and destroys RAII / object semantics. This header
|
|
* re-wraps the C ABI into lightweight C++ value types so that application code
|
|
* can keep a natural, object-oriented shape:
|
|
*
|
|
* @code
|
|
* oak::Node item = ...;
|
|
* QString label = item.get_label();
|
|
* oak::Node parent = item.folder();
|
|
* @endcode
|
|
*
|
|
* Semantics:
|
|
* - A wrapper is a *borrowed, non-owning* view over an opaque engine handle
|
|
* (OakEngineNode* / OakEngineProject*). Copying a wrapper copies the
|
|
* handle, not the underlying object. Lifetime follows the engine object.
|
|
* - A default-constructed wrapper holds nullptr and is "null"; it is safe to
|
|
* call any method on a null wrapper (it yields a no-op / empty result,
|
|
* exactly like the underlying C ABI which accepts NULL handles).
|
|
* - Identity is the handle address, so wrappers work as QHash keys and in
|
|
* pointer-style comparisons (a == b, a != nullptr).
|
|
*
|
|
* The wrapper lives in namespace `oak` (deliberately distinct from the
|
|
* engine's internal `olive` namespace, which is still visible in parts of the
|
|
* application through coreengine.h; keeping the names separate avoids clashes).
|
|
*
|
|
* Application code should reach the engine through these wrappers rather than
|
|
* calling oakengine_*() directly; direct C ABI calls in app/ are exceptions
|
|
* that must be justified and registered (see docs/zh/plans/r8-p3-node-param-abi.md).
|
|
*/
|
|
|
|
class Node;
|
|
class Project;
|
|
class Footage;
|
|
class Input;
|
|
class Keyframe;
|
|
|
|
/**
|
|
* @brief Mirror of engine's olive::Node::CategoryID ordinals
|
|
* (engine/node/node.h). Values must stay in sync with the engine enum;
|
|
* oakengine_node_category_at() returns these ordinals directly.
|
|
*/
|
|
enum NodeCategory {
|
|
k_category_unknown = -1,
|
|
k_category_output,
|
|
k_category_generator,
|
|
k_category_math,
|
|
k_category_keying,
|
|
k_category_filter,
|
|
k_category_color,
|
|
k_category_time,
|
|
k_category_timeline,
|
|
k_category_transition,
|
|
k_category_distort,
|
|
k_category_project,
|
|
k_category_open_fx,
|
|
k_category_count
|
|
};
|
|
|
|
struct NodeConnection;
|
|
struct ContextNodeItem;
|
|
|
|
/**
|
|
* @brief Wrapper around an opaque OakEngineNode handle.
|
|
*/
|
|
class Node
|
|
{
|
|
public:
|
|
/// Construct a null node.
|
|
Node() = default;
|
|
|
|
/// Wrap a borrowed engine handle (no ownership transfer).
|
|
Node(OakEngineNode *h) : h_(h) {}
|
|
Node(OakEngineProject *) = delete; // avoid accidental wrong-handle wrapping
|
|
|
|
/// The underlying borrowed handle (for C ABI interop / Qt internalPointer).
|
|
OakEngineNode *handle() const { return h_; }
|
|
|
|
/// Implicit conversion to the raw handle for boundary interop.
|
|
operator OakEngineNode *() const { return h_; }
|
|
|
|
/// True when the wrapper holds no handle.
|
|
bool is_null() const { return h_ == nullptr; }
|
|
|
|
/* ---- Identity -------------------------------------------------------- */
|
|
|
|
bool operator==(const Node &o) const { return h_ == o.h_; }
|
|
bool operator!=(const Node &o) const { return h_ != o.h_; }
|
|
bool operator==(std::nullptr_t) const { return h_ == nullptr; }
|
|
bool operator!=(std::nullptr_t) const { return h_ != nullptr; }
|
|
|
|
/* ---- Type queries ---------------------------------------------------- */
|
|
|
|
QString id() const { return str([this](char *b, int n) { return oakengine_node_get_type_id(h_, b, n); }); }
|
|
QString name() const { return str([this](char *b, int n) { return oakengine_node_get_name(h_, b, n); }); }
|
|
QString get_label() const { return str([this](char *b, int n) { return oakengine_node_get_label(h_, b, n); }); }
|
|
QString description() const { return str([this](char *b, int n) { return oakengine_node_get_description(h_, b, n); }); }
|
|
QString short_name() const { return str([this](char *b, int n) { return oakengine_node_get_short_name(h_, b, n); }); }
|
|
QString label_and_name() const { return str([this](char *b, int n) { return oakengine_node_get_label_and_name(h_, b, n); }); }
|
|
|
|
bool is_folder() const { return oakengine_node_is_folder(h_) != 0; }
|
|
bool is_item() const { return oakengine_node_is_item(h_) != 0; }
|
|
bool is_footage() const { return oakengine_node_is_footage(h_) != 0; }
|
|
bool is_sequence() const { return oakengine_node_is_sequence(h_) != 0; }
|
|
bool is_track() const { return oakengine_node_is_track(h_) != 0; }
|
|
bool is_clip() const { return oakengine_node_is_clip(h_) != 0; }
|
|
bool is_group() const { return oakengine_node_is_group(h_) != 0; }
|
|
bool is_multicam() const { return oakengine_node_is_multicam(h_) != 0; }
|
|
bool is_viewer_output() const { return oakengine_node_is_viewer_output(h_) != 0; }
|
|
|
|
int color_label() const { return oakengine_node_get_color_label(h_); }
|
|
int effective_color_label() const { return oakengine_node_get_effective_color_label(h_); }
|
|
|
|
/// The node's flags (OR-combination of engine Node::Flag values).
|
|
uint64_t flags() const { return oakengine_node_get_flags(h_); }
|
|
|
|
/// Re-run the node's translation pass (Node::retranslate()).
|
|
void retranslate() const { oakengine_node_retranslate(h_); }
|
|
|
|
/**
|
|
* @brief Standalone copy of this node (Node::copy()).
|
|
*
|
|
* NOTE: unlike everything else on this wrapper, the returned handle is
|
|
* OWNED by the caller (not added to any project). Hand it on to a
|
|
* project/undo command or release it through the C ABI.
|
|
*/
|
|
Node create_copy() const { return Node(oakengine_node_create_copy(h_)); }
|
|
|
|
/// The node's title-bar brush (Node::brush()); Qt type crosses the ABI
|
|
/// as an opaque out-pointer (same precedent as the C ABI it wraps).
|
|
QBrush brush(double top, double bottom) const
|
|
{
|
|
QBrush b;
|
|
oakengine_node_get_brush(h_, top, bottom, &b);
|
|
return b;
|
|
}
|
|
|
|
/* ---- Factory library (static) ----------------------------------------- */
|
|
|
|
/// Number of registered node prototypes in the engine's NodeFactory.
|
|
static int factory_count() { return oakengine_node_factory_id_count(); }
|
|
|
|
/// Borrowed prototype node at `index` (read-only metadata queries only).
|
|
static Node factory_node_at(int index) { return Node(oakengine_node_factory_node_at(index)); }
|
|
|
|
/// Value of the engine's Node::k_dont_show_in_create_menu flag.
|
|
static uint64_t flag_dont_show_in_create_menu() { return oakengine_node_flag_dont_show_in_create_menu(); }
|
|
|
|
/// Translated display name of a NodeCategory ordinal (Node::get_category_name()).
|
|
static QString category_name(int category_id)
|
|
{
|
|
char buf[256];
|
|
buf[0] = '\0';
|
|
oakengine_node_category_name(category_id, buf, sizeof(buf));
|
|
return QString::fromUtf8(buf);
|
|
}
|
|
|
|
/* ---- Categories -------------------------------------------------------- */
|
|
|
|
int category_count() const { return oakengine_node_category_count(h_); }
|
|
|
|
/// NodeCategory ordinal at `index` (-1 when out of range).
|
|
int category_at(int index) const { return oakengine_node_category_at(h_, index); }
|
|
|
|
/// True when `category` is in this node's category list.
|
|
bool in_category(NodeCategory category) const
|
|
{
|
|
const int n = category_count();
|
|
for (int i = 0; i < n; i++) {
|
|
if (category_at(i) == category) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
QString sub_category() const { return str([this](char *b, int n) { return oakengine_node_get_sub_category(h_, b, n); }); }
|
|
|
|
/* ---- Graph navigation -------------------------------------------------- */
|
|
|
|
Node folder() const { return Node(oakengine_node_folder(h_)); }
|
|
Project project() const; // defined after Project
|
|
|
|
/**
|
|
* @brief Node::data() equivalent. Returns a QVariant mirroring the engine
|
|
* type: QString for icon/tooltip, qint64 for created/modified time, and an
|
|
* invalid QVariant when the engine returns no value.
|
|
*
|
|
* Role ordinals match olive::Node::DataType: 0=icon, 1=duration,
|
|
* 2=created_time, 3=modified_time, 4=frequency_rate, 5=tooltip.
|
|
*/
|
|
QVariant data(int role) const
|
|
{
|
|
int type = 0;
|
|
int64_t iv = 0;
|
|
char buf[4096];
|
|
buf[0] = '\0';
|
|
oakengine_node_get_data(h_, role, &type, &iv, buf, sizeof(buf));
|
|
switch (type) {
|
|
case 1:
|
|
return QString::fromUtf8(buf);
|
|
case 2:
|
|
return QVariant(qint64(iv));
|
|
default:
|
|
return QVariant();
|
|
}
|
|
}
|
|
|
|
/* ---- Folder operations (valid when is_folder()) ------------------------ */
|
|
|
|
int item_child_count() const { return oakengine_folder_item_child_count(h_); }
|
|
Node item_child(int index) const { return Node(oakengine_folder_item_child(h_, index)); }
|
|
int index_of_child(const Node &child) const { return oakengine_folder_index_of_child(h_, child.h_); }
|
|
bool has_child_recursive(const Node &child) const { return oakengine_folder_has_child_recursive(h_, child.h_) != 0; }
|
|
|
|
/* ---- ViewerOutput operations (valid when is_viewer_output()) ----------- */
|
|
|
|
int enabled_stream_count() const { return oakengine_viewer_get_enabled_stream_count(h_); }
|
|
|
|
/* ---- Label mutation ---------------------------------------------------- */
|
|
|
|
void set_label(const QString &label) const { oakengine_node_set_label(h_, label.toUtf8().constData()); }
|
|
|
|
/* ---- Inputs ------------------------------------------------------------ */
|
|
|
|
/// Number of declared inputs (array elements not counted separately).
|
|
int input_count() const { return oakengine_node_input_count(h_); }
|
|
|
|
/// The input id at `index` (empty when out of range).
|
|
QString input_id(int index) const
|
|
{
|
|
char buf[256];
|
|
buf[0] = '\0';
|
|
oakengine_node_input_id(h_, index, buf, sizeof(buf));
|
|
return QString::fromUtf8(buf);
|
|
}
|
|
|
|
/// All declared inputs of this node.
|
|
QVector<Input> inputs() const; // defined after Input
|
|
|
|
/// This node's effect input (typically the texture input); null when none.
|
|
Input effect_input() const; // defined after Input
|
|
|
|
/* ---- Topology ----------------------------------------------------------- */
|
|
|
|
/// True when this node (recursively, when requested) receives input from `other`.
|
|
bool inputs_from(const Node &other, bool recursive) const
|
|
{
|
|
return oakengine_node_inputs_from(h_, other.h_, recursive ? 1 : 0) != 0;
|
|
}
|
|
|
|
int output_connection_count() const { return oakengine_node_output_connection_count(h_); }
|
|
|
|
/// The receiving node of the output connection at `index` (null if out of range).
|
|
Node output_connection_node(int index) const
|
|
{
|
|
OakEngineNode *n = nullptr;
|
|
char buf[256];
|
|
int element = 0;
|
|
oakengine_node_output_connection_at(h_, index, &n, buf, sizeof(buf), &element);
|
|
return Node(n);
|
|
}
|
|
|
|
/// Full output connection info at `index` (including hidden flag).
|
|
NodeConnection output_connection_at_ex(int index) const; // defined after NodeConnection is complete
|
|
|
|
/// Total number of input connections on this node (flat enumeration).
|
|
int input_connection_count_all() const { return oakengine_node_input_connection_count_all(h_); }
|
|
|
|
/// Full input connection info at flat `index` (including source node).
|
|
NodeConnection input_connection_at_all(int index) const; // defined after NodeConnection is complete
|
|
|
|
/* ---- Exclusive dependencies (Node::get_exclusive_dependencies()) -------- */
|
|
|
|
int exclusive_dependency_count() const { return oakengine_node_get_exclusive_dependency_count(h_); }
|
|
Node exclusive_dependency_at(int index) const { return Node(oakengine_node_get_exclusive_dependency_at(h_, index)); }
|
|
|
|
/* ---- Context operations (valid when this node is a context, e.g. group) -- */
|
|
|
|
int context_node_count() const { return oakengine_node_context_node_count(h_); }
|
|
|
|
/// The child at `index` with its graph position and expanded flag.
|
|
ContextNodeItem context_node_at(int index) const; // defined after ContextNodeItem is complete
|
|
|
|
/// Graph position of `node` inside this context; false when not found.
|
|
bool context_position_of(const Node &node, QPointF *pos, bool *expanded = nullptr) const
|
|
{
|
|
double x = 0, y = 0;
|
|
int exp = 0;
|
|
if (oakengine_node_get_context_position(h_, node.h_, &x, &y, &exp) != 0) {
|
|
return false;
|
|
}
|
|
if (pos) {
|
|
*pos = QPointF(x, y);
|
|
}
|
|
if (expanded) {
|
|
*expanded = exp != 0;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/// Set the graph position of `node` inside this context (undoable).
|
|
void set_context_position_of(const Node &node, const QPointF &pos) const
|
|
{
|
|
oakengine_node_set_context_position(h_, node.h_, pos.x(), pos.y());
|
|
}
|
|
|
|
/// Set the expanded flag of `node` inside this context (undoable).
|
|
void set_context_expanded_of(const Node &node, bool expanded) const
|
|
{
|
|
oakengine_node_set_context_expanded(h_, node.h_, expanded ? 1 : 0);
|
|
}
|
|
|
|
/* ---- Plugin ------------------------------------------------------------- */
|
|
|
|
bool has_plugin() const { return oakengine_node_has_plugin(h_) != 0; }
|
|
int plugin_message_count() const { return oakengine_node_plugin_message_count(h_); }
|
|
|
|
/// Plugin persistent message text at `index`; *type receives
|
|
/// 0=error, 1=warning, 2=message. Empty string when out of range.
|
|
QString plugin_message_at(int index, int *type = nullptr) const
|
|
{
|
|
char buf[2048];
|
|
buf[0] = '\0';
|
|
int t = 0;
|
|
if (oakengine_node_plugin_message_at(h_, index, &t, buf, sizeof(buf)) != 0) {
|
|
return QString();
|
|
}
|
|
if (type) {
|
|
*type = t;
|
|
}
|
|
return QString::fromUtf8(buf);
|
|
}
|
|
|
|
/// Clear all persistent messages on the node's plugin instance.
|
|
void clear_plugin_messages() const { oakengine_node_plugin_clear_messages(h_); }
|
|
|
|
/* ---- Footage view (valid when is_footage()) ----------------------------- */
|
|
|
|
/// Borrow this node as a Footage; returns a null Footage when not footage.
|
|
Footage as_footage() const; // defined after Footage
|
|
|
|
/// Enabled stream references as (track_type, index) pairs.
|
|
QVector<QPair<int, int>> enabled_streams() const
|
|
{
|
|
QVector<QPair<int, int>> out;
|
|
const int n = oakengine_viewer_get_enabled_stream_count(h_);
|
|
if (n <= 0) {
|
|
return out;
|
|
}
|
|
QVector<int> types(n), indices(n);
|
|
const int got = oakengine_viewer_get_enabled_streams(h_, types.data(), indices.data(), n);
|
|
out.reserve(got);
|
|
for (int i = 0; i < got; i++) {
|
|
out.append({ types[i], indices[i] });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
private:
|
|
/// Helper: run a buf/size C ABI getter and return the result as QString.
|
|
template <typename Fn>
|
|
QString str(Fn fn) const
|
|
{
|
|
char buf[1024];
|
|
buf[0] = '\0';
|
|
fn(buf, sizeof(buf));
|
|
return QString::fromUtf8(buf);
|
|
}
|
|
|
|
OakEngineNode *h_ = nullptr;
|
|
};
|
|
|
|
/**
|
|
* @brief One edge of the node graph: a receiving (input) side plus,
|
|
* for input-side enumeration, the feeding (output) source node.
|
|
*
|
|
* Returned by value from Node::output_connection_at_ex() and
|
|
* Node::input_connection_at_all().
|
|
*/
|
|
struct NodeConnection {
|
|
Node node; ///< The node owning the receiving input.
|
|
QString input_id; ///< Id of the receiving input on `node`.
|
|
int element = -1; ///< Array element of the receiving input (-1 = whole input).
|
|
bool hidden = false; ///< Whether the receiving input is hidden.
|
|
Node source_node; ///< Input-side enumeration only: the node feeding the input.
|
|
};
|
|
|
|
/**
|
|
* @brief A node's placement inside a context (graph position + expanded flag).
|
|
*/
|
|
struct ContextNodeItem {
|
|
Node node;
|
|
QPointF position;
|
|
bool expanded = false;
|
|
};
|
|
|
|
/**
|
|
* @brief Wrapper around an opaque OakEngineProject handle.
|
|
*
|
|
* Like Node this is a borrowed view; project *ownership* (create/free) is
|
|
* managed elsewhere (the application core). This wrapper only provides
|
|
* query/navigation access.
|
|
*/
|
|
class Project
|
|
{
|
|
public:
|
|
Project() = default;
|
|
Project(OakEngineProject *h) : h_(h) {}
|
|
|
|
OakEngineProject *handle() const { return h_; }
|
|
operator OakEngineProject *() const { return h_; }
|
|
|
|
bool is_null() const { return h_ == nullptr; }
|
|
|
|
bool operator==(const Project &o) const { return h_ == o.h_; }
|
|
bool operator!=(const Project &o) const { return h_ != o.h_; }
|
|
bool operator==(std::nullptr_t) const { return h_ == nullptr; }
|
|
bool operator!=(std::nullptr_t) const { return h_ != nullptr; }
|
|
|
|
/// Root folder node of the project (Project::root()).
|
|
Node root() const { return Node(oakengine_project_root(h_)); }
|
|
|
|
QString name() const
|
|
{
|
|
char buf[1024];
|
|
buf[0] = '\0';
|
|
oakengine_project_name(h_, buf, sizeof(buf));
|
|
return QString::fromUtf8(buf);
|
|
}
|
|
|
|
private:
|
|
OakEngineProject *h_ = nullptr;
|
|
};
|
|
|
|
inline Project Node::project() const
|
|
{
|
|
return Project(oakengine_node_get_project(h_));
|
|
}
|
|
|
|
/**
|
|
* @brief RAII wrapper around a borrowed OakEngineFootage handle.
|
|
*
|
|
* Unlike Node/Project (non-owning views), a Footage wrapper *owns* the
|
|
* borrowed handle and releases it with oakengine_footage_free() on
|
|
* destruction, mirroring the engine's borrow/free contract. Copying is
|
|
* disabled; move transfers ownership.
|
|
*/
|
|
class Footage
|
|
{
|
|
public:
|
|
Footage() = default;
|
|
|
|
/// Take ownership of a borrowed handle (from oakengine_footage_borrow()).
|
|
explicit Footage(OakEngineFootage *h) : h_(h) {}
|
|
|
|
/// Borrow the footage view of a node (null when the node is not footage).
|
|
static Footage borrow(OakEngineNode *node) { return Footage(oakengine_footage_borrow(node)); }
|
|
|
|
~Footage() { reset(); }
|
|
|
|
Footage(const Footage &) = delete;
|
|
Footage &operator=(const Footage &) = delete;
|
|
|
|
Footage(Footage &&o) noexcept : h_(o.h_) { o.h_ = nullptr; }
|
|
Footage &operator=(Footage &&o) noexcept
|
|
{
|
|
if (this != &o) {
|
|
reset();
|
|
h_ = o.h_;
|
|
o.h_ = nullptr;
|
|
}
|
|
return *this;
|
|
}
|
|
|
|
OakEngineFootage *handle() const { return h_; }
|
|
bool is_null() const { return h_ == nullptr; }
|
|
explicit operator bool() const { return h_ != nullptr; }
|
|
|
|
QString filename() const { return str([this](char *b, int n) { return oakengine_footage_get_filename(h_, b, n); }); }
|
|
QString proxy_path() const { return str([this](char *b, int n) { return oakengine_footage_proxy_get_path(h_, b, n); }); }
|
|
bool proxy_enabled() const { return oakengine_footage_proxy_is_enabled(h_) != 0; }
|
|
void set_proxy_enabled(bool e) const { oakengine_footage_proxy_set_enabled(h_, e ? 1 : 0); }
|
|
void invalidate() const { oakengine_footage_invalidate(h_); }
|
|
int relink(const QString &path) const { return oakengine_footage_relink(h_, path.toUtf8().constData()); }
|
|
int proxy_delete() const { return oakengine_footage_proxy_delete(h_); }
|
|
|
|
static QString last_error()
|
|
{
|
|
char buf[512];
|
|
buf[0] = '\0';
|
|
oakengine_footage_last_error(buf, sizeof(buf));
|
|
return QString::fromUtf8(buf);
|
|
}
|
|
|
|
private:
|
|
void reset()
|
|
{
|
|
if (h_) {
|
|
oakengine_footage_free(h_);
|
|
h_ = nullptr;
|
|
}
|
|
}
|
|
|
|
template <typename Fn>
|
|
QString str(Fn fn) const
|
|
{
|
|
char buf[4096];
|
|
buf[0] = '\0';
|
|
fn(buf, sizeof(buf));
|
|
return QString::fromUtf8(buf);
|
|
}
|
|
|
|
OakEngineFootage *h_ = nullptr;
|
|
};
|
|
|
|
inline Footage Node::as_footage() const
|
|
{
|
|
return Footage::borrow(h_);
|
|
}
|
|
|
|
/**
|
|
* @brief Value type identifying a specific input (parameter) on a node.
|
|
*
|
|
* Mirrors the engine's olive::NodeInput (engine/node/param.h) but stores an
|
|
* opaque OakEngineNode* handle instead of the internal Node* pointer.
|
|
* Lightweight and copyable; suitable for QVariant storage and QHash keys.
|
|
*/
|
|
class Input
|
|
{
|
|
public:
|
|
Input() : node_(nullptr), element_(-1) {}
|
|
|
|
Input(OakEngineNode *node, const QString &input_id, int element = -1)
|
|
: node_(node), input_(input_id), element_(element) {}
|
|
|
|
/* ---- Accessors --------------------------------------------------------- */
|
|
|
|
OakEngineNode *node_handle() const { return node_; }
|
|
Node node() const { return Node(node_); }
|
|
const QString &input_id() const { return input_; }
|
|
int element() const { return element_; }
|
|
|
|
void set_node_handle(OakEngineNode *n) { node_ = n; }
|
|
void set_input_id(const QString &id) { input_ = id; }
|
|
void set_element(int e) { element_ = e; }
|
|
|
|
/* ---- Validity ----------------------------------------------------------- */
|
|
|
|
bool is_valid() const
|
|
{
|
|
return node_ && !input_.isEmpty() && element_ >= -1;
|
|
}
|
|
|
|
/* ---- Comparison (for QHash / containers) -------------------------------- */
|
|
|
|
bool operator==(const Input &o) const
|
|
{
|
|
return node_ == o.node_ && input_ == o.input_ && element_ == o.element_;
|
|
}
|
|
bool operator!=(const Input &o) const { return !(*this == o); }
|
|
bool operator<(const Input &o) const
|
|
{
|
|
if (node_ != o.node_) return node_ < o.node_;
|
|
if (input_ != o.input_) return input_ < o.input_;
|
|
return element_ < o.element_;
|
|
}
|
|
|
|
/* ---- C ABI delegated queries -------------------------------------------- */
|
|
|
|
QString name() const
|
|
{
|
|
char buf[1024];
|
|
buf[0] = '\0';
|
|
oakengine_node_get_input_name(node_, input_.toUtf8().constData(), buf, sizeof(buf));
|
|
return QString::fromUtf8(buf);
|
|
}
|
|
|
|
bool is_hidden() const
|
|
{
|
|
return oakengine_node_input_is_hidden(node_, input_.toUtf8().constData()) != 0;
|
|
}
|
|
|
|
bool is_connected() const
|
|
{
|
|
return oakengine_node_input_is_connected(node_, input_.toUtf8().constData()) != 0;
|
|
}
|
|
|
|
bool is_connectable() const
|
|
{
|
|
return oakengine_node_input_is_connectable(node_, input_.toUtf8().constData()) != 0;
|
|
}
|
|
|
|
bool is_keyframing() const
|
|
{
|
|
return oakengine_node_input_is_keyframed_ex(node_, input_.toUtf8().constData(), element_) != 0;
|
|
}
|
|
|
|
bool is_array() const
|
|
{
|
|
return oakengine_node_input_is_array(node_, input_.toUtf8().constData()) != 0;
|
|
}
|
|
|
|
int array_size() const
|
|
{
|
|
return oakengine_node_input_array_size(node_, input_.toUtf8().constData());
|
|
}
|
|
|
|
int flags() const
|
|
{
|
|
return oakengine_node_input_get_flags(node_, input_.toUtf8().constData());
|
|
}
|
|
|
|
/// NodeValue::Type enum ordinal (engine-internal numbering).
|
|
int data_type() const
|
|
{
|
|
return oakengine_node_input_get_data_type(node_, input_.toUtf8().constData());
|
|
}
|
|
|
|
/// oak_node_value_type (C ABI type enum) for this input.
|
|
int c_type() const
|
|
{
|
|
return oakengine_node_input_get_type(node_, input_.toUtf8().constData());
|
|
}
|
|
|
|
/// Number of keyframe tracks for this input's data type.
|
|
int keyframe_track_count() const
|
|
{
|
|
return oakengine_node_value_keyframe_track_count(c_type());
|
|
}
|
|
|
|
bool is_keyframable() const
|
|
{
|
|
return oakengine_node_input_is_keyframable(node_, input_.toUtf8().constData());
|
|
}
|
|
|
|
Node connected_node() const
|
|
{
|
|
return Node(oakengine_node_input_get_connected_node(node_, input_.toUtf8().constData(), element_));
|
|
}
|
|
|
|
/// Remove the edge feeding this input (undoable).
|
|
bool disconnect() const
|
|
{
|
|
return oakengine_node_disconnect_ex(node_, input_.toUtf8().constData(), element_) == 0;
|
|
}
|
|
|
|
/// Read an integer input property; true on success.
|
|
bool property_int(const char *key, int64_t *out) const
|
|
{
|
|
return oakengine_node_input_get_property_int(node_, input_.toUtf8().constData(), key, out) == 0;
|
|
}
|
|
|
|
/// True if the input has a property with the given key.
|
|
bool has_property(const char *key) const
|
|
{
|
|
return oakengine_node_input_has_property(node_, input_.toUtf8().constData(), key) != 0;
|
|
}
|
|
|
|
/// Read a numeric input property as a double (-1 track = whole value); true on success.
|
|
bool property_number(const char *key, int track, double *out) const
|
|
{
|
|
return oakengine_node_input_get_property_number(node_, input_.toUtf8().constData(), key, track, out) == 0;
|
|
}
|
|
|
|
/// Keyframes at a rational time across all tracks of this input.
|
|
QVector<Keyframe> keyframes_at_time(int64_t time_num, int64_t time_den) const; // defined after Keyframe
|
|
|
|
/// Earliest keyframe time on this input; false when no keyframes.
|
|
bool earliest_keyframe_time(int64_t *num, int64_t *den) const
|
|
{
|
|
return oakengine_node_keyframe_earliest_time(node_, input_.toUtf8().constData(), element_, num, den) != 0;
|
|
}
|
|
|
|
/// Latest keyframe time on this input; false when no keyframes.
|
|
bool latest_keyframe_time(int64_t *num, int64_t *den) const
|
|
{
|
|
return oakengine_node_keyframe_latest_time(node_, input_.toUtf8().constData(), element_, num, den) != 0;
|
|
}
|
|
|
|
void reset() { *this = Input(); }
|
|
|
|
private:
|
|
OakEngineNode *node_;
|
|
QString input_;
|
|
int element_;
|
|
};
|
|
|
|
inline QVector<Input> Node::inputs() const
|
|
{
|
|
QVector<Input> out;
|
|
const int n = input_count();
|
|
out.reserve(n);
|
|
for (int i = 0; i < n; i++) {
|
|
out.append(Input(h_, input_id(i)));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
inline Input Node::effect_input() const
|
|
{
|
|
char buf[256];
|
|
buf[0] = '\0';
|
|
int element = -1;
|
|
if (oakengine_node_get_effect_input(h_, buf, sizeof(buf), &element) != 0) {
|
|
return Input();
|
|
}
|
|
return Input(h_, QString::fromUtf8(buf), element);
|
|
}
|
|
|
|
/**
|
|
* @brief Wrapper around an opaque OakEngineKeyframe handle (borrowed view).
|
|
*/
|
|
class Keyframe
|
|
{
|
|
public:
|
|
Keyframe() = default;
|
|
|
|
/// Wrap a borrowed engine handle (no ownership transfer).
|
|
Keyframe(OakEngineKeyframe *h) : h_(h) {}
|
|
|
|
OakEngineKeyframe *handle() const { return h_; }
|
|
operator OakEngineKeyframe *() const { return h_; }
|
|
|
|
bool is_null() const { return h_ == nullptr; }
|
|
|
|
bool operator==(const Keyframe &o) const { return h_ == o.h_; }
|
|
bool operator!=(const Keyframe &o) const { return h_ != o.h_; }
|
|
bool operator==(std::nullptr_t) const { return h_ == nullptr; }
|
|
bool operator!=(std::nullptr_t) const { return h_ != nullptr; }
|
|
|
|
/// Node that owns this keyframe.
|
|
Node node() const { return Node(oakengine_keyframe_get_node(h_)); }
|
|
|
|
/// Id of the input that owns this keyframe.
|
|
QString input_id() const
|
|
{
|
|
char buf[256];
|
|
buf[0] = '\0';
|
|
oakengine_keyframe_get_input_id(h_, buf, sizeof(buf));
|
|
return QString::fromUtf8(buf);
|
|
}
|
|
|
|
int element() const { return oakengine_keyframe_get_element(h_); }
|
|
int track() const { return oakengine_keyframe_get_track(h_); }
|
|
|
|
/// The keyframe's time as a rational (num/den).
|
|
void time(int64_t *num, int64_t *den) const { oakengine_keyframe_get_time(h_, num, den); }
|
|
|
|
/// Easing type (0=linear, 1=bezier, 2=hold; -1 on null).
|
|
int type() const { return oakengine_keyframe_get_type(h_); }
|
|
|
|
/// The keyframe's value on its track (POD; convert via app/common/oakvaluehelper.h).
|
|
oak_node_value value() const
|
|
{
|
|
oak_node_value v{};
|
|
oakengine_keyframe_get_value(h_, &v);
|
|
return v;
|
|
}
|
|
|
|
/// Bezier control point (0 = in-handle, 1 = out-handle).
|
|
QPointF bezier_point(int point_index) const
|
|
{
|
|
double x = 0, y = 0;
|
|
oakengine_keyframe_get_bezier_point(h_, point_index, &x, &y);
|
|
return QPointF(x, y);
|
|
}
|
|
|
|
/// Bezier control point, or the identity point for non-bezier keyframes.
|
|
QPointF valid_bezier_point(int point_index) const
|
|
{
|
|
double x = 0, y = 0;
|
|
oakengine_keyframe_get_valid_bezier_point(h_, point_index, &x, &y);
|
|
return QPointF(x, y);
|
|
}
|
|
|
|
/// True if a sibling keyframe exists at `time_ts` on a different track of the same input.
|
|
bool has_sibling_at_time(int64_t time_ts, int track) const
|
|
{
|
|
return oakengine_keyframe_has_sibling_at_time(h_, time_ts, track) != 0;
|
|
}
|
|
|
|
private:
|
|
OakEngineKeyframe *h_ = nullptr;
|
|
};
|
|
|
|
/**
|
|
* @brief Value type identifying a specific keyframe track on a node input.
|
|
*
|
|
* Mirrors the engine's olive::NodeKeyframeTrackReference. A track index of
|
|
* -1 means "the whole input" (no specific track selected).
|
|
*/
|
|
class KeyframeTrackRef
|
|
{
|
|
public:
|
|
KeyframeTrackRef() : track_(-1) {}
|
|
|
|
KeyframeTrackRef(const Input &input, int track = 0)
|
|
: input_(input), track_(track) {}
|
|
|
|
const Input &input() const { return input_; }
|
|
int track() const { return track_; }
|
|
|
|
bool is_valid() const { return input_.is_valid() && track_ >= 0; }
|
|
|
|
bool operator==(const KeyframeTrackRef &o) const
|
|
{
|
|
return input_ == o.input_ && track_ == o.track_;
|
|
}
|
|
bool operator!=(const KeyframeTrackRef &o) const { return !(*this == o); }
|
|
|
|
/// Number of keyframes on this track.
|
|
int keyframe_count() const
|
|
{
|
|
if (!is_valid()) {
|
|
return 0;
|
|
}
|
|
return oakengine_node_keyframe_count_on_track(
|
|
input_.node_handle(), input_.input_id().toUtf8().constData(),
|
|
input_.element(), track_);
|
|
}
|
|
|
|
/// Borrowed keyframe at on-track `index` (null when out of range).
|
|
Keyframe keyframe_at(int index) const
|
|
{
|
|
if (!is_valid()) {
|
|
return Keyframe();
|
|
}
|
|
return Keyframe(oakengine_node_keyframe_handle_on_track(
|
|
input_.node_handle(), input_.input_id().toUtf8().constData(),
|
|
input_.element(), track_, index));
|
|
}
|
|
|
|
/// All keyframes on this track.
|
|
QVector<Keyframe> keyframes() const
|
|
{
|
|
QVector<Keyframe> out;
|
|
const int n = keyframe_count();
|
|
out.reserve(n);
|
|
for (int i = 0; i < n; i++) {
|
|
out.append(keyframe_at(i));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
void reset() { *this = KeyframeTrackRef(); }
|
|
|
|
private:
|
|
Input input_;
|
|
int track_;
|
|
};
|
|
|
|
/**
|
|
* @brief Value type identifying an input on a node without an array element.
|
|
*
|
|
* Mirrors the engine's olive::NodeInputPair ({node, input} — NO element);
|
|
* used as the array-widget key.
|
|
*/
|
|
class InputPair
|
|
{
|
|
public:
|
|
InputPair() = default;
|
|
|
|
InputPair(OakEngineNode *node, const QString &input_id)
|
|
: node_(node), input_(input_id) {}
|
|
|
|
OakEngineNode *node_handle() const { return node_; }
|
|
Node node() const { return Node(node_); }
|
|
const QString &input_id() const { return input_; }
|
|
|
|
bool operator==(const InputPair &o) const
|
|
{
|
|
return node_ == o.node_ && input_ == o.input_;
|
|
}
|
|
bool operator!=(const InputPair &o) const { return !(*this == o); }
|
|
|
|
private:
|
|
OakEngineNode *node_ = nullptr;
|
|
QString input_;
|
|
};
|
|
|
|
/* ---- Out-of-line Node methods needing complete helper types ----------------- */
|
|
|
|
inline NodeConnection Node::output_connection_at_ex(int index) const
|
|
{
|
|
NodeConnection c;
|
|
char buf[256];
|
|
buf[0] = '\0';
|
|
OakEngineNode *n = nullptr;
|
|
int element = -1;
|
|
int hidden = 0;
|
|
if (oakengine_node_output_connection_at_ex(h_, index, &n, buf, sizeof(buf), &element, &hidden) != 0) {
|
|
return c;
|
|
}
|
|
c.node = Node(n);
|
|
c.input_id = QString::fromUtf8(buf);
|
|
c.element = element;
|
|
c.hidden = hidden != 0;
|
|
return c;
|
|
}
|
|
|
|
inline NodeConnection Node::input_connection_at_all(int index) const
|
|
{
|
|
NodeConnection c;
|
|
char buf[256];
|
|
buf[0] = '\0';
|
|
OakEngineNode *n = nullptr;
|
|
OakEngineNode *src = nullptr;
|
|
int element = -1;
|
|
int hidden = 0;
|
|
if (oakengine_node_input_connection_at_all(h_, index, &n, buf, sizeof(buf), &element, &src, &hidden) != 0) {
|
|
return c;
|
|
}
|
|
c.node = Node(n);
|
|
c.input_id = QString::fromUtf8(buf);
|
|
c.element = element;
|
|
c.hidden = hidden != 0;
|
|
c.source_node = Node(src);
|
|
return c;
|
|
}
|
|
|
|
inline ContextNodeItem Node::context_node_at(int index) const
|
|
{
|
|
ContextNodeItem item;
|
|
double x = 0, y = 0;
|
|
int exp = 0;
|
|
item.node = Node(oakengine_node_context_node_at(h_, index, &x, &y, &exp));
|
|
item.position = QPointF(x, y);
|
|
item.expanded = exp != 0;
|
|
return item;
|
|
}
|
|
|
|
inline QVector<Keyframe> Input::keyframes_at_time(int64_t time_num, int64_t time_den) const
|
|
{
|
|
QVector<Keyframe> out;
|
|
if (!is_valid()) {
|
|
return out;
|
|
}
|
|
QVector<OakEngineKeyframe *> buf(128);
|
|
int n = oakengine_node_keyframes_at_time(
|
|
node_, input_.toUtf8().constData(), element_, time_num, time_den,
|
|
buf.data(), buf.size());
|
|
if (n == buf.size()) {
|
|
// Rare: more keyframes at one instant than the first buffer held
|
|
buf.resize(4096);
|
|
n = oakengine_node_keyframes_at_time(
|
|
node_, input_.toUtf8().constData(), element_, time_num, time_den,
|
|
buf.data(), buf.size());
|
|
}
|
|
out.reserve(n);
|
|
for (int i = 0; i < n; i++) {
|
|
out.append(Keyframe(buf.at(i)));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/// qHash support so wrappers can be used as QHash keys.
|
|
inline uint qHash(const Node &n, uint seed = 0)
|
|
{
|
|
return ::qHash(reinterpret_cast<quintptr>(n.handle()), seed);
|
|
}
|
|
inline uint qHash(const Project &p, uint seed = 0)
|
|
{
|
|
return ::qHash(reinterpret_cast<quintptr>(p.handle()), seed);
|
|
}
|
|
inline uint qHash(const Input &i, uint seed = 0)
|
|
{
|
|
uint h = ::qHash(reinterpret_cast<quintptr>(i.node_handle()), seed);
|
|
h ^= ::qHash(i.input_id(), seed);
|
|
h ^= ::qHash(i.element(), seed);
|
|
return h;
|
|
}
|
|
inline uint qHash(const Keyframe &k, uint seed = 0)
|
|
{
|
|
return ::qHash(reinterpret_cast<quintptr>(k.handle()), seed);
|
|
}
|
|
inline uint qHash(const KeyframeTrackRef &r, uint seed = 0)
|
|
{
|
|
return qHash(r.input(), seed) ^ ::qHash(r.track(), seed);
|
|
}
|
|
inline uint qHash(const InputPair &p, uint seed = 0)
|
|
{
|
|
return ::qHash(reinterpret_cast<quintptr>(p.node_handle()), seed) ^
|
|
::qHash(p.input_id(), seed);
|
|
}
|
|
|
|
} // namespace oak
|
|
|
|
Q_DECLARE_METATYPE(oak::Input)
|
|
Q_DECLARE_METATYPE(oak::KeyframeTrackRef)
|
|
Q_DECLARE_METATYPE(oak::InputPair)
|
|
|
|
#endif // OAKUTIL_OAKNODE_H
|