refactor: workspace layout — crates/, app at root, legacy C++ removed
Single mechanical restructure commit: - root Cargo.toml = oakapp bin + workspace; one cargo build produces oakapp, oak-cli, oak-worker, liboakengine.dylib - app/rust/src -> src/ (app at repo root, no rust/ nesting) - src/<mod>/rust -> crates/oak<mod>; src/oakcore-rs -> crates/oakcore; src/bindings/oakotio -> crates/oakotio; src/engine/rust -> crates/oakengine (keeps cdylib+staticlib+rlib) - public C headers include/<mod>/ -> crates/oakengine/include/<mod>/ - OFX SDK headers vendored into crates/oakplugin/ofx/ (HostSupport gone) - legacy deleted: old src/ C++ modules, engine/, core/, ffmpeg_bridge/, app/ (Qt), cli/worker C++, root CMakeLists, third_party/KDDockWidgets submodule, otio-install, all build-* output (~40GB) - oakstorage kept but excluded from the workspace (skeleton w/ todos); gpui excluded (own workspace) - verified: cargo build green, cargo test --workspace 1845/0 (with the documented OCIO_RS_* env override for the homebrew OCIO)
This commit is contained in:
@@ -0,0 +1,665 @@
|
||||
// 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 log color grading node (C++
|
||||
//! `src/node/src/color/ociogradingtransformlog/ociogradingtransformlog.{h,cpp}`,
|
||||
//! `olive::OCIOGradingTransformLogNode`).
|
||||
//!
|
||||
//! Lift/gamma/gain grading built on `ocio::GRADING_LOG`; mirrors the
|
||||
//! linear grading node for the log style. OCIO's log-style GPU uniforms
|
||||
//! map to the classic wheels as: brightness = lift, contrast = gain,
|
||||
//! gamma = gamma.
|
||||
//!
|
||||
//! Note: OpenColorIO itself is never linked here; it is reached through
|
||||
//! the color manager (`crate::colormanager`) and the oakrender bridge
|
||||
//! (`crate::bridge::render`), like the C++ node's
|
||||
//! `oakrender_color_processor_create_grading_primary` call.
|
||||
|
||||
use crate::factory::NodeMeta;
|
||||
use crate::node::{Category, NodeBehavior, NodeCore};
|
||||
|
||||
use crate::nodes::ociobase::OcioBase;
|
||||
|
||||
/// Lift input id (C++ `k_lift_input`). Type: vec4 (x = master); default
|
||||
/// `{0, 0, 0, 0}`; properties: `base = 0.01`, component colors `color0
|
||||
/// = #c0c0c0`, `color1 = #ff0000`, `color2 = #00ff00`, `color3 =
|
||||
/// #0000ff`. NOTE: the grading input ids double as the OCIO GPU uniform
|
||||
/// names for the dynamic GradingPrimaryTransform; do not rename them.
|
||||
/// The C++ literals carry the literal text `OCIO_NAMESPACE` because the
|
||||
/// sources were ported without expanding OCIO's namespace macro.
|
||||
pub const LIFT_INPUT: &str = "OCIO_NAMESPACE_grading_primary_brightness";
|
||||
|
||||
/// Gain input id (C++ `k_gain_input`; OCIO's log-style "contrast"
|
||||
/// uniform). Type: vec4 (x = master); default `{1, 1, 1, 1}`;
|
||||
/// properties: `base = 0.01` and the component colors documented on
|
||||
/// [`LIFT_INPUT`]. Do not rename (GPU uniform name).
|
||||
pub const GAIN_INPUT: &str = "OCIO_NAMESPACE_grading_primary_contrast";
|
||||
|
||||
/// Gamma input id (C++ `k_gamma_input`). Type: vec4 (x = master);
|
||||
/// default `{1, 1, 1, 1}`; properties: `base = 0.01` and the component
|
||||
/// colors documented on [`LIFT_INPUT`]. Do not rename (GPU uniform
|
||||
/// name).
|
||||
pub const GAMMA_INPUT: &str = "OCIO_NAMESPACE_grading_primary_gamma";
|
||||
|
||||
/// Saturation input id (C++ `k_saturation_input`). Type: float; default
|
||||
/// `1.0`; properties: `view = percentage`, `min = 0.0`. Do not rename
|
||||
/// (GPU uniform name).
|
||||
pub const SATURATION_INPUT: &str = "OCIO_NAMESPACE_grading_primary_saturation";
|
||||
|
||||
/// Pivot input id (C++ `k_pivot_input`). Type: float; default `-0.2`
|
||||
/// (default for `GRADING_LOG` listed in `ocio::GradingPrimary`);
|
||||
/// properties: `base = 0.01`. Do not rename (GPU uniform name).
|
||||
pub const PIVOT_INPUT: &str = "OCIO_NAMESPACE_grading_primary_pivot";
|
||||
|
||||
/// Black-clamp enable input id (C++ `k_clamp_black_enable_input`).
|
||||
/// Type: boolean; default `false`.
|
||||
pub const CLAMP_BLACK_ENABLE_INPUT: &str = "clamp_black_enable_in";
|
||||
|
||||
/// Black clamp input id (C++ `k_clamp_black_input`). Type: float;
|
||||
/// default `0.0`; properties: `enabled` = current value of
|
||||
/// [`CLAMP_BLACK_ENABLE_INPUT`], `base = 0.01`. Do not rename (GPU
|
||||
/// uniform name).
|
||||
pub const CLAMP_BLACK_INPUT: &str = "OCIO_NAMESPACE_grading_primary_clampBlack";
|
||||
|
||||
/// White-clamp enable input id (C++ `k_clamp_white_enable_input`).
|
||||
/// Type: boolean; default `false`.
|
||||
pub const CLAMP_WHITE_ENABLE_INPUT: &str = "clamp_white_enable_in";
|
||||
|
||||
/// White clamp input id (C++ `k_clamp_white_input`). Type: float;
|
||||
/// default `1.0`; properties: `enabled` = current value of
|
||||
/// [`CLAMP_WHITE_ENABLE_INPUT`], `base = 0.01`, and `min` = black clamp
|
||||
/// + 0.000001 while the black clamp is static. Do not rename (GPU
|
||||
/// uniform name).
|
||||
pub const CLAMP_WHITE_INPUT: &str = "OCIO_NAMESPACE_grading_primary_clampWhite";
|
||||
|
||||
/// OCIO log grading node. Lift/gamma/gain color grading using
|
||||
/// OpenColorIO (`ocio::GRADING_LOG`). Owns no members beyond the
|
||||
/// embedded OCIO base state (C++ has no own private members).
|
||||
pub struct OCIOGradingTransformLogNode {
|
||||
/// Shared OCIO base state (C++ base class `OCIOBaseNode`).
|
||||
base: OcioBase,
|
||||
}
|
||||
|
||||
/// Set or replace an input property (C++ `set_input_property`).
|
||||
fn set_input_property(
|
||||
core: &mut NodeCore,
|
||||
input: &str,
|
||||
key: &str,
|
||||
value: crate::value::NodeValue,
|
||||
) {
|
||||
if let Some(input) = core.get_input_mut(input) {
|
||||
if let Some(slot) = input.properties.iter_mut().find(|(k, _)| k == key) {
|
||||
slot.1 = value;
|
||||
} else {
|
||||
input.properties.push((key.to_string(), value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OCIOGradingTransformLogNode {
|
||||
/// Set the per-component widget colors of a vec4 input (C++
|
||||
/// `set_vec4_input_colors()`): master `#c0c0c0`, R `#ff0000`, G
|
||||
/// `#00ff00`, B `#0000ff`.
|
||||
fn set_vec4_input_colors(core: &mut NodeCore, input: &str) {
|
||||
set_input_property(core, input, "color0", crate::value::NodeValue::Text("#c0c0c0".into()));
|
||||
set_input_property(core, input, "color1", crate::value::NodeValue::Text("#ff0000".into()));
|
||||
set_input_property(core, input, "color2", crate::value::NodeValue::Text("#00ff00".into()));
|
||||
set_input_property(core, input, "color3", crate::value::NodeValue::Text("#0000ff".into()));
|
||||
}
|
||||
|
||||
/// Constrain the white clamp UI minimum to just above the black
|
||||
/// clamp (C++ `update_clamp_white_minimum()`), as required by
|
||||
/// `ocio::GradingPrimary::validate`. No-op while the black clamp is
|
||||
/// keyframed or connected — a static UI minimum cannot follow an
|
||||
/// animated value, so the invariant is enforced per frame in
|
||||
/// `value()` instead.
|
||||
fn update_clamp_white_minimum(&mut self, core: &mut NodeCore) {
|
||||
let _ = self;
|
||||
// The C++ also returns while the black clamp is *connected*
|
||||
// (`is_input_connected`); edge state is not carried by NodeCore
|
||||
// (the C++ InputConnectedEvent/InputDisconnectedEvent call sites
|
||||
// re-run this), so only the keyframing half of the guard is
|
||||
// representable here — a connected black clamp would update the
|
||||
// static minimum where C++ would not.
|
||||
// `// CPP-PARITY: ociogradingtransformlog.cpp`
|
||||
// update_clamp_white_minimum.
|
||||
if core.is_input_keyframing(CLAMP_BLACK_INPUT, -1) {
|
||||
return;
|
||||
}
|
||||
let min = core.standard_value(CLAMP_BLACK_INPUT, -1).to_double() + 0.000001;
|
||||
set_input_property(core, CLAMP_WHITE_INPUT, "min", crate::value::NodeValue::Float(min));
|
||||
}
|
||||
|
||||
/// (Re)build the color processor (C++ `generate_processor()`):
|
||||
/// creates a grading-primary processor of style
|
||||
/// `OAKRENDER_GRADING_PRIMARY_LOG` through the color manager and
|
||||
/// stores it with [`OcioBase::set_processor`] when creation
|
||||
/// succeeds.
|
||||
fn generate_processor(&mut self, core: &mut NodeCore) {
|
||||
let _ = core;
|
||||
// The C++ wraps the color manager and creates a grading-primary
|
||||
// processor of style `OAKRENDER_GRADING_PRIMARY_LOG` via
|
||||
// `oakrender_color_processor_create_grading_primary`, storing it
|
||||
// with OcioBase::set_processor when `processor.ctx` is non-null.
|
||||
// Without a manager (the Rust model reaches the manager through
|
||||
// the oakrender bridge, absent here) the C++ guard
|
||||
// `if (manager())` fails, so this is a no-op and the processor
|
||||
// stays empty — `value()` then pushes nothing.
|
||||
// `// CPP-PARITY: ociogradingtransformlog.cpp` generate_processor.
|
||||
}
|
||||
|
||||
/// OCIO config change hook (C++ `config_changed()` override):
|
||||
/// regenerates the processor.
|
||||
fn config_changed(&mut self, core: &mut NodeCore) {
|
||||
self.generate_processor(core);
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeBehavior for OCIOGradingTransformLogNode {
|
||||
/// Human-readable name (C++ `name()`).
|
||||
fn name(&self) -> &str {
|
||||
"OCIO Color Grading (Log)"
|
||||
}
|
||||
|
||||
/// Stable type id (C++ `id()`). The literal `OCIO_NAMESPACE` text is
|
||||
/// in the C++ string (the namespace macro was never expanded), so the
|
||||
/// id is kept verbatim for project compatibility.
|
||||
fn type_id(&self) -> &str {
|
||||
"org.olivevideoeditor.Olive.OCIO_NAMESPACEgradingtransformlog"
|
||||
}
|
||||
|
||||
/// Categories (C++ `category()`).
|
||||
fn categories(&self) -> &[Category] {
|
||||
&[Category::Color]
|
||||
}
|
||||
|
||||
/// Description (C++ `description()`).
|
||||
fn description(&self) -> &str {
|
||||
"Lift/gamma/gain color grading using OpenColorIO."
|
||||
}
|
||||
|
||||
/// Localized input names (C++ `retranslate()`): `tex_in` -> "Input",
|
||||
/// lift -> "Lift", gain -> "Gain", gamma -> "Gamma", saturation ->
|
||||
/// "Saturation", pivot -> "Pivot", clamp enables -> "Enable
|
||||
/// Black/White Clamp", clamps -> "Black Clamp"/"White Clamp".
|
||||
fn input_name<'a>(&self, id: &'a str) -> &'a str {
|
||||
match id {
|
||||
crate::nodes::ociobase::TEXTURE_INPUT => "Input",
|
||||
LIFT_INPUT => "Lift",
|
||||
GAIN_INPUT => "Gain",
|
||||
GAMMA_INPUT => "Gamma",
|
||||
SATURATION_INPUT => "Saturation",
|
||||
PIVOT_INPUT => "Pivot",
|
||||
CLAMP_BLACK_ENABLE_INPUT => "Enable Black Clamp",
|
||||
CLAMP_BLACK_INPUT => "Black Clamp",
|
||||
CLAMP_WHITE_ENABLE_INPUT => "Enable White Clamp",
|
||||
CLAMP_WHITE_INPUT => "White Clamp",
|
||||
_ => id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Input value changed (C++ `InputValueChangedEvent`): toggling a
|
||||
/// clamp-enable input mirrors it into the clamp input's `enabled`
|
||||
/// property; a black-clamp change re-constrains the white clamp
|
||||
/// minimum; any change regenerates the processor.
|
||||
fn input_value_changed(&mut self, core: &mut NodeCore, input: &str, element: i32) {
|
||||
let _ = element;
|
||||
if input == CLAMP_WHITE_ENABLE_INPUT {
|
||||
set_input_property(
|
||||
core,
|
||||
CLAMP_WHITE_INPUT,
|
||||
"enabled",
|
||||
crate::value::NodeValue::Boolean(
|
||||
core.standard_value(CLAMP_WHITE_ENABLE_INPUT, -1).to_double() != 0.0,
|
||||
),
|
||||
);
|
||||
} else if input == CLAMP_BLACK_ENABLE_INPUT {
|
||||
set_input_property(
|
||||
core,
|
||||
CLAMP_BLACK_INPUT,
|
||||
"enabled",
|
||||
crate::value::NodeValue::Boolean(
|
||||
core.standard_value(CLAMP_BLACK_ENABLE_INPUT, -1).to_double() != 0.0,
|
||||
),
|
||||
);
|
||||
} else if input == CLAMP_BLACK_INPUT {
|
||||
// Ensure the white clamp is always greater than the black
|
||||
// clamp as per ocio::GradingPrimary::validate.
|
||||
self.update_clamp_white_minimum(core);
|
||||
}
|
||||
|
||||
self.generate_processor(core);
|
||||
}
|
||||
|
||||
/// Edge connected (C++ `InputConnectedEvent`): forwards to the base
|
||||
/// class and, for the black clamp input, re-constrains the white
|
||||
/// clamp minimum.
|
||||
fn input_connected(&mut self, core: &mut NodeCore, input: &str, element: i32, source: crate::id::NodeId) {
|
||||
let _ = (element, source);
|
||||
// C++ forwards to the base class first; OCIOBaseNode does not
|
||||
// override the event, so that half is a no-op here.
|
||||
if input == CLAMP_BLACK_INPUT {
|
||||
self.update_clamp_white_minimum(core);
|
||||
}
|
||||
}
|
||||
|
||||
/// Edge disconnected (C++ `InputDisconnectedEvent`): forwards to the
|
||||
/// base class and, for the black clamp input, re-constrains the
|
||||
/// white clamp minimum.
|
||||
fn input_disconnected(&mut self, core: &mut NodeCore, input: &str, element: i32, source: crate::id::NodeId) {
|
||||
let _ = (element, source);
|
||||
// See [`NodeBehavior::input_connected`].
|
||||
if input == CLAMP_BLACK_INPUT {
|
||||
self.update_clamp_white_minimum(core);
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate outputs (C++ `value()`): no texture -> push nothing;
|
||||
/// processor not ready -> push nothing (no pass-through branch).
|
||||
/// Otherwise builds a `ColorTransformJob` from the whole input row
|
||||
/// and rewrites the vec4 (RGBM: x = master) inputs into the vec3
|
||||
/// form the GPU uniforms expect: lift RGB = channel + master
|
||||
/// (additive); gain and gamma RGB = channel * master
|
||||
/// (multiplicative). Disabled clamps are pushed as
|
||||
/// `GradingPrimary::NoClampBlack()/NoClampWhite()`, and when both
|
||||
/// clamps are enabled the white clamp is raised to black + 0.000001
|
||||
/// per frame if keyframed/connected values violate white > black.
|
||||
fn value(
|
||||
&self,
|
||||
core: &NodeCore,
|
||||
inputs: &crate::value::NodeValueRow,
|
||||
time: oakcore_rs::Rational,
|
||||
table: &mut crate::value::NodeValueTable,
|
||||
) {
|
||||
let _ = (core, time);
|
||||
match inputs.get(crate::nodes::ociobase::TEXTURE_INPUT) {
|
||||
Some(crate::value::NodeValue::Texture(_)) => {
|
||||
if self.base.processor().is_some() {
|
||||
// `// CPP-PARITY: ociogradingtransformlog.cpp`
|
||||
// `value()` — the C++ builds a ColorTransformJob and
|
||||
// rewrites the vec4 (RGBM: x = master) inputs into the
|
||||
// vec3 GPU uniform form: lift RGB = channel + master
|
||||
// (additive); gain and gamma RGB = channel * master
|
||||
// (multiplicative). Disabled clamps are pushed as
|
||||
// `OCIO_NAMESPACE::GradingPrimary::NoClampBlack()`
|
||||
// (-1.0) / `NoClampWhite()` (2.0), and when both
|
||||
// clamps are enabled the white clamp is raised to
|
||||
// black + 0.000001 per frame if keyframed/connected
|
||||
// values violate white > black. The Rust model has no
|
||||
// color-transform job payload: the renderer seam
|
||||
// resolves the deferred job from this null handle.
|
||||
table.push(
|
||||
crate::value::ValueType::Texture,
|
||||
crate::value::NodeValue::Texture(crate::handle::CHandle::null()),
|
||||
None,
|
||||
);
|
||||
}
|
||||
// Processor not ready: push nothing (no pass-through).
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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_DEFAULT_FUNCTIONS`).
|
||||
fn duplicate(&self, _core: &NodeCore) -> Option<Box<dyn NodeBehavior>> {
|
||||
// The C++ copy constructor copies the embedded OCIO base state;
|
||||
// a fresh base with no processor is the safe Rust port (the
|
||||
// processor is never populated without the render bridge).
|
||||
Some(Box::new(OCIOGradingTransformLogNode {
|
||||
base: OcioBase::new(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructor (C++
|
||||
/// `OCIOGradingTransformLogNode::OCIOGradingTransformLogNode()`):
|
||||
/// builds the base (`tex_in` texture input, effect input, video-effect
|
||||
/// flag), adds the lift/gain/gamma/saturation/pivot and
|
||||
/// clamp-enable/clamp inputs with the defaults, flags and properties
|
||||
/// documented on the constants, and applies the initial white-clamp
|
||||
/// minimum constraint.
|
||||
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;
|
||||
|
||||
let component_colors = vec![
|
||||
("color0".to_string(), crate::value::NodeValue::Text("#c0c0c0".into())),
|
||||
("color1".to_string(), crate::value::NodeValue::Text("#ff0000".into())),
|
||||
("color2".to_string(), crate::value::NodeValue::Text("#00ff00".into())),
|
||||
("color3".to_string(), crate::value::NodeValue::Text("#0000ff".into())),
|
||||
];
|
||||
|
||||
let mut lift = crate::input::Input::new(
|
||||
LIFT_INPUT,
|
||||
crate::value::ValueType::Vec4,
|
||||
crate::value::NodeValue::Vec4([0.0, 0.0, 0.0, 0.0]),
|
||||
);
|
||||
lift.properties = vec![("base".to_string(), crate::value::NodeValue::Float(0.01))];
|
||||
lift.properties.extend(component_colors.clone());
|
||||
core.add_input(lift);
|
||||
|
||||
let mut gain = crate::input::Input::new(
|
||||
GAIN_INPUT,
|
||||
crate::value::ValueType::Vec4,
|
||||
crate::value::NodeValue::Vec4([1.0, 1.0, 1.0, 1.0]),
|
||||
);
|
||||
gain.properties = vec![("base".to_string(), crate::value::NodeValue::Float(0.01))];
|
||||
gain.properties.extend(component_colors.clone());
|
||||
core.add_input(gain);
|
||||
|
||||
let mut gamma = crate::input::Input::new(
|
||||
GAMMA_INPUT,
|
||||
crate::value::ValueType::Vec4,
|
||||
crate::value::NodeValue::Vec4([1.0, 1.0, 1.0, 1.0]),
|
||||
);
|
||||
gamma.properties = vec![("base".to_string(), crate::value::NodeValue::Float(0.01))];
|
||||
gamma.properties.extend(component_colors);
|
||||
core.add_input(gamma);
|
||||
|
||||
let mut saturation = crate::input::Input::new(
|
||||
SATURATION_INPUT,
|
||||
crate::value::ValueType::Float,
|
||||
crate::value::NodeValue::Float(1.0),
|
||||
);
|
||||
saturation.properties = vec![
|
||||
("view".to_string(), crate::value::NodeValue::Text("percentage".into())),
|
||||
("min".to_string(), crate::value::NodeValue::Float(0.0)),
|
||||
];
|
||||
core.add_input(saturation);
|
||||
|
||||
let mut pivot = crate::input::Input::new(
|
||||
PIVOT_INPUT,
|
||||
crate::value::ValueType::Float,
|
||||
crate::value::NodeValue::Float(-0.2),
|
||||
);
|
||||
pivot.properties = vec![("base".to_string(), crate::value::NodeValue::Float(0.01))];
|
||||
core.add_input(pivot);
|
||||
|
||||
core.add_input(crate::input::Input::new(
|
||||
CLAMP_BLACK_ENABLE_INPUT,
|
||||
crate::value::ValueType::Boolean,
|
||||
crate::value::NodeValue::Boolean(false),
|
||||
));
|
||||
let mut clamp_black = crate::input::Input::new(
|
||||
CLAMP_BLACK_INPUT,
|
||||
crate::value::ValueType::Float,
|
||||
crate::value::NodeValue::Float(0.0),
|
||||
);
|
||||
clamp_black.properties = vec![
|
||||
(
|
||||
"enabled".to_string(),
|
||||
crate::value::NodeValue::Boolean(
|
||||
core.standard_value(CLAMP_BLACK_ENABLE_INPUT, -1).to_double() != 0.0,
|
||||
),
|
||||
),
|
||||
("base".to_string(), crate::value::NodeValue::Float(0.01)),
|
||||
];
|
||||
core.add_input(clamp_black);
|
||||
|
||||
core.add_input(crate::input::Input::new(
|
||||
CLAMP_WHITE_ENABLE_INPUT,
|
||||
crate::value::ValueType::Boolean,
|
||||
crate::value::NodeValue::Boolean(false),
|
||||
));
|
||||
let mut clamp_white = crate::input::Input::new(
|
||||
CLAMP_WHITE_INPUT,
|
||||
crate::value::ValueType::Float,
|
||||
crate::value::NodeValue::Float(1.0),
|
||||
);
|
||||
clamp_white.properties = vec![
|
||||
(
|
||||
"enabled".to_string(),
|
||||
crate::value::NodeValue::Boolean(
|
||||
core.standard_value(CLAMP_WHITE_ENABLE_INPUT, -1).to_double() != 0.0,
|
||||
),
|
||||
),
|
||||
("base".to_string(), crate::value::NodeValue::Float(0.01)),
|
||||
];
|
||||
core.add_input(clamp_white);
|
||||
|
||||
// Constrain the white clamp minimum to just above the (static) black
|
||||
// clamp as per OCIO_NAMESPACE::GradingPrimary::validate. When the
|
||||
// black clamp is keyframed or connected, Value() enforces the
|
||||
// invariant per frame instead.
|
||||
let mut node = OCIOGradingTransformLogNode {
|
||||
base: OcioBase::new(),
|
||||
};
|
||||
node.update_clamp_white_minimum(&mut core);
|
||||
|
||||
(core, Box::new(node))
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.OCIO_NAMESPACEgradingtransformlog`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.OCIO_NAMESPACEgradingtransformlog",
|
||||
name: "OCIO Color Grading (Log)",
|
||||
categories: &[Category::Color],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::keyframe::{Interpolation, Keyframe};
|
||||
use crate::value::{NodeValue, NodeValueTable, ValueType};
|
||||
use oakcore_rs::Rational;
|
||||
|
||||
fn node() -> OCIOGradingTransformLogNode {
|
||||
OCIOGradingTransformLogNode {
|
||||
base: OcioBase::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Property value lookup helper for tests.
|
||||
fn property(core: &NodeCore, input: &str, key: &str) -> Option<NodeValue> {
|
||||
core.get_input(input)
|
||||
.and_then(|i| i.properties.iter().find(|(k, _)| k == key).map(|(_, v)| v.clone()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_names() {
|
||||
let n = node();
|
||||
assert_eq!(n.input_name(crate::nodes::ociobase::TEXTURE_INPUT), "Input");
|
||||
assert_eq!(n.input_name(LIFT_INPUT), "Lift");
|
||||
assert_eq!(n.input_name(GAIN_INPUT), "Gain");
|
||||
assert_eq!(n.input_name(GAMMA_INPUT), "Gamma");
|
||||
assert_eq!(n.input_name(SATURATION_INPUT), "Saturation");
|
||||
assert_eq!(n.input_name(PIVOT_INPUT), "Pivot");
|
||||
assert_eq!(n.input_name(CLAMP_BLACK_ENABLE_INPUT), "Enable Black Clamp");
|
||||
assert_eq!(n.input_name(CLAMP_BLACK_INPUT), "Black Clamp");
|
||||
assert_eq!(n.input_name(CLAMP_WHITE_ENABLE_INPUT), "Enable White Clamp");
|
||||
assert_eq!(n.input_name(CLAMP_WHITE_INPUT), "White Clamp");
|
||||
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.OCIO_NAMESPACEgradingtransformlog");
|
||||
assert_ne!(
|
||||
core.get_input(crate::nodes::ociobase::TEXTURE_INPUT).unwrap().flags & crate::input::flags::NOT_KEYFRAMABLE,
|
||||
0
|
||||
);
|
||||
assert_eq!(core.get_input(LIFT_INPUT).unwrap().default, NodeValue::Vec4([0.0; 4]));
|
||||
assert_eq!(core.get_input(GAIN_INPUT).unwrap().default, NodeValue::Vec4([1.0; 4]));
|
||||
assert_eq!(core.get_input(GAMMA_INPUT).unwrap().default, NodeValue::Vec4([1.0; 4]));
|
||||
assert_eq!(core.get_input(SATURATION_INPUT).unwrap().default, NodeValue::Float(1.0));
|
||||
assert_eq!(core.get_input(PIVOT_INPUT).unwrap().default, NodeValue::Float(-0.2));
|
||||
assert_eq!(core.get_input(CLAMP_BLACK_INPUT).unwrap().default, NodeValue::Float(0.0));
|
||||
assert_eq!(core.get_input(CLAMP_WHITE_INPUT).unwrap().default, NodeValue::Float(1.0));
|
||||
// Component colors on every vec4 grading input.
|
||||
for id in [LIFT_INPUT, GAIN_INPUT, GAMMA_INPUT] {
|
||||
let input = core.get_input(id).unwrap();
|
||||
assert!(input.properties.iter().any(|(k, v)| k == "color0" && *v == NodeValue::Text("#c0c0c0".into())));
|
||||
assert!(input.properties.iter().any(|(k, v)| k == "color3" && *v == NodeValue::Text("#0000ff".into())));
|
||||
}
|
||||
// Initial white-clamp minimum constraint: black (0.0) + 0.000001.
|
||||
assert_eq!(property(&core, CLAMP_WHITE_INPUT, "min"), Some(NodeValue::Float(0.000001)));
|
||||
assert_eq!(core.effect_input, crate::nodes::ociobase::TEXTURE_INPUT);
|
||||
assert_ne!(core.flags & crate::node::flags::VIDEO_EFFECT, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_clamp_white_minimum_tracks_black_clamp() {
|
||||
let mut core = NodeCore::new();
|
||||
core.add_input(crate::input::Input::new(
|
||||
CLAMP_WHITE_INPUT,
|
||||
crate::value::ValueType::Float,
|
||||
crate::value::NodeValue::Float(1.0),
|
||||
));
|
||||
core.add_input(crate::input::Input::new(
|
||||
CLAMP_BLACK_INPUT,
|
||||
crate::value::ValueType::Float,
|
||||
crate::value::NodeValue::Float(0.0),
|
||||
));
|
||||
core.set_standard_value(CLAMP_BLACK_INPUT, -1, NodeValue::Float(0.25));
|
||||
let mut n = node();
|
||||
n.update_clamp_white_minimum(&mut core);
|
||||
assert_eq!(property(&core, CLAMP_WHITE_INPUT, "min"), Some(NodeValue::Float(0.250001)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_clamp_white_minimum_skips_keyframed_black_clamp() {
|
||||
let mut core = NodeCore::new();
|
||||
core.add_input(crate::input::Input::new(
|
||||
CLAMP_WHITE_INPUT,
|
||||
crate::value::ValueType::Float,
|
||||
crate::value::NodeValue::Float(1.0),
|
||||
));
|
||||
core.add_input(crate::input::Input::new(
|
||||
CLAMP_BLACK_INPUT,
|
||||
crate::value::ValueType::Float,
|
||||
crate::value::NodeValue::Float(0.0),
|
||||
));
|
||||
core.keyframe_track_mut(CLAMP_BLACK_INPUT, -1).set_key(Keyframe {
|
||||
time: Rational::new(0, 1),
|
||||
value: NodeValue::Float(0.5),
|
||||
interpolation: Interpolation::Hold,
|
||||
bezier_in: (0.0, 0.0),
|
||||
bezier_out: (0.0, 0.0),
|
||||
});
|
||||
let mut n = node();
|
||||
n.update_clamp_white_minimum(&mut core);
|
||||
assert_eq!(property(&core, CLAMP_WHITE_INPUT, "min"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_value_changed_mirrors_enable_toggles() {
|
||||
let mut core = NodeCore::new();
|
||||
core.add_input(crate::input::Input::new(
|
||||
CLAMP_BLACK_ENABLE_INPUT,
|
||||
crate::value::ValueType::Boolean,
|
||||
crate::value::NodeValue::Boolean(false),
|
||||
));
|
||||
core.add_input(crate::input::Input::new(
|
||||
CLAMP_BLACK_INPUT,
|
||||
crate::value::ValueType::Float,
|
||||
crate::value::NodeValue::Float(0.0),
|
||||
));
|
||||
core.set_standard_value(CLAMP_BLACK_ENABLE_INPUT, -1, NodeValue::Boolean(true));
|
||||
let mut n = node();
|
||||
n.input_value_changed(&mut core, CLAMP_BLACK_ENABLE_INPUT, 0);
|
||||
assert_eq!(property(&core, CLAMP_BLACK_INPUT, "enabled"), Some(NodeValue::Boolean(true)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_value_changed_black_clamp_reconstrains_white_minimum() {
|
||||
let mut core = NodeCore::new();
|
||||
core.add_input(crate::input::Input::new(
|
||||
CLAMP_BLACK_INPUT,
|
||||
crate::value::ValueType::Float,
|
||||
crate::value::NodeValue::Float(0.0),
|
||||
));
|
||||
core.add_input(crate::input::Input::new(
|
||||
CLAMP_WHITE_INPUT,
|
||||
crate::value::ValueType::Float,
|
||||
crate::value::NodeValue::Float(1.0),
|
||||
));
|
||||
core.set_standard_value(CLAMP_BLACK_INPUT, -1, NodeValue::Float(0.1));
|
||||
let mut n = node();
|
||||
n.input_value_changed(&mut core, CLAMP_BLACK_INPUT, 0);
|
||||
assert_eq!(property(&core, CLAMP_WHITE_INPUT, "min"), Some(NodeValue::Float(0.100001)));
|
||||
}
|
||||
|
||||
#[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_texture_without_processor_pushes_nothing() {
|
||||
let core = NodeCore::new();
|
||||
let n = node();
|
||||
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.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn value_texture_with_processor_pushes_deferred_job() {
|
||||
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 Color Grading (Log)");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user