Files
oak-editor/crates/oak-node/src/nodes/group.rs
T
Mike-Solar 244d5e860f
CI / Build & test (Windows) (push) Failing after 7s
workspace: kebab-case crates, app under crates/oak-app, shared versions
All crates take the oak-* kebab-case naming (oak-audio, oak-codec,
oak-common, oak-core, oak-ffmpeg-link, oak-node, oak-otio, oak-plugin,
oak-render, oak-storage, oak-task, oak-timeline, oak-undo), with the
lib identifiers rewritten (oakrender:: -> oak_render::, oakcore_rs:: ->
oak_core::, ...) across all 226 referencing files.

The GUI application moves from the workspace root into
crates/oak-app/: src/, build.rs (paths fixed for the new location) and
tests/ travel with it, the root Cargo.toml becomes workspace-only
([workspace] + workspace.package + profiles), and the app package
inherits the workspace version. The screenshots example becomes a
standalone crate examples/simple_player/ with its own Cargo.toml.

Every crate now inherits the single workspace version
(version.workspace = true), and the workflows' crate paths and the
build docs follow the renames.

Validated with a clean cargo check --workspace.
2026-08-22 16:58:37 +08:00

350 lines
11 KiB
Rust

// Oak Video Editor - 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/>.
//! Node group (C++ `src/node/src/group/group.{h,cpp}`,
//! `olive::NodeGroup`).
//!
//! A group wraps an inner node context and exposes selected inner
//! inputs as its own ("passthroughs"). The C++ class also declares the
//! undo commands `NodeGroupAddInputPassthrough` /
//! `NodeGroupSetOutputPassthrough`; the C ABI's undoable variants
//! (`include/node/group.h`) build equivalent vtable commands through
//! oakundo commands in the ops layer.
use crate::factory::NodeMeta;
use crate::graph::Graph;
use crate::id::NodeId;
use crate::node::{Category, NodeBehavior, NodeCore};
/// Reference to an input of a node inside the group's context
/// (C++ `NodeInput`: node pointer + input id + array element).
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InnerInput {
/// The inner node this input belongs to.
pub node: NodeId,
/// Input id on the inner node.
pub input: String,
/// Array element index (-1/0 for non-array inputs).
pub element: i32,
}
/// One passthrough entry (C++ `NodeGroup::InputPassthrough`):
/// the group's own input id mapped to the inner input it mirrors.
pub type InputPassthrough = (String, InnerInput);
/// Node group node. Exposes inner-node inputs as its own inputs and
/// forwards its output from a designated inner node.
///
/// The C++ `output_passthrough_` is a raw `Node*`; here it is the
/// inner node's [`NodeId`] (`None` = unset). Note: the brief for this
/// port mentioned `connected_render_output`, but the actual C++
/// `NodeGroup` does not override it — only the methods below are
/// overridden.
#[derive(Clone)]
pub struct NodeGroup {
/// Passthrough table: group input id -> inner input
/// (C++ `input_passthroughs_`).
input_passthroughs: Vec<InputPassthrough>,
/// Inner node whose output this group forwards
/// (C++ `output_passthrough_`).
output_passthrough: Option<NodeId>,
}
impl NodeGroup {
/// Add (or find) a passthrough for an inner input (C++
/// `add_input_passthrough()`): returns the existing id if the
/// input is already passed through; otherwise mints a fresh id
/// (`input.input`, suffixed `_2`, `_3`, ... until unique, or
/// `force_id` when given), adds a group input with the inner
/// input's type/default/flags, and records the mapping.
///
/// `descriptor` is the inner input's [`crate::input::Input`] (the
/// C++ reads it off the inner node directly; the caller resolves
/// it so the group input can be added while the group entry is
/// mutably borrowed).
pub fn add_input_passthrough(
&mut self,
core: &mut NodeCore,
input: InnerInput,
force_id: &str,
descriptor: &crate::input::Input,
) -> String {
for (id, inner) in &self.input_passthroughs {
if inner == &input {
return id.clone();
}
}
// Mint the passthrough id.
let id = if force_id.is_empty() {
let mut id = input.input.clone();
let mut i = 2;
while core.has_input(&id) {
id = format!("{}_{}", input.input, i);
i += 1;
}
id
} else {
force_id.to_string()
};
core.add_input(crate::input::Input {
id: id.clone(),
value_type: descriptor.value_type,
default: descriptor.default.clone(),
flags: descriptor.flags,
display_name: id.clone(),
properties: Vec::new(),
array_size: 0,
});
self.input_passthroughs.push((id.clone(), input));
id
}
/// Remove the passthrough (and the group input) for an inner
/// input (C++ `remove_input_passthrough()`); no-op if absent.
pub fn remove_input_passthrough(&mut self, core: &mut NodeCore, input: &InnerInput) {
if let Some(i) = self
.input_passthroughs
.iter()
.position(|(_, inner)| inner == input)
{
let id = self.input_passthroughs.remove(i).0;
core.remove_input(&id);
}
}
/// The inner output node (C++ `get_output_passthrough()`).
pub fn output_passthrough(&self) -> Option<NodeId> {
self.output_passthrough
}
/// Set the inner output node (C++ `set_output_passthrough()`); the
/// C++ asserts the node belongs to the group's context (debug-only,
/// not enforced here).
pub fn set_output_passthrough(&mut self, node: Option<NodeId>) {
self.output_passthrough = node;
}
/// Whether an inner input is already passed through (C++
/// `contains_input_passthrough()`).
pub fn contains_input_passthrough(&self, input: &InnerInput) -> bool {
self.input_passthroughs
.iter()
.any(|(_, inner)| inner == input)
}
/// Group input id for an inner input, or empty (C++
/// `get_id_of_passthrough()`).
pub fn id_of_passthrough(&self, input: &InnerInput) -> &str {
for (id, inner) in &self.input_passthroughs {
if inner == input {
return id;
}
}
""
}
/// Inner input behind a group input id, if any (C++
/// `get_input_from_id()`).
pub fn input_from_id(&self, id: &str) -> Option<&InnerInput> {
self.input_passthroughs
.iter()
.find(|(pid, _)| pid == id)
.map(|(_, inner)| inner)
}
/// The passthrough entries (C++ `get_input_passthroughs()`).
pub fn passthroughs(&self) -> &[InputPassthrough] {
&self.input_passthroughs
}
/// Fully resolve a group input to the innermost non-group input
/// (C++ `resolve_input()`): loops [`Self::get_inner`] until the
/// input no longer refers to a group passthrough.
pub fn resolve_input(graph: &Graph, input: InnerInput) -> InnerInput {
let mut input = input;
while Self::get_inner(graph, &mut input) {}
input
}
/// One resolution step (C++ `get_inner()`): if `input` points at a
/// group node's passthrough input, rewrite it to the mapped inner
/// input and return true; false otherwise.
pub fn get_inner(graph: &Graph, input: &mut InnerInput) -> bool {
let inner = graph
.get(input.node)
.and_then(|e| e.behavior.as_any())
.and_then(|a| a.downcast_ref::<NodeGroup>())
.and_then(|g| g.input_from_id(&input.input));
match inner {
Some(inner) => {
input.node = inner.node;
input.input = inner.input.clone();
true
}
None => false,
}
}
}
impl NodeBehavior for NodeGroup {
/// Human-readable name (C++ `name()`).
fn name(&self) -> &str {
"Group"
}
/// Stable type id (C++ `id()`).
fn type_id(&self) -> &str {
"org.olivevideoeditor.Olive.group"
}
/// Categories (C++ `category()` returns `{k_category_unknown}` =
/// -1, which has no `Category` enum counterpart; modeled as an
/// empty slice — the group is hidden from the create menu via
/// the dont-show-in-create-menu flag set in [`create`]).
fn categories(&self) -> &[Category] {
&[]
}
/// Description (C++ `description()`).
fn description(&self) -> &str {
"A group of nodes that is represented as a single node."
}
/// Localized input name (C++ `get_input_name()` override): forwards
/// to the passed-through inner node's input name. The trait's
/// borrowed return cannot carry a graph-resolved name, so the
/// override relies on the default (the plain id / "Enabled"); the
/// C++ forwarding needs a future graph-capable signature.
fn input_name<'a>(&self, id: &'a str) -> &'a str {
id
}
/// Custom project save (C++ `save_custom()`). The node-reference
/// half of the C++ format (inner-node ids, output-passthrough
/// target, per-input name/flags/properties) requires a graph the
/// trait signature does not carry, so only the core-visible
/// passthrough data is written: id, input and element.
fn save_custom(&self, core: &NodeCore, writer: &mut dyn crate::serializer::XmlWrite) {
let _ = core;
writer.start_element("inputpassthroughs");
for (id, inner) in &self.input_passthroughs {
writer.start_element("inputpassthrough");
writer.attribute("input", &inner.input);
writer.attribute("element", &inner.element.to_string());
writer.attribute("id", id);
writer.end_element();
}
writer.end_element();
}
/// Custom project load (C++ `load_custom()`). Parses the
/// `inputpassthroughs` entries written by [`Self::save_custom`];
/// the C++ defers the node references into `SerializedData` and
/// resolves them in `PostLoadEvent`, a channel this crate's
/// serializer does not drive — `post_load` is therefore a no-op.
fn load_custom(
&mut self,
_core: &mut NodeCore,
reader: &mut dyn crate::serializer::XmlRead,
) -> bool {
while reader.next_start_element() {
match reader.name() {
"inputpassthroughs" => {
while reader.next_start_element() {
if reader.name() == "inputpassthrough" {
let id = reader.attribute("id").unwrap_or_default();
let input = reader.attribute("input").unwrap_or_default();
let element = reader
.attribute("element")
.and_then(|e| e.parse().ok())
.unwrap_or(0);
// The inner node id is not resolvable without
// the graph channel; keep the entry with a
// placeholder node so a later graph pass can
// re-map it.
self.input_passthroughs.push((
id,
InnerInput {
node: crate::id::NodeId::INVALID,
input,
element,
},
));
}
reader.skip_current_element();
}
}
_ => reader.skip_current_element(),
}
}
true
}
/// Post-load fixup (C++ `PostLoadEvent()`). The C++ re-resolves the
/// deferred passthrough links against the now-live inner nodes and
/// restores each group's output passthrough; this crate's serializer
/// does not drive the load_custom/save_custom channel, so there is
/// nothing to fix up here yet.
fn post_load(&mut self, _core: &mut NodeCore) {}
/// Deep copy (C++ `copy()` via `NODE_DEFAULT_FUNCTIONS`); clones
/// the passthrough table and output reference too.
fn duplicate(&self, _core: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
Some(Box::new(self.clone()))
}
/// Downcast (C++ `dynamic_cast<NodeGroup *>`).
fn as_any(&self) -> Option<&dyn std::any::Any> {
Some(self)
}
/// See [`NodeBehavior::as_any`].
fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
Some(self)
}
}
/// Constructor (C++ `NodeGroup::NodeGroup()`): no inputs of its own
/// (passthroughs are added dynamically); initializes the output
/// passthrough to unset and sets the dont-show-in-create-menu flag
/// (`// CPP-PARITY: src/node/src/group/group.cpp:35`, flag value
/// `k_dont_show_in_create_menu = 0x8` = [`crate::input::flags::HIDDEN`]).
pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
let mut core = NodeCore::new();
core.flags |= crate::input::flags::HIDDEN as u64;
(
core,
Box::new(NodeGroup {
input_passthroughs: Vec::new(),
output_passthrough: None,
}),
)
}
/// Register this node type (C++ factory entry for
/// `org.olivevideoeditor.Olive.group`; note C++ files it under
/// `k_category_unknown`, which is unrepresentable in [`Category`]).
pub fn register(meta: &mut Vec<NodeMeta>) {
meta.push(NodeMeta {
type_id: "org.olivevideoeditor.Olive.group",
name: "Group",
categories: &[],
create,
});
}