Files
oak-editor/crates/oak-node/src/nodes/ociolut.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

683 lines
24 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/>.
//! OCIO LUT file node (C++ `src/node/src/color/ociolut/ociolut.{h,cpp}`,
//! `olive::OCIOLutNode`).
//!
//! Note: OpenColorIO itself is never linked here; it is reached through
//! the color manager (`crate::colormanager`) and the oakrender bridge
//! (oakrender, opaque handles), like the C++ node's
//! `oakrender_color_processor_create_lut` / `oakrender_lut_*` calls.
use std::sync::Mutex;
use crate::factory::NodeMeta;
use crate::node::{Category, NodeBehavior, NodeCore};
use crate::nodes::ociobase::OcioBase;
/// LUT file input id (C++ `k_file_input`). Type: file; default: empty
/// string; flags: not-keyframable, not-connectable; properties:
/// `filter = "LUT Files (*.<ext> ...);;All Files (*)"` (extensions from
/// `oakrender_lut_supported_extension_at`, `*.*` fallback),
/// `placeholder = "Select a LUT file"`, `lut_library = true`.
pub const FILE_INPUT: &str = "lut_file_in";
/// Direction combo input id (C++ `k_direction_input`). Type: combo;
/// default `0` (forward); flags: not-keyframable, not-connectable.
/// Combo strings (set in `retranslate`): "Forward", "Inverse".
pub const DIRECTION_INPUT: &str = "lut_dir_in";
/// Deferred processor-generation state (C++ `mutable` members
/// `gen_mutex_`, `processor_dirty_`, `last_path_`, `last_direction_`,
/// `last_processor_`, `last_error_`). Grouped behind one mutex: the C++
/// mutable-in-const-method pattern maps to interior mutability here,
/// and the C++ code already serializes all of these under `gen_mutex_`.
struct ProcessorState {
/// Regeneration pending (C++ `processor_dirty_`, starts `true`).
dirty: bool,
/// Path of the LUT the cached processor was built from (C++
/// `last_path_`).
last_path: String,
/// Direction the cached processor was built for (C++
/// `last_direction_`, starts `-1`).
last_direction: i64,
/// Cached processor for change detection (C++ `last_processor_`);
/// released with the node.
last_processor: Option<crate::handle::CHandle>,
/// Human-readable reason no LUT processor is active (C++
/// `last_error_`); empty when a valid processor is in use or no LUT
/// file has been selected yet.
last_error: String,
}
impl Default for ProcessorState {
/// Fresh state: dirty, empty path, no cached processor or error.
fn default() -> Self {
ProcessorState {
dirty: true,
last_path: String::new(),
last_direction: -1,
last_processor: None,
last_error: String::new(),
}
}
}
// The cached processor handle wraps a refcounted C object that is only
// dereferenced from the render path; see `OcioBase` for the rationale.
unsafe impl Send for ProcessorState {}
/// OCIO LUT node. Applies a LUT file through OpenColorIO.
pub struct OCIOLutNode {
/// Shared OCIO base state (C++ base class `OCIOBaseNode`).
base: OcioBase,
/// Processor generation cache/lock (C++ `gen_mutex_` + the mutable
/// `last_*` members).
state: Mutex<ProcessorState>,
}
impl OCIOLutNode {
/// Human-readable description of why no LUT processor is active
/// (C++ `last_error()`); empty when a valid LUT processor is in use
/// or no LUT file has been selected yet.
pub fn last_error(&self) -> String {
self.state.lock().unwrap().last_error.clone()
}
/// Record a new error string (C++ `set_last_error()`); no-op when
/// unchanged. The Qt version surfaced the error on the main-window
/// status bar; here it is only recorded and read back via
/// [`Self::last_error`].
fn set_last_error(&self, error: &str) {
let mut state = self.state.lock().unwrap();
if state.last_error == error {
return;
}
state.last_error = error.to_string();
}
/// Direction combo value with legacy fallback (C++ file-static
/// `read_direction_input()`): integer combo value, or — for old
/// serializers that stored the combo as a string — case-insensitive
/// "forward"/"0" -> 0 and "inverse"/"1" -> 1, defaulting to 0 with a
/// stderr warning for anything else.
fn read_direction_input(core: &NodeCore) -> i64 {
match &core.standard_value(DIRECTION_INPUT, -1) {
crate::value::NodeValue::Combo(i) => *i,
crate::value::NodeValue::Int(i) => *i,
crate::value::NodeValue::Text(s) => {
let lower = s.to_lowercase();
if lower == "forward" || lower == "0" {
0
} else if lower == "inverse" || lower == "1" {
1
} else {
eprintln!("OCIOLutNode: unexpected direction value {}", s);
0
}
}
other => {
eprintln!(
"OCIOLutNode: unexpected direction value {}",
other.to_double()
);
0
}
}
}
/// Whether this is the main GUI process (C++ file-static
/// `is_main_process()`): true when a render manager exists (the
/// render worker never creates one).
fn is_main_process() -> bool {
// The C++ probes `oakrender_manager_available()`, which the
// oakrender bridge does not expose. Without a render manager (the
// oakrender-free test environment) the probe would report the
// worker process, so the worker branch — defer processor
// generation to value() time — always applies here.
// `// CPP-PARITY: ociolut.cpp` is_main_process.
false
}
/// (Re)generate the processor now (C++ `generate_processor()`):
/// ensures the processor is current, then — in the main process
/// only — invalidates the texture-input cache and cancels background
/// video cache tasks so in-flight renders cannot write stale frames.
fn generate_processor(&mut self, core: &mut NodeCore) {
self.ensure_processor(core);
// The C++ main-process half (`invalidate_all(k_texture_input)`
// + `oakrender_cancel_video_tasks(0)`) refreshes the viewer after
// a processor change; it needs the render manager seam that the
// Rust model does not expose, and `is_main_process()` is false
// here, so it is skipped (`// CPP-PARITY: ociolut.cpp`
// generate_processor).
}
/// Ensure the processor matches the current inputs (C++
/// `ensure_processor()`): under the generation mutex, returns early
/// when not dirty, a cached processor exists, and path/direction are
/// unchanged; otherwise rebuilds from the inputs.
fn ensure_processor(&self, core: &NodeCore) {
{
let state = self.state.lock().unwrap();
if !state.dirty
&& state.last_processor.as_ref().is_some_and(|p| !p.is_null())
&& Self::file_path(core) == state.last_path
&& Self::read_direction_input(core) == state.last_direction
{
return;
}
}
self.create_processor_from_inputs(core);
}
/// Rebuild the processor from the LUT path and direction (C++
/// `create_processor_from_inputs()`): no manager, empty path,
/// non-regular file, or unsupported extension -> clear both
/// processors, reset the cache markers, record the error, and return
/// false; unchanged path+direction with a live cached processor ->
/// clear the dirty flag and return false (reuse); otherwise create
/// the LUT processor via `oakrender_color_processor_create_lut`
/// (direction 0 = forward), update the cache markers and both
/// processor slots, and return true.
fn create_processor_from_inputs(&self, core: &NodeCore) -> bool {
let _ = core;
let mut state = self.state.lock().unwrap();
// C++ branch 1: no color manager. The Rust model reaches the
// manager through the oakrender bridge (absent here), so this
// branch is always taken: reset the cache markers and report
// false. The C++ additionally clears the standard processor and
// frees `last_processor_` — the Rust base processor is only
// reachable through `&mut self` and can never hold a processor
// without the render bridge, so those clears are no-ops here.
// The empty-path/non-regular-file/unsupported-extension error
// branches are unreachable without a manager and are not
// representable. `// CPP-PARITY: ociolut.cpp`
// create_processor_from_inputs.
state.last_processor = None;
state.last_path.clear();
state.last_direction = -1;
state.dirty = false;
false
}
/// OCIO config change hook (C++ `config_changed()` override):
/// regenerates immediately in the main process, or just marks the
/// processor dirty in the render worker (deferred to render time).
fn config_changed(&mut self, core: &mut NodeCore) {
let _ = core;
// C++: main process -> generate_processor(); render worker ->
// mark dirty. `is_main_process()` is false here, so the worker
// branch applies and the processor is rebuilt at value() time.
self.state.lock().unwrap().dirty = true;
}
/// The standard value of [`FILE_INPUT`] as a path string.
fn file_path(core: &NodeCore) -> String {
match &core.standard_value(FILE_INPUT, -1) {
crate::value::NodeValue::Text(s) => s.clone(),
_ => String::new(),
}
}
}
impl NodeBehavior for OCIOLutNode {
/// Human-readable name (C++ `name()`).
fn name(&self) -> &str {
"OCIO LUT"
}
/// Stable type id (C++ `id()`).
fn type_id(&self) -> &str {
"org.olivevideoeditor.Olive.ociolut"
}
/// Categories (C++ `category()`).
fn categories(&self) -> &[Category] {
&[Category::Color]
}
/// Description (C++ `description()`).
fn description(&self) -> &str {
"Applies a LUT file through OpenColorIO."
}
/// Localized input names (C++ `retranslate()`): `tex_in` -> "Input",
/// `lut_file_in` -> "LUT File", `lut_dir_in` -> "Direction" (also
/// sets the direction combo strings "Forward"/"Inverse").
fn input_name<'a>(&self, id: &'a str) -> &'a str {
match id {
crate::nodes::ociobase::TEXTURE_INPUT => "Input",
FILE_INPUT => "LUT File",
DIRECTION_INPUT => "Direction",
_ => id,
}
}
/// Combo input option labels (C++ `retranslate()` /
/// `set_combo_box_strings`): `lut_dir_in` -> "Forward", "Inverse".
fn input_combo_strings(&self, id: &str) -> Vec<&'static str> {
match id {
DIRECTION_INPUT => vec!["Forward", "Inverse"],
_ => Vec::new(),
}
}
/// Input value changed (C++ `InputValueChangedEvent`): for
/// `lut_file_in` or `lut_dir_in`, regenerates the processor
/// immediately in the main process; in the render worker (where
/// generation can be slow and the main process may be blocked
/// waiting on LoadGraph) only marks the processor dirty so it is
/// rebuilt at render time.
fn input_value_changed(&mut self, core: &mut NodeCore, input: &str, element: i32) {
let _ = element;
if input == FILE_INPUT || input == DIRECTION_INPUT {
// C++: main process -> generate_processor(); render worker ->
// mark dirty. `is_main_process()` is false here (no render
// manager), so the worker branch applies.
self.state.lock().unwrap().dirty = true;
let _ = core;
}
}
/// Evaluate outputs (C++ `value()`): first `ensure_processor()` so
/// the processor is up to date before the base class emits the color
/// transform job (essential in the render worker, where creation is
/// deferred until the first render), then delegates to
/// [`OcioBase::value`].
fn value(
&self,
core: &NodeCore,
inputs: &crate::value::NodeValueRow,
time: oak_core::Rational,
table: &mut crate::value::NodeValueTable,
) {
self.ensure_processor(core);
self.base.value(core, inputs, time, table);
}
/// Added to a graph (C++ base `AddedToGraphEvent`): captures the
/// project's color manager and runs `config_changed()` via
/// [`OcioBase::added_to_graph`].
fn added_to_graph(&mut self, core: &mut NodeCore) {
self.base.added_to_graph(core);
self.config_changed(core);
}
/// Removed from a graph (C++ base `RemovedFromGraphEvent`): clears
/// the color manager pointer via [`OcioBase::removed_from_graph`].
fn removed_from_graph(&mut self, core: &mut NodeCore) {
self.base.removed_from_graph(core);
}
/// Deep copy (C++ `copy()` via `NODE_COPY_FUNCTION`; the destructor
/// additionally disconnects all signals and frees the cached
/// processor).
fn duplicate(&self, _core: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
// The C++ copy constructor carries over the processor-cache
// members; the refcounted `last_processor_` cannot be shared
// safely by value (the destructor frees it), so a fresh
// processor state (dirty, no cache) plus a fresh empty OcioBase
// is the safe port — the processor is never populated without
// the render bridge anyway.
Some(Box::new(OCIOLutNode {
base: OcioBase::new(),
state: Mutex::new(ProcessorState::default()),
}))
}
}
/// Constructor (C++ `OCIOLutNode::OCIOLutNode()`): builds the base
/// (`tex_in` texture input, effect input, video-effect flag) and adds
/// `lut_file_in` (with the LUT filter/placeholder/lut-library
/// properties) and `lut_dir_in` with the defaults, flags and properties
/// documented on the constants; the processor state starts dirty.
pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
let mut core = NodeCore::new();
// OCIOBaseNode base constructor.
let mut tex = crate::input::Input::new(
crate::nodes::ociobase::TEXTURE_INPUT,
crate::value::ValueType::Texture,
crate::value::NodeValue::None,
);
tex.flags |= crate::input::flags::NOT_KEYFRAMABLE;
core.add_input(tex);
core.effect_input = crate::nodes::ociobase::TEXTURE_INPUT.to_string();
core.flags |= crate::node::flags::VIDEO_EFFECT;
// The file input is a string-carried type in Rust (no file value
// type); the C++ k_file type maps to the same string marshalling.
let mut file = crate::input::Input::new(
FILE_INPUT,
crate::value::ValueType::Text,
crate::value::NodeValue::Text(String::new()),
);
file.flags |= crate::input::flags::NOT_KEYFRAMABLE | crate::input::flags::NOT_CONNECTABLE;
file.properties = vec![
// The C++ collects the supported LUT extensions from
// `oakrender_lut_supported_extension_at`; without the render
// bridge the list is empty and the `*.*` fallback applies
// (`// CPP-PARITY: ociolut.cpp` constructor).
(
"filter".to_string(),
crate::value::NodeValue::Text("LUT Files (*.*);;All Files (*)".into()),
),
(
"placeholder".to_string(),
crate::value::NodeValue::Text("Select a LUT file".into()),
),
(
"lut_library".to_string(),
crate::value::NodeValue::Boolean(true),
),
];
core.add_input(file);
let mut direction = crate::input::Input::new(
DIRECTION_INPUT,
crate::value::ValueType::Combo,
crate::value::NodeValue::Combo(0),
);
direction.flags |= crate::input::flags::NOT_KEYFRAMABLE | crate::input::flags::NOT_CONNECTABLE;
core.add_input(direction);
(
core,
Box::new(OCIOLutNode {
base: OcioBase::new(),
state: Mutex::new(ProcessorState::default()),
}),
)
}
/// Register this node type (C++ factory entry for
/// `org.olivevideoeditor.Olive.ociolut`).
pub fn register(meta: &mut Vec<NodeMeta>) {
meta.push(NodeMeta {
type_id: "org.olivevideoeditor.Olive.ociolut",
name: "OCIO LUT",
categories: &[Category::Color],
create,
});
}
#[cfg(test)]
mod tests {
use super::*;
use crate::value::{NodeValue, NodeValueTable, ValueType};
use oak_core::Rational;
fn node() -> OCIOLutNode {
OCIOLutNode {
base: OcioBase::new(),
state: Mutex::new(ProcessorState::default()),
}
}
#[test]
fn input_names() {
let n = node();
assert_eq!(n.input_name(crate::nodes::ociobase::TEXTURE_INPUT), "Input");
assert_eq!(n.input_name(FILE_INPUT), "LUT File");
assert_eq!(n.input_name(DIRECTION_INPUT), "Direction");
assert_eq!(n.input_name("other_in"), "other_in");
}
#[test]
fn create_wires_inputs_flags_and_properties() {
let (core, behavior) = create();
assert_eq!(behavior.type_id(), "org.olivevideoeditor.Olive.ociolut");
assert_ne!(
core.get_input(crate::nodes::ociobase::TEXTURE_INPUT)
.unwrap()
.flags & crate::input::flags::NOT_KEYFRAMABLE,
0
);
let file = core.get_input(FILE_INPUT).unwrap();
assert_eq!(file.default, NodeValue::Text(String::new()));
assert_ne!(file.flags & crate::input::flags::NOT_KEYFRAMABLE, 0);
assert_ne!(file.flags & crate::input::flags::NOT_CONNECTABLE, 0);
// `*.*` filter fallback without the render bridge.
assert!(file.properties.iter().any(|(k, v)| k == "filter"
&& *v == NodeValue::Text("LUT Files (*.*);;All Files (*)".into())));
assert!(file
.properties
.iter()
.any(|(k, v)| k == "placeholder" && *v == NodeValue::Text("Select a LUT file".into())));
assert!(file
.properties
.iter()
.any(|(k, v)| k == "lut_library" && *v == NodeValue::Boolean(true)));
let dir = core.get_input(DIRECTION_INPUT).unwrap();
assert_eq!(dir.default, NodeValue::Combo(0));
assert_ne!(dir.flags & crate::input::flags::NOT_KEYFRAMABLE, 0);
assert_ne!(dir.flags & crate::input::flags::NOT_CONNECTABLE, 0);
assert_eq!(core.effect_input, crate::nodes::ociobase::TEXTURE_INPUT);
assert_ne!(core.flags & crate::node::flags::VIDEO_EFFECT, 0);
}
#[test]
fn read_direction_input_parses_combo_and_legacy_strings() {
let mut core = NodeCore::new();
core.add_input(crate::input::Input::new(
DIRECTION_INPUT,
crate::value::ValueType::Combo,
crate::value::NodeValue::Combo(0),
));
assert_eq!(OCIOLutNode::read_direction_input(&core), 0);
core.set_standard_value(DIRECTION_INPUT, -1, NodeValue::Combo(1));
assert_eq!(OCIOLutNode::read_direction_input(&core), 1);
// Legacy string storage (old serializers).
core.set_standard_value(DIRECTION_INPUT, -1, NodeValue::Text("forward".into()));
assert_eq!(OCIOLutNode::read_direction_input(&core), 0);
core.set_standard_value(DIRECTION_INPUT, -1, NodeValue::Text("Inverse".into()));
assert_eq!(OCIOLutNode::read_direction_input(&core), 1);
core.set_standard_value(DIRECTION_INPUT, -1, NodeValue::Text("0".into()));
assert_eq!(OCIOLutNode::read_direction_input(&core), 0);
}
#[test]
fn last_error_roundtrip_and_set_once() {
let n = node();
assert_eq!(n.last_error(), "");
n.set_last_error("OCIO LUT: file does not exist: /nope.cube");
assert_eq!(n.last_error(), "OCIO LUT: file does not exist: /nope.cube");
// No-op when unchanged.
n.set_last_error("OCIO LUT: file does not exist: /nope.cube");
assert_eq!(n.last_error(), "OCIO LUT: file does not exist: /nope.cube");
n.set_last_error("");
assert_eq!(n.last_error(), "");
}
#[test]
fn create_processor_from_inputs_resets_markers_without_manager() {
let n = node();
let core = NodeCore::new();
{
let mut state = n.state.lock().unwrap();
state.dirty = true;
state.last_path = "/tmp/foo.cube".to_string();
state.last_direction = 1;
state.last_processor = Some(crate::handle::CHandle::null());
state.last_error = "stale error".to_string();
}
let created = n.create_processor_from_inputs(&core);
assert!(!created);
let state = n.state.lock().unwrap();
assert!(!state.dirty);
assert_eq!(state.last_path, "");
assert_eq!(state.last_direction, -1);
assert!(state.last_processor.is_none());
// The no-manager branch does not touch the recorded error (C++).
assert_eq!(state.last_error, "stale error");
}
#[test]
fn ensure_processor_rebuilds_when_no_cached_processor() {
let n = node();
let core = NodeCore::new();
// Fresh state is dirty -> rebuild (resets markers, returns false).
n.ensure_processor(&core);
let state = n.state.lock().unwrap();
assert!(!state.dirty);
assert!(state.last_processor.is_none());
}
#[test]
fn input_value_changed_marks_dirty() {
let mut core = NodeCore::new();
let mut n = node();
n.state.lock().unwrap().dirty = false;
n.input_value_changed(&mut core, FILE_INPUT, 0);
assert!(n.state.lock().unwrap().dirty);
n.state.lock().unwrap().dirty = false;
n.input_value_changed(&mut core, DIRECTION_INPUT, 0);
assert!(n.state.lock().unwrap().dirty);
// Unrelated inputs are ignored.
n.state.lock().unwrap().dirty = false;
n.input_value_changed(&mut core, crate::nodes::ociobase::TEXTURE_INPUT, 0);
assert!(!n.state.lock().unwrap().dirty);
}
#[test]
fn config_changed_marks_dirty() {
let mut core = NodeCore::new();
let mut n = node();
n.state.lock().unwrap().dirty = false;
n.config_changed(&mut core);
assert!(n.state.lock().unwrap().dirty);
}
#[test]
fn file_path_reads_text_standard_value() {
let mut core = NodeCore::new();
core.add_input(crate::input::Input::new(
FILE_INPUT,
crate::value::ValueType::Text,
crate::value::NodeValue::Text(String::new()),
));
core.set_standard_value(FILE_INPUT, -1, NodeValue::Text("/tmp/x.cube".into()));
assert_eq!(OCIOLutNode::file_path(&core), "/tmp/x.cube");
// Non-text standard value (or missing input) yields empty.
core.set_standard_value(FILE_INPUT, -1, NodeValue::Float(3.0));
assert_eq!(OCIOLutNode::file_path(&core), "");
}
#[test]
fn ensure_processor_reuses_cached_when_unchanged() {
let n = node();
let mut core = NodeCore::new();
core.add_input(crate::input::Input::new(
FILE_INPUT,
crate::value::ValueType::Text,
crate::value::NodeValue::Text(String::new()),
));
core.add_input(crate::input::Input::new(
DIRECTION_INPUT,
crate::value::ValueType::Combo,
crate::value::NodeValue::Combo(0),
));
core.set_standard_value(FILE_INPUT, -1, NodeValue::Text("/tmp/x.cube".into()));
core.set_standard_value(DIRECTION_INPUT, -1, NodeValue::Combo(1));
{
let mut state = n.state.lock().unwrap();
state.dirty = false;
state.last_path = "/tmp/x.cube".to_string();
state.last_direction = 1;
state.last_processor = Some(crate::handle::make_owned::<u8>(1));
}
// Unchanged path+direction with a live processor: early return,
// markers are preserved (create_processor_from_inputs would have
// reset them).
n.ensure_processor(&core);
let state = n.state.lock().unwrap();
assert_eq!(state.last_path, "/tmp/x.cube");
assert_eq!(state.last_direction, 1);
assert!(state.last_processor.is_some());
}
#[test]
fn generate_processor_wraps_ensure_processor() {
let mut n = node();
let mut core = NodeCore::new();
n.state.lock().unwrap().dirty = true;
// Worker branch: generate_processor just ensures; markers reset.
n.generate_processor(&mut core);
let state = n.state.lock().unwrap();
assert!(!state.dirty);
assert_eq!(state.last_path, "");
assert_eq!(state.last_direction, -1);
}
#[test]
fn is_main_process_always_false_without_bridge() {
assert!(!OCIOLutNode::is_main_process());
}
#[test]
fn value_no_texture_pushes_nothing() {
let (core, behavior) = create();
let mut table = NodeValueTable::default();
behavior.value(
&core,
&crate::value::NodeValueRow::default(),
Rational::new(0, 1),
&mut table,
);
assert!(table.is_empty());
}
#[test]
fn value_passes_texture_through_without_processor() {
// Without the render bridge no LUT processor can be created, so
// the base value() passes the input texture through unchanged.
let core = NodeCore::new();
let n = node();
let tex = NodeValue::Texture(crate::handle::CHandle::null());
let inputs = crate::value::NodeValueRow::from([(
crate::nodes::ociobase::TEXTURE_INPUT.to_string(),
tex.clone(),
)]);
let mut table = NodeValueTable::default();
n.value(&core, &inputs, Rational::new(0, 1), &mut table);
assert_eq!(table.get(ValueType::Texture), Some(&tex));
}
#[test]
fn value_pushes_deferred_job_with_processor() {
let core = NodeCore::new();
let mut n = node();
n.base.set_processor(Some(crate::handle::CHandle::null()));
let inputs = crate::value::NodeValueRow::from([(
crate::nodes::ociobase::TEXTURE_INPUT.to_string(),
NodeValue::Texture(crate::handle::CHandle::null()),
)]);
let mut table = NodeValueTable::default();
n.value(&core, &inputs, Rational::new(0, 1), &mut table);
assert!(table.get(ValueType::Texture).is_some());
}
#[test]
fn duplicate_clones() {
let (core, behavior) = create();
let dup = behavior.duplicate(&core).unwrap();
assert_eq!(dup.name(), "OCIO LUT");
}
}