clippy: clear the workspace errors and apply the machine fixes
- Mark the raw-pointer interop entry points unsafe with # Safety docs (oak-core upload/download/frame-from-pixels, oak-audio convert) and satisfy the existing callers (tests). - mut_from_ref: allow with the ABI contract documented (the handle get_mut helpers in oak-timeline/oak-render/oak-task take the shared reference the C ABI passes; exclusivity is the caller's unsafe contract). - Fix the eq_op in the white-balance normalization (green / green). - Apply cargo clippy --fix across the workspace (redundant closures and field names, field reassignment, items after test modules, ...). - Revert the replace_box fix in image_effect's clip_define: a redefinition must allocate a new box, otherwise the old clip handle stays valid and the HS-map replace contract (clip != clip2) breaks. - 283 warnings remain; they are all non-machine-applicable (chunks_exact -> as_chunks needs a manual iter_mut, too_many_arguments, complex types, missing Safety docs, ...) and are tracked as the follow-up.
This commit is contained in:
@@ -5653,7 +5653,7 @@ mod tests {
|
||||
/// the mock engine's playback ticks with executor time, so a clock
|
||||
/// advance would move the playhead out from under the assertions.
|
||||
fn settle_key(cx: &mut TestAppContext, window: AnyWindowHandle, key: &str) {
|
||||
cx.dispatch_keystroke(window.into(), gpui::Keystroke::parse(key).unwrap());
|
||||
cx.dispatch_keystroke(window, gpui::Keystroke::parse(key).unwrap());
|
||||
cx.run_until_parked();
|
||||
cx.run_until_parked();
|
||||
}
|
||||
|
||||
+167
-179
@@ -36,23 +36,23 @@ use gpui::colors::DefaultColors;
|
||||
use gpui::prelude::*;
|
||||
use gpui::timeline::FrameRate;
|
||||
use gpui::{
|
||||
div, px, App, ClickEvent, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, Keystroke, PathPromptOptions, Render, SharedString, Window,
|
||||
div, px, App, ClickEvent, Context, ElementId, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, Keystroke, PathPromptOptions, Render, SharedString, Window,
|
||||
};
|
||||
use gpui_elements::editable_text::{EditableTextState, StringStorage, TextChanged};
|
||||
|
||||
use crate::actions::ActionId;
|
||||
use crate::i18n;
|
||||
use crate::oakui::real::{
|
||||
audio_input_device, audio_input_devices, audio_output_device, audio_output_devices,
|
||||
config_get_bool, config_get_int, config_get_string, config_set_bool, config_set_int,
|
||||
config_set_string, encoding_formats, proxy_dividers, renderer_backends, set_audio_input_device,
|
||||
set_audio_output_device, set_theme_dark, theme_is_dark, CONFIG_KEY_DEFAULT_TRANSITION_SEC,
|
||||
CONFIG_KEY_DISK_CACHE_PATH, CONFIG_KEY_FFMPEG_PATH, CONFIG_KEY_PG_URL,
|
||||
CONFIG_KEY_PREVIEW_WINDOW, CONFIG_KEY_PROXY_DIVIDER, CONFIG_KEY_RENDERER_BACKEND,
|
||||
CONFIG_KEY_SNAPSHOT_INTERVAL_SEC, CONFIG_KEY_STORAGE_BACKEND, CONFIG_KEY_USE_PROXY,
|
||||
DEFAULT_PREVIEW_WINDOW_FORWARD, DEFAULT_SNAPSHOT_INTERVAL_SEC, DEFAULT_TRANSITION_SEC,
|
||||
EXPORT_FORMAT_MP4,
|
||||
audio_input_device, audio_input_devices, audio_output_device, audio_output_devices,
|
||||
config_get_bool, config_get_int, config_get_string, config_set_bool, config_set_int,
|
||||
config_set_string, encoding_formats, proxy_dividers, renderer_backends, set_audio_input_device,
|
||||
set_audio_output_device, set_theme_dark, theme_is_dark, CONFIG_KEY_DEFAULT_TRANSITION_SEC,
|
||||
CONFIG_KEY_DISK_CACHE_PATH, CONFIG_KEY_FFMPEG_PATH, CONFIG_KEY_PG_URL,
|
||||
CONFIG_KEY_PREVIEW_WINDOW, CONFIG_KEY_PROXY_DIVIDER, CONFIG_KEY_RENDERER_BACKEND,
|
||||
CONFIG_KEY_SNAPSHOT_INTERVAL_SEC, CONFIG_KEY_STORAGE_BACKEND, CONFIG_KEY_USE_PROXY,
|
||||
DEFAULT_PREVIEW_WINDOW_FORWARD, DEFAULT_SNAPSHOT_INTERVAL_SEC, DEFAULT_TRANSITION_SEC,
|
||||
EXPORT_FORMAT_MP4,
|
||||
};
|
||||
// The `DisplayBitDepth` config key lives with the format mapping it
|
||||
// drives (oak-render's backend); the preferences dropdown and the
|
||||
@@ -184,20 +184,22 @@ impl PreferencesContent {
|
||||
];
|
||||
ComboBox::new(12, options, window, cx)
|
||||
});
|
||||
cx.subscribe(&display_bit_depth, |_this, _combo, event: &ComboBoxEvent, cx| {
|
||||
let ComboBoxEvent::Selected { value, .. } = event;
|
||||
let depth = if *value == 1 { "8" } else { "10" };
|
||||
config_set_string(CONFIG_KEY_DISPLAY_BIT_DEPTH, depth);
|
||||
println!("[preferences] display bit depth → {depth}");
|
||||
let _ = cx;
|
||||
})
|
||||
cx.subscribe(
|
||||
&display_bit_depth,
|
||||
|_this, _combo, event: &ComboBoxEvent, cx| {
|
||||
let ComboBoxEvent::Selected { value, .. } = event;
|
||||
let depth = if *value == 1 { "8" } else { "10" };
|
||||
config_set_string(CONFIG_KEY_DISPLAY_BIT_DEPTH, depth);
|
||||
println!("[preferences] display bit depth → {depth}");
|
||||
let _ = cx;
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
let bit_depth_selected =
|
||||
if config_get_string(CONFIG_KEY_DISPLAY_BIT_DEPTH) == "8" {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let bit_depth_selected = if config_get_string(CONFIG_KEY_DISPLAY_BIT_DEPTH) == "8" {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
display_bit_depth.update(cx, |combo, cx| {
|
||||
combo.set_selected(Some(bit_depth_selected), cx)
|
||||
});
|
||||
@@ -269,11 +271,8 @@ impl PreferencesContent {
|
||||
// playhead during playback (`PlaybackPreRenderFrames`, consumed by
|
||||
// `update_preview_window` every playback tick — no restart needed).
|
||||
let cache_ahead = cx.new(|cx| {
|
||||
let current = config_get_int(
|
||||
CONFIG_KEY_PREVIEW_WINDOW,
|
||||
DEFAULT_PREVIEW_WINDOW_FORWARD,
|
||||
)
|
||||
.clamp(8, 1200);
|
||||
let current = config_get_int(CONFIG_KEY_PREVIEW_WINDOW, DEFAULT_PREVIEW_WINDOW_FORWARD)
|
||||
.clamp(8, 1200);
|
||||
SpinBox::new(
|
||||
10,
|
||||
SliderModel::new(ValueKind::Integer, 8.0, 1200.0, 8.0, current as f64),
|
||||
@@ -366,13 +365,13 @@ impl PreferencesContent {
|
||||
})
|
||||
.detach();
|
||||
|
||||
// --- 色彩 Color: display ICC color management -----------------------
|
||||
// On by default: the viewer frames are transformed through the
|
||||
// display's ICC profile (system profile, or a custom file below).
|
||||
// A mode change re-evaluates the platform display policy and
|
||||
// retags the windows immediately (no restart).
|
||||
use crate::oakui::displaycolor::{CONFIG_KEY_COLOR_MODE, CONFIG_KEY_CUSTOM_ICC};
|
||||
let display_icc = cx.new(|cx| {
|
||||
// --- 色彩 Color: display ICC color management -----------------------
|
||||
// On by default: the viewer frames are transformed through the
|
||||
// display's ICC profile (system profile, or a custom file below).
|
||||
// A mode change re-evaluates the platform display policy and
|
||||
// retags the windows immediately (no restart).
|
||||
use crate::oakui::displaycolor::{CONFIG_KEY_COLOR_MODE, CONFIG_KEY_CUSTOM_ICC};
|
||||
let display_icc = cx.new(|cx| {
|
||||
let mode = config_get_string(CONFIG_KEY_COLOR_MODE);
|
||||
CheckBox::new(
|
||||
13,
|
||||
@@ -500,18 +499,21 @@ impl PreferencesContent {
|
||||
];
|
||||
ComboBox::new(11, options, window, cx)
|
||||
});
|
||||
cx.subscribe(&storage_backend, |this, _combo, event: &ComboBoxEvent, cx| {
|
||||
match *event {
|
||||
ComboBoxEvent::Selected { value } => {
|
||||
let backend = if value == 1 { "pg" } else { "sqlite" };
|
||||
this.storage_is_pg = backend == "pg";
|
||||
config_set_string(CONFIG_KEY_STORAGE_BACKEND, backend);
|
||||
// Re-render so the connection-string row shows/hides with
|
||||
// the selection.
|
||||
cx.notify();
|
||||
cx.subscribe(
|
||||
&storage_backend,
|
||||
|this, _combo, event: &ComboBoxEvent, cx| {
|
||||
match *event {
|
||||
ComboBoxEvent::Selected { value } => {
|
||||
let backend = if value == 1 { "pg" } else { "sqlite" };
|
||||
this.storage_is_pg = backend == "pg";
|
||||
config_set_string(CONFIG_KEY_STORAGE_BACKEND, backend);
|
||||
// Re-render so the connection-string row shows/hides with
|
||||
// the selection.
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
storage_backend.update(cx, |combo, cx| {
|
||||
combo.set_selected(Some(if storage_is_pg { 1 } else { 0 }), cx)
|
||||
@@ -1085,8 +1087,7 @@ impl ExportDialogContent {
|
||||
ComboBoxOption::new(0, i18n::tr("export.color.sdr")),
|
||||
ComboBoxOption::new(1, i18n::tr("export.color.hdr")),
|
||||
];
|
||||
ComboBox::new(7, options, window, cx)
|
||||
.with_placeholder(i18n::tr("export.color"))
|
||||
ComboBox::new(7, options, window, cx).with_placeholder(i18n::tr("export.color"))
|
||||
});
|
||||
color.update(cx, |combo, cx| combo.set_selected(Some(0), cx));
|
||||
|
||||
@@ -1095,8 +1096,7 @@ impl ExportDialogContent {
|
||||
ComboBoxOption::new(0, i18n::tr("export.range.all")),
|
||||
ComboBoxOption::new(1, i18n::tr("export.range.inout")),
|
||||
];
|
||||
ComboBox::new(8, options, window, cx)
|
||||
.with_placeholder(i18n::tr("export.range"))
|
||||
ComboBox::new(8, options, window, cx).with_placeholder(i18n::tr("export.range"))
|
||||
});
|
||||
range.update(cx, |combo, cx| combo.set_selected(Some(0), cx));
|
||||
|
||||
@@ -1149,8 +1149,7 @@ impl ExportDialogContent {
|
||||
// an empty list disables the dialog's OK via the picker staying
|
||||
// unselected.
|
||||
let sequence = cx.new(|cx| {
|
||||
ComboBox::new(13, Vec::new(), window, cx)
|
||||
.with_placeholder(i18n::tr("export.sequence"))
|
||||
ComboBox::new(13, Vec::new(), window, cx).with_placeholder(i18n::tr("export.sequence"))
|
||||
});
|
||||
|
||||
Self {
|
||||
@@ -1188,7 +1187,8 @@ impl ExportDialogContent {
|
||||
.enumerate()
|
||||
.map(|(i, (_, name))| ComboBoxOption::new(i, name.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
self.sequence.update(cx, |combo, cx| combo.set_options(options, cx));
|
||||
self.sequence
|
||||
.update(cx, |combo, cx| combo.set_options(options, cx));
|
||||
self.sequence
|
||||
.update(cx, |combo, cx| combo.set_selected(preselect, cx));
|
||||
self.sequences = sequences;
|
||||
@@ -1386,10 +1386,7 @@ impl Render for ExportDialogContent {
|
||||
.flex()
|
||||
.gap_2()
|
||||
.child(self.resolution_w.clone())
|
||||
.child(div()
|
||||
.text_color(colors.disabled)
|
||||
.text_xs()
|
||||
.child("×"))
|
||||
.child(div().text_color(colors.disabled).text_xs().child("×"))
|
||||
.child(self.resolution_h.clone()),
|
||||
))
|
||||
.child(form_row(
|
||||
@@ -1639,7 +1636,11 @@ impl<E: crate::oakui::engine::AppEngine> ProxyDialogContent<E> {
|
||||
config_set_bool("ProxyIncludeAudio", params.include_audio);
|
||||
config_set_int(
|
||||
"ProxyMaxConcurrent",
|
||||
self.max_concurrent.read(cx).value().to_f64().clamp(1.0, 16.0) as i64,
|
||||
self.max_concurrent
|
||||
.read(cx)
|
||||
.value()
|
||||
.to_f64()
|
||||
.clamp(1.0, 16.0) as i64,
|
||||
);
|
||||
config_set_string(
|
||||
CONFIG_KEY_FFMPEG_PATH,
|
||||
@@ -1727,8 +1728,8 @@ fn divider_label(divider: i32) -> String {
|
||||
|
||||
/// The display string of a proxy lifecycle state.
|
||||
fn proxy_state_label(state: crate::oakui::engine::ProxyMediaState) -> String {
|
||||
use crate::oakui::engine::ProxyMediaState;
|
||||
match state {
|
||||
use crate::oakui::engine::ProxyMediaState;
|
||||
match state {
|
||||
ProxyMediaState::Missing => i18n::tr("proxydialog.state.missing"),
|
||||
ProxyMediaState::Generating => i18n::tr("proxydialog.state.generating"),
|
||||
ProxyMediaState::Ready => i18n::tr("proxydialog.state.ready"),
|
||||
@@ -1747,7 +1748,7 @@ impl<E: crate::oakui::engine::AppEngine> Render for ProxyDialogContent<E> {
|
||||
.map(|row| {
|
||||
let mut state = proxy_state_label(row.state);
|
||||
if row.has_custom {
|
||||
state.push_str(&i18n::tr("proxydialog.custom_suffix"));
|
||||
state.push_str(i18n::tr("proxydialog.custom_suffix"));
|
||||
}
|
||||
let enabled =
|
||||
row.enabled && row.state == crate::oakui::engine::ProxyMediaState::Ready;
|
||||
@@ -1960,20 +1961,20 @@ impl<E: crate::oakui::engine::AppEngine> ProjectPropertiesContent<E> {
|
||||
let (working, gamut, transfer) = engine.read(cx).project_color_settings();
|
||||
working_space.update(cx, |combo, cx| {
|
||||
combo.set_selected(
|
||||
Some(oak_core::colormath::WorkingColorSpace::from_setting(&working) as usize),
|
||||
cx,
|
||||
Some(oak_core::colormath::WorkingColorSpace::from_setting(&working) as usize),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
output_gamut.update(cx, |combo, cx| {
|
||||
combo.set_selected(
|
||||
Some(oak_core::colormath::OutputGamut::from_setting(&gamut) as usize),
|
||||
cx,
|
||||
Some(oak_core::colormath::OutputGamut::from_setting(&gamut) as usize),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
output_transfer.update(cx, |combo, cx| {
|
||||
combo.set_selected(
|
||||
Some(oak_core::colormath::OutputTransfer::from_setting(&transfer) as usize),
|
||||
cx,
|
||||
Some(oak_core::colormath::OutputTransfer::from_setting(&transfer) as usize),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -2057,8 +2058,8 @@ impl<E: crate::oakui::engine::AppEngine> ProjectPropertiesContent<E> {
|
||||
/// The color pipeline settings currently selected in the combos, as
|
||||
/// the canonical persisted strings.
|
||||
fn color_settings(&self, cx: &App) -> (String, String, String) {
|
||||
use oak_core::colormath::{OutputGamut, OutputTransfer, WorkingColorSpace};
|
||||
let working = self
|
||||
use oak_core::colormath::{OutputGamut, OutputTransfer, WorkingColorSpace};
|
||||
let working = self
|
||||
.working_space
|
||||
.read(cx)
|
||||
.selected()
|
||||
@@ -2139,7 +2140,7 @@ impl<E: crate::oakui::engine::AppEngine> Render for ProjectPropertiesContent<E>
|
||||
directories: false,
|
||||
multiple: false,
|
||||
prompt: None,
|
||||
allowed_extensions: Vec::new(),
|
||||
allowed_extensions: Vec::new(),
|
||||
});
|
||||
cx.spawn(async move |this, cx| {
|
||||
if let Ok(Ok(Some(paths))) = receiver.await {
|
||||
@@ -2742,7 +2743,7 @@ impl Render for KeyboardTabContent {
|
||||
.child(
|
||||
div()
|
||||
.id(ElementId::named_usize("keyboard-shortcut-capture", index))
|
||||
.debug_selector(move || format!("keyboard-capture-{index}").into())
|
||||
.debug_selector(move || format!("keyboard-capture-{index}"))
|
||||
.min_w(px(150.0))
|
||||
.px_2()
|
||||
.py_0p5()
|
||||
@@ -3348,7 +3349,11 @@ impl Render for TextValue {
|
||||
.bg(colors.background)
|
||||
.px_2()
|
||||
.py_1()
|
||||
.child(text_input("oak-seq-name", cx).state(weak).accepts_input(true))
|
||||
.child(
|
||||
text_input("oak-seq-name", cx)
|
||||
.state(weak)
|
||||
.accepts_input(true),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3388,12 +3393,13 @@ impl SequenceFormatSeed {
|
||||
pub fn from_format(format: &crate::oakui::engine::VideoFormat, interlaced: bool) -> Self {
|
||||
let preset = (1..=SEQUENCE_PRESET_COUNT)
|
||||
.find(|i| {
|
||||
sequence_preset_format(*i) == Some((
|
||||
format.width,
|
||||
format.height,
|
||||
format.rate.num,
|
||||
format.rate.den,
|
||||
))
|
||||
sequence_preset_format(*i)
|
||||
== Some((
|
||||
format.width,
|
||||
format.height,
|
||||
format.rate.num,
|
||||
format.rate.den,
|
||||
))
|
||||
})
|
||||
.unwrap_or(0);
|
||||
let rate = SEQUENCE_RATES
|
||||
@@ -3424,26 +3430,14 @@ pub struct SequenceFormatFields {
|
||||
|
||||
impl SequenceFormatFields {
|
||||
/// Builds the fields and wires the preset / field cross-updates.
|
||||
pub fn build(
|
||||
seed: SequenceFormatSeed,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
pub fn build(seed: SequenceFormatSeed, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let preset = cx.new(|cx| ComboBox::new(40, sequence_preset_options(), window, cx));
|
||||
preset.update(cx, |combo, cx| {
|
||||
combo.set_selected(Some(seed.preset), cx)
|
||||
});
|
||||
preset.update(cx, |combo, cx| combo.set_selected(Some(seed.preset), cx));
|
||||
|
||||
let width = cx.new(|cx| {
|
||||
SpinBox::new(
|
||||
41,
|
||||
SliderModel::new(
|
||||
ValueKind::Integer,
|
||||
16.0,
|
||||
8192.0,
|
||||
2.0,
|
||||
f64::from(seed.width),
|
||||
),
|
||||
SliderModel::new(ValueKind::Integer, 16.0, 8192.0, 2.0, f64::from(seed.width)),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -3504,27 +3498,24 @@ impl SequenceFormatFields {
|
||||
// Editing a dimension or the frame rate reverts to the custom entry.
|
||||
cx.subscribe(&width, |this, _spin, event: &SpinBoxEvent, cx| {
|
||||
if let SpinBoxEvent::ValueChanged { .. } = event {
|
||||
this.preset.update(cx, |combo, cx| {
|
||||
combo.set_selected(Some(0), cx)
|
||||
});
|
||||
this.preset
|
||||
.update(cx, |combo, cx| combo.set_selected(Some(0), cx));
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
cx.subscribe(&height, |this, _spin, event: &SpinBoxEvent, cx| {
|
||||
if let SpinBoxEvent::ValueChanged { .. } = event {
|
||||
this.preset.update(cx, |combo, cx| {
|
||||
combo.set_selected(Some(0), cx)
|
||||
});
|
||||
this.preset
|
||||
.update(cx, |combo, cx| combo.set_selected(Some(0), cx));
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
cx.subscribe(&rate, |this, _combo, event: &ComboBoxEvent, cx| {
|
||||
let ComboBoxEvent::Selected { .. } = event;
|
||||
this.preset.update(cx, |combo, cx| {
|
||||
combo.set_selected(Some(0), cx)
|
||||
});
|
||||
this.preset
|
||||
.update(cx, |combo, cx| combo.set_selected(Some(0), cx));
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
@@ -3709,17 +3700,15 @@ impl<E: crate::oakui::engine::AppEngine> Render for NewSequenceContent<E> {
|
||||
self.name.clone(),
|
||||
))
|
||||
.child(format_rows)
|
||||
.child(
|
||||
if let Some(error) = &self.error {
|
||||
div()
|
||||
.debug_selector(|| "seqprops-error".into())
|
||||
.text_color(gpui::rgb(0xe5484d))
|
||||
.text_xs()
|
||||
.child(error.clone())
|
||||
} else {
|
||||
div()
|
||||
},
|
||||
)
|
||||
.child(if let Some(error) = &self.error {
|
||||
div()
|
||||
.debug_selector(|| "seqprops-error".into())
|
||||
.text_color(gpui::rgb(0xe5484d))
|
||||
.text_xs()
|
||||
.child(error.clone())
|
||||
} else {
|
||||
div()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3818,13 +3807,7 @@ impl<E: crate::oakui::engine::AppEngine> SequencePropertiesContent<E> {
|
||||
let format = self.format(cx);
|
||||
let interlaced = self.interlaced(cx);
|
||||
self.engine.update(cx, |engine, cx| {
|
||||
engine.update_sequence_parameters(
|
||||
self.sequence_id,
|
||||
name,
|
||||
format,
|
||||
interlaced,
|
||||
cx,
|
||||
)
|
||||
engine.update_sequence_parameters(self.sequence_id, name, format, interlaced, cx)
|
||||
})?;
|
||||
self.set_error(None, cx);
|
||||
Ok(())
|
||||
@@ -3858,17 +3841,15 @@ impl<E: crate::oakui::engine::AppEngine> Render for SequencePropertiesContent<E>
|
||||
self.name.clone(),
|
||||
))
|
||||
.child(format_rows)
|
||||
.child(
|
||||
if let Some(error) = &self.error {
|
||||
div()
|
||||
.debug_selector(|| "seqprops-error".into())
|
||||
.text_color(gpui::rgb(0xe5484d))
|
||||
.text_xs()
|
||||
.child(error.clone())
|
||||
} else {
|
||||
div()
|
||||
},
|
||||
)
|
||||
.child(if let Some(error) = &self.error {
|
||||
div()
|
||||
.debug_selector(|| "seqprops-error".into())
|
||||
.text_color(gpui::rgb(0xe5484d))
|
||||
.text_xs()
|
||||
.child(error.clone())
|
||||
} else {
|
||||
div()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3970,11 +3951,7 @@ const WIZARD_SYNC_MODES: &[&str] = &[
|
||||
|
||||
impl<E: crate::oakui::engine::AppEngine> MulticamWizardContent<E> {
|
||||
/// Builds the content seeded with the engine's wizard footage.
|
||||
pub fn new(
|
||||
engine: Entity<E>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
pub fn new(engine: Entity<E>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let name = cx.new(|cx| {
|
||||
let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx));
|
||||
TextValue { editor }
|
||||
@@ -4061,18 +4038,22 @@ impl<E: crate::oakui::engine::AppEngine> Render for MulticamWizardContent<E> {
|
||||
.rounded_sm()
|
||||
.hover(|style| style.bg(colors.container))
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(
|
||||
move |this: &mut Self, _event: &ClickEvent, _window, cx| {
|
||||
.on_click(
|
||||
cx.listener(move |this: &mut Self, _event: &ClickEvent, _window, cx| {
|
||||
this.toggle_row(index, cx);
|
||||
},
|
||||
))
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.size(px(12.0))
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.bg(if is_checked { colors.selected } else { colors.background })
|
||||
.bg(if is_checked {
|
||||
colors.selected
|
||||
} else {
|
||||
colors.background
|
||||
})
|
||||
.text_color(colors.text)
|
||||
.child(if is_checked { "✓" } else { "" }),
|
||||
)
|
||||
@@ -4083,10 +4064,12 @@ impl<E: crate::oakui::engine::AppEngine> Render for MulticamWizardContent<E> {
|
||||
.child(entry.name.clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(colors.disabled)
|
||||
.child(entry.duration_s.map(|d| format!("{d:.1}s")).unwrap_or_default()),
|
||||
div().text_xs().text_color(colors.disabled).child(
|
||||
entry
|
||||
.duration_s
|
||||
.map(|d| format!("{d:.1}s"))
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
);
|
||||
rows_div = rows_div.child(row);
|
||||
}
|
||||
@@ -4157,16 +4140,11 @@ impl RenameContent {
|
||||
impl Render for RenameContent {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let colors = cx.default_colors().clone();
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.w_full()
|
||||
.child(form_row(
|
||||
&colors,
|
||||
i18n::tr("project.context.rename").into(),
|
||||
self.field.clone(),
|
||||
))
|
||||
div().flex().flex_col().gap_3().w_full().child(form_row(
|
||||
&colors,
|
||||
i18n::tr("project.context.rename").into(),
|
||||
self.field.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4193,7 +4171,10 @@ impl ExportProjectDialogContent {
|
||||
let formats: Vec<(i32, String)> = vec![
|
||||
(PROJECT_FORMAT_OTIO, "OpenTimelineIO (.otio)".to_string()),
|
||||
(PROJECT_FORMAT_OVE, "Oak (.ove)".to_string()),
|
||||
(PROJECT_FORMAT_FCPXML, "Final Cut Pro XML (.fcpxml)".to_string()),
|
||||
(
|
||||
PROJECT_FORMAT_FCPXML,
|
||||
"Final Cut Pro XML (.fcpxml)".to_string(),
|
||||
),
|
||||
];
|
||||
let options = formats
|
||||
.iter()
|
||||
@@ -4204,7 +4185,9 @@ impl ExportProjectDialogContent {
|
||||
ComboBox::new(4, options, window, cx)
|
||||
.with_placeholder(i18n::tr("project.export.format"))
|
||||
});
|
||||
format.update(cx, |combo, cx| combo.set_selected(Some(PROJECT_FORMAT_OTIO as usize), cx));
|
||||
format.update(cx, |combo, cx| {
|
||||
combo.set_selected(Some(PROJECT_FORMAT_OTIO as usize), cx)
|
||||
});
|
||||
Self { format, formats }
|
||||
}
|
||||
|
||||
@@ -4232,16 +4215,11 @@ impl ExportProjectDialogContent {
|
||||
impl Render for ExportProjectDialogContent {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let colors = cx.default_colors().clone();
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.w_full()
|
||||
.child(form_row(
|
||||
&colors,
|
||||
i18n::tr("project.export.format").into(),
|
||||
self.format.clone(),
|
||||
))
|
||||
div().flex().flex_col().gap_3().w_full().child(form_row(
|
||||
&colors,
|
||||
i18n::tr("project.export.format").into(),
|
||||
self.format.clone(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4264,9 +4242,8 @@ impl NewProjectContent {
|
||||
name.update(cx, |field, cx| {
|
||||
field.set_value(i18n::tr("manager.new.default_name"), cx)
|
||||
});
|
||||
let format = cx.new(|cx| {
|
||||
SequenceFormatFields::build(SequenceFormatSeed::hd_1080p25(), window, cx)
|
||||
});
|
||||
let format =
|
||||
cx.new(|cx| SequenceFormatFields::build(SequenceFormatSeed::hd_1080p25(), window, cx));
|
||||
Self { name, format }
|
||||
}
|
||||
|
||||
@@ -4305,8 +4282,8 @@ impl Render for NewProjectContent {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
fn keystroke(key: &str) -> Keystroke {
|
||||
use super::*;
|
||||
fn keystroke(key: &str) -> Keystroke {
|
||||
gpui::Keystroke::parse(key).unwrap()
|
||||
}
|
||||
|
||||
@@ -4316,9 +4293,10 @@ mod tests {
|
||||
#[gpui::test]
|
||||
async fn export_dialog_sequence_picker(cx: &mut gpui::TestAppContext) {
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(gpui::size(gpui::px(440.0), gpui::px(400.0)), |window, cx| {
|
||||
ExportDialogContent::new(window, cx)
|
||||
});
|
||||
let window = cx.open_window(
|
||||
gpui::size(gpui::px(440.0), gpui::px(400.0)),
|
||||
ExportDialogContent::new,
|
||||
);
|
||||
cx.run_until_parked();
|
||||
let content = window.root(cx).expect("dialog content root");
|
||||
|
||||
@@ -4349,7 +4327,9 @@ mod tests {
|
||||
);
|
||||
|
||||
cx.update(|cx| {
|
||||
content.update(cx, |content, cx| content.set_sequences(Vec::new(), None, cx));
|
||||
content.update(cx, |content, cx| {
|
||||
content.set_sequences(Vec::new(), None, cx)
|
||||
});
|
||||
});
|
||||
assert_eq!(
|
||||
cx.read(|cx| content.read(cx).selected_sequence(cx)),
|
||||
@@ -4370,8 +4350,16 @@ mod tests {
|
||||
assert_eq!(sequence_preset_format(7), None);
|
||||
// The preset labels come from the language packs (default English
|
||||
// here): the 4K entries say what they set.
|
||||
assert!(labels[5].contains("3840×2160"), "4K UHD label: {}", labels[5]);
|
||||
assert!(labels[6].contains("4096×2160"), "4K DCI label: {}", labels[6]);
|
||||
assert!(
|
||||
labels[5].contains("3840×2160"),
|
||||
"4K UHD label: {}",
|
||||
labels[5]
|
||||
);
|
||||
assert!(
|
||||
labels[6].contains("4096×2160"),
|
||||
"4K DCI label: {}",
|
||||
labels[6]
|
||||
);
|
||||
}
|
||||
|
||||
/// A probed footage format seeds the new-sequence fields: an exact
|
||||
|
||||
+17
-17
@@ -296,6 +296,23 @@ fn reload_for_test() {
|
||||
*tables = load_packs();
|
||||
}
|
||||
|
||||
/// Serializes tests that mutate the language global / HOME.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test_lock() -> std::sync::MutexGuard<'static, ()> {
|
||||
lang_test_lock().lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// Compatibility alias used by the app/actions test modules. Both entry
|
||||
/// points MUST share the one static mutex: with two separate mutexes the
|
||||
/// i18n tests and the app/actions tests do not exclude each other, and
|
||||
/// the language global races (observed as a Windows-only CI failure
|
||||
/// where `tr` returned the English value mid-test).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn lang_test_lock() -> &'static std::sync::Mutex<()> {
|
||||
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
&LOCK
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -454,20 +471,3 @@ mod tests {
|
||||
set_language_code("en-US");
|
||||
}
|
||||
}
|
||||
|
||||
/// Serializes tests that mutate the language global / HOME.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test_lock() -> std::sync::MutexGuard<'static, ()> {
|
||||
lang_test_lock().lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
/// Compatibility alias used by the app/actions test modules. Both entry
|
||||
/// points MUST share the one static mutex: with two separate mutexes the
|
||||
/// i18n tests and the app/actions tests do not exclude each other, and
|
||||
/// the language global races (observed as a Windows-only CI failure
|
||||
/// where `tr` returned the English value mid-test).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn lang_test_lock() -> &'static std::sync::Mutex<()> {
|
||||
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
|
||||
&LOCK
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
//! are unit tested here; the app-level flows are covered by the
|
||||
//! `OakApp<MockEngine>` tests in `crate::app`.
|
||||
|
||||
use crate::oakui::component::text_input;
|
||||
use gpui::colors::DefaultColors;
|
||||
use gpui::prelude::*;
|
||||
use gpui::{
|
||||
@@ -37,7 +38,6 @@ use gpui::{
|
||||
Window,
|
||||
};
|
||||
use gpui_elements::editable_text::{EditableTextState, StringStorage};
|
||||
use crate::oakui::component::text_input;
|
||||
|
||||
use crate::i18n;
|
||||
use crate::oakui::{AppEngine, LibraryProject};
|
||||
@@ -162,7 +162,11 @@ impl<E: AppEngine> ProjectManager<E> {
|
||||
}
|
||||
|
||||
/// An action-row button targeting the selection.
|
||||
fn selected_action(&mut self, action: impl FnOnce(String) -> ManagerEvent, cx: &mut Context<Self>) {
|
||||
fn selected_action(
|
||||
&mut self,
|
||||
action: impl FnOnce(String) -> ManagerEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(uuid) = self.selected_uuid() {
|
||||
self.emit(action(uuid), cx);
|
||||
}
|
||||
@@ -252,13 +256,11 @@ impl<E: AppEngine> Render for ProjectManager<E> {
|
||||
.min_h_0()
|
||||
.overflow_y_scroll()
|
||||
.children(if self.rows.is_empty() {
|
||||
vec![
|
||||
div()
|
||||
.p_4()
|
||||
.text_color(colors.disabled)
|
||||
.child(i18n::tr("manager.empty"))
|
||||
.into_any_element(),
|
||||
]
|
||||
vec![div()
|
||||
.p_4()
|
||||
.text_color(colors.disabled)
|
||||
.child(i18n::tr("manager.empty"))
|
||||
.into_any_element()]
|
||||
} else {
|
||||
self.rows
|
||||
.iter()
|
||||
@@ -272,11 +274,14 @@ impl<E: AppEngine> Render for ProjectManager<E> {
|
||||
.gap_2()
|
||||
.py_1()
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(move |this, event: &ClickEvent, _w, cx| {
|
||||
this.row_clicked(index, event.click_count(), cx);
|
||||
}));
|
||||
.on_click(cx.listener(
|
||||
move |this, event: &ClickEvent, _w, cx| {
|
||||
this.row_clicked(index, event.click_count(), cx);
|
||||
},
|
||||
));
|
||||
if selected {
|
||||
line = line.bg(colors.selected).text_color(colors.selected_text);
|
||||
line =
|
||||
line.bg(colors.selected).text_color(colors.selected_text);
|
||||
} else {
|
||||
line = line.text_color(colors.text);
|
||||
}
|
||||
@@ -301,72 +306,69 @@ impl<E: AppEngine> Render for ProjectManager<E> {
|
||||
}),
|
||||
);
|
||||
|
||||
let mut root = div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.w_full()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
tool("manager-new", i18n::tr("manager.new")).on_click(
|
||||
cx.listener(|this, _e: &ClickEvent, _w, cx| {
|
||||
this.emit(ManagerEvent::Create, cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
tool("manager-import", i18n::tr("manager.import")).on_click(
|
||||
let mut root =
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.w_full()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
tool("manager-new", i18n::tr("manager.new")).on_click(cx.listener(
|
||||
|this, _e: &ClickEvent, _w, cx| {
|
||||
this.emit(ManagerEvent::Create, cx);
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(tool("manager-import", i18n::tr("manager.import")).on_click(
|
||||
cx.listener(|this, _e: &ClickEvent, _w, cx| {
|
||||
this.emit(ManagerEvent::Import, cx);
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(list)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.justify_end()
|
||||
.gap_2()
|
||||
.child(
|
||||
action("manager-open", i18n::tr("manager.open")).on_click(
|
||||
)),
|
||||
)
|
||||
.child(list)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.justify_end()
|
||||
.gap_2()
|
||||
.child(action("manager-open", i18n::tr("manager.open")).on_click(
|
||||
cx.listener(|this, _e: &ClickEvent, _w, cx| {
|
||||
this.selected_action(ManagerEvent::Open, cx);
|
||||
}),
|
||||
))
|
||||
.child(
|
||||
action("manager-rename", i18n::tr("manager.rename")).on_click(
|
||||
cx.listener(|this, _e: &ClickEvent, _w, cx| {
|
||||
this.selected_action(ManagerEvent::Rename, cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
action("manager-duplicate", i18n::tr("manager.duplicate")).on_click(
|
||||
cx.listener(|this, _e: &ClickEvent, _w, cx| {
|
||||
this.selected_action(ManagerEvent::Duplicate, cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
action("manager-delete", i18n::tr("manager.delete")).on_click(
|
||||
cx.listener(|this, _e: &ClickEvent, _w, cx| {
|
||||
this.selected_action(ManagerEvent::Delete, cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
action("manager-export", i18n::tr("manager.export")).on_click(
|
||||
cx.listener(|this, _e: &ClickEvent, _w, cx| {
|
||||
this.selected_action(ManagerEvent::Export, cx);
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
action("manager-rename", i18n::tr("manager.rename")).on_click(
|
||||
cx.listener(|this, _e: &ClickEvent, _w, cx| {
|
||||
this.selected_action(ManagerEvent::Rename, cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
action("manager-duplicate", i18n::tr("manager.duplicate")).on_click(
|
||||
cx.listener(|this, _e: &ClickEvent, _w, cx| {
|
||||
this.selected_action(ManagerEvent::Duplicate, cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
action("manager-delete", i18n::tr("manager.delete")).on_click(
|
||||
cx.listener(|this, _e: &ClickEvent, _w, cx| {
|
||||
this.selected_action(ManagerEvent::Delete, cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
action("manager-export", i18n::tr("manager.export")).on_click(
|
||||
cx.listener(|this, _e: &ClickEvent, _w, cx| {
|
||||
this.selected_action(ManagerEvent::Export, cx);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
if let Some(status) = &self.status {
|
||||
root = root.child(
|
||||
div()
|
||||
@@ -396,10 +398,7 @@ pub struct NamePrompt {
|
||||
impl NamePrompt {
|
||||
/// Builds the prompt seeded with `initial`.
|
||||
pub fn new(initial: &str, cx: &mut Context<Self>) -> Self {
|
||||
let editor = cx.new(|cx| {
|
||||
let editor = EditableTextState::new(StringStorage::default(), cx);
|
||||
editor
|
||||
});
|
||||
let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx));
|
||||
editor.update(cx, |editor, cx| editor.emplace(initial, cx));
|
||||
Self { editor }
|
||||
}
|
||||
@@ -411,7 +410,8 @@ impl NamePrompt {
|
||||
|
||||
/// Replaces the entered name (tests / prefill).
|
||||
pub fn set_value(&mut self, value: &str, cx: &mut Context<Self>) {
|
||||
self.editor.update(cx, |editor, cx| editor.emplace(value, cx));
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.emplace(value, cx));
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,8 @@ impl ContextMenuHandle {
|
||||
|
||||
/// Open the menu at `position` (window coordinates).
|
||||
pub fn show(&self, position: Point<Pixels>, menu: Menu, cx: &mut App) {
|
||||
self.menu.update(cx, |menu_view, cx| menu_view.show(position, menu, cx));
|
||||
self.menu
|
||||
.update(cx, |menu_view, cx| menu_view.show(position, menu, cx));
|
||||
}
|
||||
|
||||
/// The popup entity, to be rendered as a child of the panel so the
|
||||
@@ -126,7 +127,8 @@ pub const LANG_ITEM_COUNT: usize = 32;
|
||||
/// Maps a menu item id in the language range back to its index into
|
||||
/// [`crate::i18n::available_languages`].
|
||||
pub fn language_item_index(item: usize) -> Option<usize> {
|
||||
(item >= LANG_ITEM_BASE && item < LANG_ITEM_BASE + LANG_ITEM_COUNT)
|
||||
(LANG_ITEM_BASE..LANG_ITEM_BASE + LANG_ITEM_COUNT)
|
||||
.contains(&item)
|
||||
.then(|| item - LANG_ITEM_BASE)
|
||||
}
|
||||
|
||||
@@ -262,7 +264,8 @@ pub fn color_label_item(selected: Option<usize>) -> MenuItem {
|
||||
/// The color index (`0..COLOR_LABEL_COUNT`) behind a triggered menu item id,
|
||||
/// when `item` is one of the color-label items.
|
||||
pub fn color_label_index(item: usize) -> Option<usize> {
|
||||
(item >= COLOR_LABEL_BASE && item < COLOR_LABEL_BASE + COLOR_LABEL_COUNT)
|
||||
(COLOR_LABEL_BASE..COLOR_LABEL_BASE + COLOR_LABEL_COUNT)
|
||||
.contains(&item)
|
||||
.then(|| item - COLOR_LABEL_BASE)
|
||||
}
|
||||
|
||||
@@ -334,12 +337,17 @@ impl Default for ViewerMenuState {
|
||||
pub fn viewer_menu(state: &ViewerMenuState) -> Menu {
|
||||
use crate::i18n::tr;
|
||||
// Zoom: Fit + one entry per zoom level, checked against the live state.
|
||||
let mut zoom_items = vec![MenuItem::new(LOCAL_VIEWER_ZOOM_FIT, tr("viewer.context.zoom_fit"))
|
||||
.with_checked(state.zoom == ViewerZoom::Fit)];
|
||||
let mut zoom_items = vec![
|
||||
MenuItem::new(LOCAL_VIEWER_ZOOM_FIT, tr("viewer.context.zoom_fit"))
|
||||
.with_checked(state.zoom == ViewerZoom::Fit),
|
||||
];
|
||||
for (index, level) in VIEWER_ZOOM_LEVELS.iter().enumerate() {
|
||||
zoom_items.push(
|
||||
MenuItem::new(viewer_zoom_level_id(index), format!("{:.0}%", level * 100.0))
|
||||
.with_checked(state.zoom == ViewerZoom::Level(index)),
|
||||
MenuItem::new(
|
||||
viewer_zoom_level_id(index),
|
||||
format!("{:.0}%", level * 100.0),
|
||||
)
|
||||
.with_checked(state.zoom == ViewerZoom::Level(index)),
|
||||
);
|
||||
}
|
||||
// Playback Resolution radio group (the C++ `PlaybackDivider` config):
|
||||
@@ -376,9 +384,10 @@ pub fn viewer_menu(state: &ViewerMenuState) -> Menu {
|
||||
Menu::new(vec![
|
||||
MenuItem::new(0, tr("viewer.context.zoom")).with_submenu(Menu::new(zoom_items)),
|
||||
MenuItem::new(LOCAL_VIEWER_FULL_SCREEN, tr("viewer.context.full_screen")),
|
||||
MenuItem::new(0, tr("viewer.context.playback_resolution"))
|
||||
.with_submenu(resolution_menu),
|
||||
MenuItem::new(0, tr("viewer.context.safe_margins")).with_submenu(safe_menu).separated(),
|
||||
MenuItem::new(0, tr("viewer.context.playback_resolution")).with_submenu(resolution_menu),
|
||||
MenuItem::new(0, tr("viewer.context.safe_margins"))
|
||||
.with_submenu(safe_menu)
|
||||
.separated(),
|
||||
MenuItem::new(LOCAL_VIEWER_STOP_ON_LAST, tr("viewer.context.stop_on_last"))
|
||||
.with_checked(state.stop_on_last)
|
||||
.separated(),
|
||||
@@ -426,7 +435,9 @@ pub fn viewer_menu_action(item: usize) -> Option<ViewerMenuAction> {
|
||||
} else if item >= LOCAL_VIEWER_ZOOM_LEVELS_BASE
|
||||
&& item < LOCAL_VIEWER_ZOOM_LEVELS_BASE + VIEWER_ZOOM_LEVELS.len()
|
||||
{
|
||||
Some(ViewerMenuAction::ZoomLevel(item - LOCAL_VIEWER_ZOOM_LEVELS_BASE))
|
||||
Some(ViewerMenuAction::ZoomLevel(
|
||||
item - LOCAL_VIEWER_ZOOM_LEVELS_BASE,
|
||||
))
|
||||
} else {
|
||||
match item {
|
||||
LOCAL_VIEWER_FULL_SCREEN => Some(ViewerMenuAction::FullScreen),
|
||||
@@ -585,7 +596,9 @@ mod tests {
|
||||
fn viewer_menu_offers_every_zoom_level() {
|
||||
// The label lookups below race with tests that flip the process
|
||||
// language: pin en-US under the shared lock.
|
||||
let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _guard = crate::i18n::lang_test_lock()
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
crate::i18n::set_language_code("en-US");
|
||||
let menu = viewer_menu(&ViewerMenuState::default());
|
||||
let zoom = menu
|
||||
@@ -599,7 +612,10 @@ mod tests {
|
||||
assert_eq!(zoom_items[0].checked, Some(true), "Fit checked by default");
|
||||
for (index, level) in VIEWER_ZOOM_LEVELS.iter().enumerate() {
|
||||
assert_eq!(zoom_items[index + 1].id, viewer_zoom_level_id(index));
|
||||
assert_eq!(zoom_items[index + 1].label, format!("{:.0}%", level * 100.0));
|
||||
assert_eq!(
|
||||
zoom_items[index + 1].label,
|
||||
format!("{:.0}%", level * 100.0)
|
||||
);
|
||||
assert_eq!(
|
||||
zoom_items[index + 1].checked,
|
||||
Some(false),
|
||||
@@ -614,7 +630,9 @@ mod tests {
|
||||
fn viewer_menu_radio_groups_default_to_the_first_entry() {
|
||||
// The label lookups below race with tests that flip the process
|
||||
// language: pin en-US under the shared lock.
|
||||
let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _guard = crate::i18n::lang_test_lock()
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
crate::i18n::set_language_code("en-US");
|
||||
let menu = viewer_menu(&ViewerMenuState::default());
|
||||
for label_key in [
|
||||
@@ -630,7 +648,9 @@ mod tests {
|
||||
let sub = item.submenu.as_ref().unwrap();
|
||||
assert_eq!(sub.items[0].checked, Some(true), "{label_key} default");
|
||||
assert!(
|
||||
sub.items[1..].iter().all(|item| item.checked == Some(false)),
|
||||
sub.items[1..]
|
||||
.iter()
|
||||
.all(|item| item.checked == Some(false)),
|
||||
"{label_key} non-defaults unchecked"
|
||||
);
|
||||
}
|
||||
@@ -640,7 +660,9 @@ mod tests {
|
||||
#[test]
|
||||
fn viewer_menu_resolution_radio_follows_the_divider() {
|
||||
// Label lookup: pin en-US under the shared language lock.
|
||||
let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _guard = crate::i18n::lang_test_lock()
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
crate::i18n::set_language_code("en-US");
|
||||
let menu = viewer_menu(&ViewerMenuState {
|
||||
playback_divider: 4,
|
||||
@@ -662,7 +684,9 @@ mod tests {
|
||||
#[test]
|
||||
fn viewer_menu_reflects_live_state() {
|
||||
// Label lookup: pin en-US under the shared language lock.
|
||||
let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _guard = crate::i18n::lang_test_lock()
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
crate::i18n::set_language_code("en-US");
|
||||
let menu = viewer_menu(&ViewerMenuState {
|
||||
playback_divider: 2,
|
||||
@@ -721,7 +745,10 @@ mod tests {
|
||||
None,
|
||||
"zoom id past the last level"
|
||||
);
|
||||
assert_eq!(viewer_menu_action(LOCAL_VIEWER_FULL_SCREEN), Some(V::FullScreen));
|
||||
assert_eq!(
|
||||
viewer_menu_action(LOCAL_VIEWER_FULL_SCREEN),
|
||||
Some(V::FullScreen)
|
||||
);
|
||||
for (id, divider) in [
|
||||
(LOCAL_VIEWER_RES_FULL, 1),
|
||||
(LOCAL_VIEWER_RES_HALF, 2),
|
||||
@@ -732,8 +759,14 @@ mod tests {
|
||||
}
|
||||
assert_eq!(viewer_menu_action(LOCAL_VIEWER_SAFE_OFF), Some(V::SafeOff));
|
||||
assert_eq!(viewer_menu_action(LOCAL_VIEWER_SAFE_ON), Some(V::SafeOn));
|
||||
assert_eq!(viewer_menu_action(LOCAL_VIEWER_SAFE_CUSTOM), Some(V::SafeCustom));
|
||||
assert_eq!(viewer_menu_action(LOCAL_VIEWER_STOP_ON_LAST), Some(V::StopOnLast));
|
||||
assert_eq!(
|
||||
viewer_menu_action(LOCAL_VIEWER_SAFE_CUSTOM),
|
||||
Some(V::SafeCustom)
|
||||
);
|
||||
assert_eq!(
|
||||
viewer_menu_action(LOCAL_VIEWER_STOP_ON_LAST),
|
||||
Some(V::StopOnLast)
|
||||
);
|
||||
assert_eq!(
|
||||
viewer_menu_action(LOCAL_VIEWER_WF_AUTOMATIC),
|
||||
Some(V::Waveform(WaveformMode::Automatic))
|
||||
@@ -747,8 +780,15 @@ mod tests {
|
||||
Some(V::Waveform(WaveformMode::Both))
|
||||
);
|
||||
assert_eq!(viewer_menu_action(LOCAL_VIEWER_SHOW_FPS), Some(V::ShowFps));
|
||||
assert_eq!(viewer_menu_action(LOCAL_VIEWER_SAVE_FRAME), Some(V::SaveFrame));
|
||||
assert_eq!(viewer_menu_action(0), None, "registry-range id is not local");
|
||||
assert_eq!(
|
||||
viewer_menu_action(LOCAL_VIEWER_SAVE_FRAME),
|
||||
Some(V::SaveFrame)
|
||||
);
|
||||
assert_eq!(
|
||||
viewer_menu_action(0),
|
||||
None,
|
||||
"registry-range id is not local"
|
||||
);
|
||||
assert_eq!(viewer_menu_action(LOCAL_ID_BASE), None, "color-label id");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,9 +82,9 @@
|
||||
use std::sync::{Arc, LazyLock, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use oak_core::color::{pipeline_output_spec, ColorProcessor};
|
||||
use oak_core::colormath::{output_spec_to_xyz_d65, OutputColorSpec, OutputGamut, OutputTransfer};
|
||||
use oak_core::configstore::ConfigStore;
|
||||
use oak_core::color::{pipeline_output_spec, ColorProcessor};
|
||||
|
||||
/// Config key: the display color management mode ("icc" / "off"). The
|
||||
/// preference only applies where the platform policy allows self-management
|
||||
@@ -167,9 +167,7 @@ pub fn display_policy() -> DisplayPolicy {
|
||||
"os" => return DisplayPolicy::OsManaged,
|
||||
"self" => return DisplayPolicy::SelfManaged,
|
||||
other => {
|
||||
log::warn!(
|
||||
"OAK_DISPLAY_POLICY={other:?} ignored (expected \"os\" or \"self\")"
|
||||
);
|
||||
log::warn!("OAK_DISPLAY_POLICY={other:?} ignored (expected \"os\" or \"self\")");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,7 +206,10 @@ pub fn display_policy() -> DisplayPolicy {
|
||||
// mapping, so self-management is the only way to honor wide-gamut
|
||||
// displays there (the "off" preference opts back into OS-management).
|
||||
#[cfg(target_os = "macos")]
|
||||
match ConfigStore::instance().get(None, CONFIG_KEY_COLOR_MODE).as_deref() {
|
||||
match ConfigStore::instance()
|
||||
.get(None, CONFIG_KEY_COLOR_MODE)
|
||||
.as_deref()
|
||||
{
|
||||
Ok("icc") => DisplayPolicy::SelfManaged,
|
||||
_ => DisplayPolicy::OsManaged,
|
||||
}
|
||||
@@ -288,7 +289,7 @@ fn current_monitor_fingerprint(window: &gpui::Window, cx: &gpui::App) -> Option<
|
||||
let center = window.bounds().center();
|
||||
let x = f64::from(center.x) * f64::from(window.scale_factor());
|
||||
let y = f64::from(center.y) * f64::from(window.scale_factor());
|
||||
return oak_core::displayicc::x11_monitor_fingerprint_at(x, y);
|
||||
oak_core::displayicc::x11_monitor_fingerprint_at(x, y)
|
||||
}
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
|
||||
{
|
||||
|
||||
@@ -90,7 +90,7 @@ fn build_display_lut() -> oak_core::lut::Lut3d {
|
||||
);
|
||||
super::displaycolor::apply_f32_rgba(&mut samples, n as i64);
|
||||
let mut data = Vec::with_capacity(n * 3);
|
||||
for px in samples.chunks_exact(4) {
|
||||
for px in samples.as_chunks::<4>().0 {
|
||||
data.extend_from_slice(&px[..3]);
|
||||
}
|
||||
oak_core::lut::Lut3d { edge, lo, hi, data }
|
||||
@@ -128,7 +128,8 @@ pub fn register_context(device: std::sync::Arc<wgpu::Device>, queue: std::sync::
|
||||
if let Ok(mut ctx) = GPU_CONTEXT.lock() {
|
||||
*ctx = Some((device.clone(), queue.clone()));
|
||||
}
|
||||
let adopted = oak_core::backend::GpuContext::adopt(device, queue, oak_core::backend::BackendKind::Auto);
|
||||
let adopted =
|
||||
oak_core::backend::GpuContext::adopt(device, queue, oak_core::backend::BackendKind::Auto);
|
||||
if !oak_core::backend::GpuContext::install_shared(Some(adopted)) {
|
||||
// The engine context was already used for GPU work before the
|
||||
// window opened: it cannot be replaced, so present falls back to
|
||||
@@ -193,8 +194,7 @@ pub fn upload_rgba16f(
|
||||
let ctx = GPU_CONTEXT.lock().ok()?;
|
||||
ctx.as_ref().map(|(d, q)| (d.clone(), q.clone()))?
|
||||
};
|
||||
let (bytes, bytes_per_row) =
|
||||
super::frames::f32_rgba_to_16f_bytes(width, height, samples)?;
|
||||
let (bytes, bytes_per_row) = super::frames::f32_rgba_to_16f_bytes(width, height, samples)?;
|
||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("oak_display_rgba16f"),
|
||||
size: wgpu::Extent3d {
|
||||
|
||||
@@ -699,12 +699,11 @@ pub fn ts_to_rational(ts: i64, tb: (i64, i64)) -> Rational {
|
||||
|
||||
/// The sequence's track list of `kind`, when it exists.
|
||||
pub fn track_list_of(g: &Graph, seq: NodeId, kind: TrackType) -> Option<NodeId> {
|
||||
for &list_id in &sequence_behavior(g, seq)?.track_lists {
|
||||
if track_list_behavior(g, list_id).map(|l| l.kind) == Some(kind) {
|
||||
return Some(list_id);
|
||||
}
|
||||
}
|
||||
None
|
||||
sequence_behavior(g, seq)?
|
||||
.track_lists
|
||||
.iter()
|
||||
.find(|&&list_id| track_list_behavior(g, list_id).map(|l| l.kind) == Some(kind))
|
||||
.copied()
|
||||
}
|
||||
|
||||
/// Find (or create) the sequence's track list of `kind` (the facade's
|
||||
@@ -1700,7 +1699,12 @@ pub fn create_adjustment_layer(
|
||||
let index = track_list_behavior(&g.graph, list)
|
||||
.and_then(|l| l.tracks.iter().position(|&t| t == track))
|
||||
.ok_or_else(|| "the track is not in its list".to_string())?;
|
||||
(list, index, ts_to_rational(in_ts, tb), ts_to_rational(out_ts, tb))
|
||||
(
|
||||
list,
|
||||
index,
|
||||
ts_to_rational(in_ts, tb),
|
||||
ts_to_rational(out_ts, tb),
|
||||
)
|
||||
};
|
||||
|
||||
// The block itself: no label, no color override, no media wiring —
|
||||
@@ -2370,7 +2374,12 @@ pub fn place_generator_clip(
|
||||
in_r,
|
||||
)
|
||||
.to_command();
|
||||
let edge = connect_command(p, generator, clip, oak_node::block::clip_input::TEXTURE_INPUT)?;
|
||||
let edge = connect_command(
|
||||
p,
|
||||
generator,
|
||||
clip,
|
||||
oak_node::block::clip_input::TEXTURE_INPUT,
|
||||
)?;
|
||||
push_multi(vec![place, edge], "Add Clip")?;
|
||||
Ok(clip)
|
||||
}
|
||||
@@ -4557,7 +4566,15 @@ mod undo_cycle_ops_tests {
|
||||
/// tests use.
|
||||
fn two_touching_clips(
|
||||
media: &std::path::Path,
|
||||
) -> (ProjectRef, NodeId, NodeId, NodeId, NodeId, (i64, i64), Rational) {
|
||||
) -> (
|
||||
ProjectRef,
|
||||
NodeId,
|
||||
NodeId,
|
||||
NodeId,
|
||||
NodeId,
|
||||
(i64, i64),
|
||||
Rational,
|
||||
) {
|
||||
let project = create_project();
|
||||
let seq = create_sequence(&project, "Default Transition");
|
||||
let footage = import_footage(&project, media).expect("import");
|
||||
@@ -4586,8 +4603,8 @@ mod undo_cycle_ops_tests {
|
||||
fn default_transition_covers_both_ends_of_a_lone_clip() {
|
||||
let _g = test_lock();
|
||||
oak_undo::global::clear().unwrap();
|
||||
let media = std::env::temp_dir()
|
||||
.join(format!("oak_lone_transition_{}.mp4", std::process::id()));
|
||||
let media =
|
||||
std::env::temp_dir().join(format!("oak_lone_transition_{}.mp4", std::process::id()));
|
||||
oak_codec::testmedia::write_test_clip(&media, 64, 64, 10, 10).expect("generate");
|
||||
let project = create_project();
|
||||
let seq = create_sequence(&project, "Lone Transition");
|
||||
@@ -4604,10 +4621,14 @@ mod undo_cycle_ops_tests {
|
||||
let half = ts_to_rational(3, tb);
|
||||
|
||||
let before = oak_undo::global::count().unwrap();
|
||||
let created = add_default_transition(&project, seq, &[clip], half)
|
||||
.expect("add default transition");
|
||||
let created =
|
||||
add_default_transition(&project, seq, &[clip], half).expect("add default transition");
|
||||
assert_eq!(created, 2, "a lone clip takes both ends");
|
||||
assert_eq!(oak_undo::global::count().unwrap(), before + 1, "one undo row");
|
||||
assert_eq!(
|
||||
oak_undo::global::count().unwrap(),
|
||||
before + 1,
|
||||
"one undo row"
|
||||
);
|
||||
|
||||
let edge = half + half;
|
||||
{
|
||||
@@ -4615,7 +4636,10 @@ mod undo_cycle_ops_tests {
|
||||
let blocks = track_behavior(&g.graph, track)
|
||||
.map(|t| t.blocks.clone())
|
||||
.unwrap_or_default();
|
||||
let index = blocks.iter().position(|&b| b == clip).expect("clip on track");
|
||||
let index = blocks
|
||||
.iter()
|
||||
.position(|&b| b == clip)
|
||||
.expect("clip on track");
|
||||
// (A leading gap when the clip starts past zero,) the head
|
||||
// transition, the clip, the tail transition.
|
||||
let (head, tail) = (blocks[index - 1], blocks[index + 1]);
|
||||
@@ -4636,12 +4660,14 @@ mod undo_cycle_ops_tests {
|
||||
"the head transition spans the clip's head"
|
||||
);
|
||||
assert_eq!(
|
||||
g.graph.connected_output(head, oak_node::block::transition_input::IN_BLOCK, -1),
|
||||
g.graph
|
||||
.connected_output(head, oak_node::block::transition_input::IN_BLOCK, -1),
|
||||
Some(clip),
|
||||
"the head transition wires only the clip in"
|
||||
);
|
||||
assert_eq!(
|
||||
g.graph.connected_output(head, oak_node::block::transition_input::OUT_BLOCK, -1),
|
||||
g.graph
|
||||
.connected_output(head, oak_node::block::transition_input::OUT_BLOCK, -1),
|
||||
None,
|
||||
"the head transition has no outgoing side"
|
||||
);
|
||||
@@ -4662,12 +4688,14 @@ mod undo_cycle_ops_tests {
|
||||
"the tail transition spans the clip's tail"
|
||||
);
|
||||
assert_eq!(
|
||||
g.graph.connected_output(tail, oak_node::block::transition_input::OUT_BLOCK, -1),
|
||||
g.graph
|
||||
.connected_output(tail, oak_node::block::transition_input::OUT_BLOCK, -1),
|
||||
Some(clip),
|
||||
"the tail transition wires only the clip out"
|
||||
);
|
||||
assert_eq!(
|
||||
g.graph.connected_output(tail, oak_node::block::transition_input::IN_BLOCK, -1),
|
||||
g.graph
|
||||
.connected_output(tail, oak_node::block::transition_input::IN_BLOCK, -1),
|
||||
None,
|
||||
"the tail transition has no incoming side"
|
||||
);
|
||||
@@ -4694,8 +4722,8 @@ mod undo_cycle_ops_tests {
|
||||
fn default_transition_covers_the_selected_seams() {
|
||||
let _g = test_lock();
|
||||
oak_undo::global::clear().unwrap();
|
||||
let media = std::env::temp_dir()
|
||||
.join(format!("oak_default_transition_{}.mp4", std::process::id()));
|
||||
let media =
|
||||
std::env::temp_dir().join(format!("oak_default_transition_{}.mp4", std::process::id()));
|
||||
oak_codec::testmedia::write_test_clip(&media, 64, 64, 10, 10).expect("generate");
|
||||
let (project, seq, track, a, b, tb, half) = two_touching_clips(&media);
|
||||
|
||||
@@ -4716,7 +4744,11 @@ mod undo_cycle_ops_tests {
|
||||
let blocks = track_behavior(&g.graph, track)
|
||||
.map(|t| t.blocks.clone())
|
||||
.unwrap_or_default();
|
||||
assert_eq!(blocks.len(), 3, "the transition joins the track: {blocks:?}");
|
||||
assert_eq!(
|
||||
blocks.len(),
|
||||
3,
|
||||
"the transition joins the track: {blocks:?}"
|
||||
);
|
||||
let t = blocks[1];
|
||||
let seam = ts_to_rational(10, tb);
|
||||
let core = block_core_of(&g.graph, t).expect("transition core");
|
||||
@@ -4737,12 +4769,14 @@ mod undo_cycle_ops_tests {
|
||||
"a default transition is symmetric"
|
||||
);
|
||||
assert_eq!(
|
||||
g.graph.connected_output(t, oak_node::block::transition_input::OUT_BLOCK, -1),
|
||||
g.graph
|
||||
.connected_output(t, oak_node::block::transition_input::OUT_BLOCK, -1),
|
||||
Some(a),
|
||||
"the previous clip feeds out_block_in"
|
||||
);
|
||||
assert_eq!(
|
||||
g.graph.connected_output(t, oak_node::block::transition_input::IN_BLOCK, -1),
|
||||
g.graph
|
||||
.connected_output(t, oak_node::block::transition_input::IN_BLOCK, -1),
|
||||
Some(b),
|
||||
"the next clip feeds in_block_in"
|
||||
);
|
||||
@@ -4787,7 +4821,10 @@ mod undo_cycle_ops_tests {
|
||||
assert_eq!(oak_undo::global::count().unwrap(), rows);
|
||||
{
|
||||
let g = lock(&project);
|
||||
assert!(g.graph.is_valid(transition), "the refused batch changed nothing");
|
||||
assert!(
|
||||
g.graph.is_valid(transition),
|
||||
"the refused batch changed nothing"
|
||||
);
|
||||
}
|
||||
|
||||
oak_undo::global::clear().unwrap();
|
||||
@@ -4844,8 +4881,8 @@ mod undo_cycle_ops_tests {
|
||||
cycle_assert(&project, seq, &post, "transition length");
|
||||
|
||||
// A request longer than the dragged clip is clamped to its length.
|
||||
let applied =
|
||||
set_transition_length(&project, b, true, frame, ts_to_rational(9_999, tb)).expect("clamp");
|
||||
let applied = set_transition_length(&project, b, true, frame, ts_to_rational(9_999, tb))
|
||||
.expect("clamp");
|
||||
assert_eq!(
|
||||
applied,
|
||||
ts_to_rational(10, tb),
|
||||
|
||||
@@ -43,19 +43,19 @@ use std::sync::{Arc, Mutex};
|
||||
use std::time::Instant;
|
||||
|
||||
use gpui::effect_stack::{
|
||||
EffectCardKind, EffectData, EffectId, EffectStackDataSource, EffectStackEvent,
|
||||
EffectCardKind, EffectData, EffectId, EffectStackDataSource, EffectStackEvent,
|
||||
};
|
||||
use gpui::node_graph::{
|
||||
EdgeData, EdgeId, NodeData, NodeGraphDataSource, NodeGraphEvent, NodeId, PortData,
|
||||
PortDataType, PortId, PortKind,
|
||||
EdgeData, EdgeId, NodeData, NodeGraphDataSource, NodeGraphEvent, NodeId, PortData,
|
||||
PortDataType, PortId, PortKind,
|
||||
};
|
||||
use gpui::timeline::{
|
||||
ClipData, ClipId, Frame, FrameRange, FrameRate, Marker, TimelineDataSource, TimelineEvent,
|
||||
TrackData, TrackKind,
|
||||
ClipData, ClipId, Frame, FrameRange, FrameRate, Marker, TimelineDataSource, TimelineEvent,
|
||||
TrackData, TrackKind,
|
||||
};
|
||||
use gpui::{
|
||||
hsla, point, prelude::*, px, App, Context, Entity, Hsla, Pixels, Point, RenderImage,
|
||||
SharedString,
|
||||
hsla, point, prelude::*, px, App, Context, Entity, Hsla, Pixels, Point, RenderImage,
|
||||
SharedString,
|
||||
};
|
||||
use gpui_widgets::audio_meter::AudioMeterDataSource;
|
||||
use gpui_widgets::project_explorer::{ProjectDataSource, ProjectEntry};
|
||||
@@ -67,8 +67,8 @@ use oak_node::track::TrackType;
|
||||
use oak_timeline::util::{block_clip_create, track_append_block};
|
||||
|
||||
use super::engine::{
|
||||
AppEngine, EngineGateway, ExportEvent, ExportSession, LibraryProject, Monitor, MulticamState,
|
||||
Project, ScopeData, Sequence, VideoFormat, WizardFootage, WizardSyncOffset,
|
||||
AppEngine, EngineGateway, ExportEvent, ExportSession, LibraryProject, Monitor, MulticamState,
|
||||
Project, ScopeData, Sequence, VideoFormat, WizardFootage, WizardSyncOffset,
|
||||
};
|
||||
use super::graphops;
|
||||
use super::nodegraph::GraphEndpoint;
|
||||
@@ -1001,7 +1001,7 @@ impl MockEngine {
|
||||
NodeGraphEvent::NodeMoveRequested { nodes, delta } => {
|
||||
for id in nodes {
|
||||
if let Some(node) = self.nodes.iter_mut().find(|n| n.id() == *id) {
|
||||
node.position = node.position + *delta;
|
||||
node.position += *delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2099,8 +2099,7 @@ impl AppEngine for MockEngine {
|
||||
.is_some_and(|t| t.kind == TrackKind::Video)
|
||||
{
|
||||
track_index
|
||||
} else if let Some(index) = self.tracks.iter().position(|t| t.kind == TrackKind::Video)
|
||||
{
|
||||
} else if let Some(index) = self.tracks.iter().position(|t| t.kind == TrackKind::Video) {
|
||||
index
|
||||
} else {
|
||||
return Err("no video track".to_string());
|
||||
@@ -2494,7 +2493,7 @@ impl AppEngine for MockEngine {
|
||||
if entry.is_dir {
|
||||
continue;
|
||||
}
|
||||
let is_audio = crate::oakui::filename_is_audio(&entry.name.to_string());
|
||||
let is_audio = crate::oakui::filename_is_audio(entry.name.as_ref());
|
||||
let state = self
|
||||
.proxy_states
|
||||
.get(&entry.id)
|
||||
@@ -2916,7 +2915,11 @@ impl ProjectDataSource for MockEngine {
|
||||
// shared node label `文本`, so the demo's browser stays locale-
|
||||
// independent).
|
||||
for &id in &self.text_footages {
|
||||
roots.push(ProjectEntry::new(id, super::graphops::TEXT_FOOTAGE_LABEL, false));
|
||||
roots.push(ProjectEntry::new(
|
||||
id,
|
||||
super::graphops::TEXT_FOOTAGE_LABEL,
|
||||
false,
|
||||
));
|
||||
}
|
||||
roots
|
||||
}
|
||||
@@ -2969,11 +2972,11 @@ impl DemoMulticamGraph {
|
||||
/// tracks are built directly in the graph (no `Add Track` undo entries —
|
||||
/// the demo's initial state is not a user edit).
|
||||
fn build() -> Self {
|
||||
use oak_node::node::NodeCore;
|
||||
use oak_node::sequence::SequenceBehavior;
|
||||
use oak_node::track::{TrackBehavior, TrackListBehavior};
|
||||
use oak_node::node::NodeCore;
|
||||
use oak_node::sequence::SequenceBehavior;
|
||||
use oak_node::track::{TrackBehavior, TrackListBehavior};
|
||||
|
||||
let project = graphops::create_project();
|
||||
let project = graphops::create_project();
|
||||
let sequence = graphops::create_sequence(&project, "Multicam Demo");
|
||||
// A video track list with four tracks, wired into the sequence.
|
||||
{
|
||||
@@ -3257,12 +3260,12 @@ impl MockEngine {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
use gpui::timeline::TrimEdge;
|
||||
use super::*;
|
||||
use gpui::timeline::TrimEdge;
|
||||
use gpui::TestAppContext;
|
||||
|
||||
fn demo_engine(app: &mut gpui::App) -> Entity<MockEngine> {
|
||||
app.new(|cx| MockEngine::demo(cx))
|
||||
fn demo_engine(app: &mut gpui::App) -> Entity<MockEngine> {
|
||||
app.new(MockEngine::demo)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -3327,8 +3330,8 @@ mod tests {
|
||||
/// The stop-on-last tick pauses on the final frame instead of wrapping.
|
||||
#[test]
|
||||
fn clock_tick_stops_on_the_last_frame_when_asked() {
|
||||
use std::time::{Duration, Instant};
|
||||
let mut clock = MockClock::new(FrameRate::new(30, 1));
|
||||
use std::time::{Duration, Instant};
|
||||
let mut clock = MockClock::new(FrameRate::new(30, 1));
|
||||
clock.play();
|
||||
// 10 s at 30 fps = 300 frames into a 5-frame sequence: wrapped 60×.
|
||||
clock.started = Some((Instant::now() - Duration::from_secs(10), Frame(0)));
|
||||
@@ -3348,8 +3351,8 @@ mod tests {
|
||||
/// Without stop-on-last the tick wraps modulo the sequence length.
|
||||
#[test]
|
||||
fn clock_tick_loops_when_not_stopping() {
|
||||
use std::time::{Duration, Instant};
|
||||
let mut clock = MockClock::new(FrameRate::new(30, 1));
|
||||
use std::time::{Duration, Instant};
|
||||
let mut clock = MockClock::new(FrameRate::new(30, 1));
|
||||
clock.play();
|
||||
clock.started = Some((Instant::now() - Duration::from_secs(10), Frame(0)));
|
||||
|
||||
@@ -3973,9 +3976,9 @@ mod tests {
|
||||
.collect();
|
||||
assert_eq!(listed.len(), 2, "both text entries are listed in the bin");
|
||||
assert!(
|
||||
listed.iter().all(|e| {
|
||||
e.name == crate::oakui::graphops::TEXT_FOOTAGE_LABEL && !e.is_dir
|
||||
}),
|
||||
listed
|
||||
.iter()
|
||||
.all(|e| { e.name == crate::oakui::graphops::TEXT_FOOTAGE_LABEL && !e.is_dir }),
|
||||
"the entries carry the shared 文本 label as leaves"
|
||||
);
|
||||
});
|
||||
|
||||
+106
-31
@@ -309,7 +309,9 @@ fn active_interact_slot() -> &'static Mutex<Option<ActiveInteract>> {
|
||||
pub fn sync_active_interact(instance: Option<u64>) {
|
||||
// Unchanged target: nothing to do.
|
||||
{
|
||||
let mut slot = active_interact_slot().lock().unwrap_or_else(|e| e.into_inner());
|
||||
let mut slot = active_interact_slot()
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
if slot
|
||||
.as_ref()
|
||||
.is_some_and(|active| Some(active.instance) == instance)
|
||||
@@ -340,7 +342,9 @@ pub fn sync_active_interact(instance: Option<u64>) {
|
||||
// Describe → CreateInstance before any draw/pen/key action).
|
||||
interact.describe();
|
||||
interact.create_instance();
|
||||
let mut slot = active_interact_slot().lock().unwrap_or_else(|e| e.into_inner());
|
||||
let mut slot = active_interact_slot()
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
*slot = Some(ActiveInteract {
|
||||
instance: id,
|
||||
interact,
|
||||
@@ -357,14 +361,19 @@ pub fn sync_active_interact(instance: Option<u64>) {
|
||||
/// interact). The viewport reports the last drawn size and updates as the
|
||||
/// viewer's frame size changes.
|
||||
pub fn active_interact() -> Option<(u64, Arc<Interact>, InteractViewport)> {
|
||||
let slot = active_interact_slot().lock().unwrap_or_else(|e| e.into_inner());
|
||||
slot.as_ref().map(|a| (a.instance, a.interact.clone(), a.viewport))
|
||||
let slot = active_interact_slot()
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
slot.as_ref()
|
||||
.map(|a| (a.instance, a.interact.clone(), a.viewport))
|
||||
}
|
||||
|
||||
/// Records the viewport the interact was last drawn at (keeps
|
||||
/// [`active_interact`]'s viewport current).
|
||||
fn note_interact_viewport(instance: u64, viewport: InteractViewport) {
|
||||
let mut slot = active_interact_slot().lock().unwrap_or_else(|e| e.into_inner());
|
||||
let mut slot = active_interact_slot()
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(active) = slot.as_mut() {
|
||||
if active.instance == instance {
|
||||
active.viewport = viewport;
|
||||
@@ -389,7 +398,7 @@ fn bgra_image_to_f32_rgba(img: &RenderImage) -> Option<(u32, u32, Vec<f32>)> {
|
||||
return None;
|
||||
}
|
||||
let mut out = Vec::with_capacity(expected);
|
||||
for px in bytes[..expected].chunks_exact(4) {
|
||||
for px in bytes[..expected].as_chunks::<4>().0 {
|
||||
out.push(px[2] as f32 / 255.0); // R
|
||||
out.push(px[1] as f32 / 255.0); // G
|
||||
out.push(px[0] as f32 / 255.0); // B
|
||||
@@ -401,7 +410,9 @@ fn bgra_image_to_f32_rgba(img: &RenderImage) -> Option<(u32, u32, Vec<f32>)> {
|
||||
/// Reads an `oakplugin` F32 RGBA image into a tightly packed `Vec<f32>`.
|
||||
fn read_image_f32(img: &oak_plugin::image::Image) -> Vec<f32> {
|
||||
img.pixels()
|
||||
.chunks_exact(4)
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|c| f32::from_ne_bytes(c[0..4].try_into().unwrap()))
|
||||
.collect()
|
||||
}
|
||||
@@ -423,7 +434,7 @@ fn read_image_f32(img: &oak_plugin::image::Image) -> Vec<f32> {
|
||||
/// straight-alpha "over". `base` is the displayed frame (alpha 1.0), so
|
||||
/// the result alpha stays 1.0. Returns `None` on a length mismatch.
|
||||
pub fn composite_overlay(overlay: &[f32], base: &[f32]) -> Option<Vec<f32>> {
|
||||
if overlay.len() != base.len() || overlay.len() % 4 != 0 {
|
||||
if overlay.len() != base.len() || !overlay.len().is_multiple_of(4) {
|
||||
return None;
|
||||
}
|
||||
let mut out = vec![0.0f32; overlay.len()];
|
||||
@@ -519,7 +530,9 @@ pub fn draw_interact_composite(
|
||||
}
|
||||
let merged = composite_overlay(&read_image_f32(&overlay), &base_f32)?;
|
||||
note_interact_viewport(instance, *viewport);
|
||||
Some(Arc::new(super::frames::f32_rgba_to_bgra_image(bw, bh, &merged)))
|
||||
Some(Arc::new(super::frames::f32_rgba_to_bgra_image(
|
||||
bw, bh, &merged,
|
||||
)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -695,8 +708,14 @@ mod tests {
|
||||
let p = viewport_pixel_to_pen(point(320.0, 10.0), area, frame);
|
||||
assert_eq!(p, None);
|
||||
// Zero-size inputs.
|
||||
assert_eq!(viewport_pixel_to_pen(point(0.0, 0.0), size(0.0, 0.0), frame), None);
|
||||
assert_eq!(viewport_pixel_to_pen(point(0.0, 0.0), area, size(0.0, 0.0)), None);
|
||||
assert_eq!(
|
||||
viewport_pixel_to_pen(point(0.0, 0.0), size(0.0, 0.0), frame),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
viewport_pixel_to_pen(point(0.0, 0.0), area, size(0.0, 0.0)),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
/// Key mapping: the common keys land on the ofxKeySyms values; printable
|
||||
@@ -710,22 +729,49 @@ mod tests {
|
||||
assert_eq!(key_symbol(&ks("z")), (ofx_key::KEY_Z, "z".to_string()));
|
||||
assert_eq!(key_symbol(&ks("1")), (b'1' as i32, "1".to_string()));
|
||||
// Named keys: symbol only, empty string.
|
||||
assert_eq!(key_symbol(&ks("space")), (ofx_key::KEY_SPACE, String::new()));
|
||||
assert_eq!(key_symbol(&ks("enter")), (ofx_key::KEY_RETURN, String::new()));
|
||||
assert_eq!(key_symbol(&ks("escape")), (ofx_key::KEY_ESCAPE, String::new()));
|
||||
assert_eq!(key_symbol(&ks("backspace")), (ofx_key::KEY_BACKSPACE, String::new()));
|
||||
assert_eq!(key_symbol(&ks("delete")), (ofx_key::KEY_DELETE, String::new()));
|
||||
assert_eq!(
|
||||
key_symbol(&ks("space")),
|
||||
(ofx_key::KEY_SPACE, String::new())
|
||||
);
|
||||
assert_eq!(
|
||||
key_symbol(&ks("enter")),
|
||||
(ofx_key::KEY_RETURN, String::new())
|
||||
);
|
||||
assert_eq!(
|
||||
key_symbol(&ks("escape")),
|
||||
(ofx_key::KEY_ESCAPE, String::new())
|
||||
);
|
||||
assert_eq!(
|
||||
key_symbol(&ks("backspace")),
|
||||
(ofx_key::KEY_BACKSPACE, String::new())
|
||||
);
|
||||
assert_eq!(
|
||||
key_symbol(&ks("delete")),
|
||||
(ofx_key::KEY_DELETE, String::new())
|
||||
);
|
||||
assert_eq!(key_symbol(&ks("left")), (ofx_key::KEY_LEFT, String::new()));
|
||||
assert_eq!(key_symbol(&ks("right")), (ofx_key::KEY_RIGHT, String::new()));
|
||||
assert_eq!(
|
||||
key_symbol(&ks("right")),
|
||||
(ofx_key::KEY_RIGHT, String::new())
|
||||
);
|
||||
assert_eq!(key_symbol(&ks("up")), (ofx_key::KEY_UP, String::new()));
|
||||
assert_eq!(key_symbol(&ks("down")), (ofx_key::KEY_DOWN, String::new()));
|
||||
assert_eq!(key_symbol(&ks("home")), (ofx_key::KEY_HOME, String::new()));
|
||||
assert_eq!(key_symbol(&ks("end")), (ofx_key::KEY_END, String::new()));
|
||||
assert_eq!(key_symbol(&ks("pageup")), (ofx_key::KEY_PAGE_UP, String::new()));
|
||||
assert_eq!(key_symbol(&ks("pagedown")), (ofx_key::KEY_PAGE_DOWN, String::new()));
|
||||
assert_eq!(
|
||||
key_symbol(&ks("pageup")),
|
||||
(ofx_key::KEY_PAGE_UP, String::new())
|
||||
);
|
||||
assert_eq!(
|
||||
key_symbol(&ks("pagedown")),
|
||||
(ofx_key::KEY_PAGE_DOWN, String::new())
|
||||
);
|
||||
assert_eq!(key_symbol(&ks("tab")), (ofx_key::KEY_TAB, String::new()));
|
||||
assert_eq!(key_symbol(&ks("f1")), (ofx_key::KEY_F1, String::new()));
|
||||
assert_eq!(key_symbol(&ks("f12")), (ofx_key::KEY_F1 + 11, String::new()));
|
||||
assert_eq!(
|
||||
key_symbol(&ks("f12")),
|
||||
(ofx_key::KEY_F1 + 11, String::new())
|
||||
);
|
||||
// Unknown multi-character keys.
|
||||
let (sym, s) = key_symbol(&ks("insert"));
|
||||
assert_eq!(sym, ofx_key::KEY_UNKNOWN);
|
||||
@@ -745,7 +791,11 @@ mod tests {
|
||||
let overlay = [1.0, 0.0, 0.0, 0.5];
|
||||
let out = composite_overlay(&overlay, &base).unwrap();
|
||||
for (i, expected) in [(0, 0.5), (1, 0.0), (2, 0.5), (3, 1.0)] {
|
||||
assert!((out[i] - expected).abs() < 1e-6, "channel {i}: {} != {expected}", out[i]);
|
||||
assert!(
|
||||
(out[i] - expected).abs() < 1e-6,
|
||||
"channel {i}: {} != {expected}",
|
||||
out[i]
|
||||
);
|
||||
}
|
||||
// Transparent overlay → base unchanged.
|
||||
let overlay = [0.0, 0.0, 0.0, 0.0];
|
||||
@@ -763,8 +813,21 @@ mod tests {
|
||||
let img = super::super::frames::f32_rgba_to_bgra_image(w, h, &samples);
|
||||
let (got_w, got_h, rgba) = bgra_image_to_f32_rgba(&img).unwrap();
|
||||
assert_eq!((got_w, got_h), (w, h));
|
||||
for (i, expected) in [(0, 1.0), (1, 0.0), (2, 0.5), (3, 1.0), (4, 0.25), (5, 0.5), (6, 0.75), (7, 1.0)] {
|
||||
assert!((rgba[i] - expected).abs() < 0.01, "channel {i}: {} != {expected}", rgba[i]);
|
||||
for (i, expected) in [
|
||||
(0, 1.0),
|
||||
(1, 0.0),
|
||||
(2, 0.5),
|
||||
(3, 1.0),
|
||||
(4, 0.25),
|
||||
(5, 0.5),
|
||||
(6, 0.75),
|
||||
(7, 1.0),
|
||||
] {
|
||||
assert!(
|
||||
(rgba[i] - expected).abs() < 0.01,
|
||||
"channel {i}: {} != {expected}",
|
||||
rgba[i]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -823,7 +886,11 @@ mod tests {
|
||||
println!("SKIP: minimal test plugin not built");
|
||||
return false;
|
||||
};
|
||||
if oak_plugin::host::Host::global().cache.scan_path(&dir).is_err() {
|
||||
if oak_plugin::host::Host::global()
|
||||
.cache
|
||||
.scan_path(&dir)
|
||||
.is_err()
|
||||
{
|
||||
println!("SKIP: test plugin scan failed");
|
||||
return false;
|
||||
}
|
||||
@@ -913,9 +980,7 @@ mod tests {
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
|
||||
let lines = loop {
|
||||
let lines = read_marker(&marker);
|
||||
if lines.iter().any(|l| l == "destroy")
|
||||
|| std::time::Instant::now() >= deadline
|
||||
{
|
||||
if lines.iter().any(|l| l == "destroy") || std::time::Instant::now() >= deadline {
|
||||
break lines;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
@@ -939,15 +1004,21 @@ mod tests {
|
||||
// The forwarded events with real arguments (C %g drops trailing
|
||||
// zeros).
|
||||
assert!(
|
||||
lines.iter().any(|l| l == "pen_motion vp=20,20 canon=20,20 pressure=1"),
|
||||
lines
|
||||
.iter()
|
||||
.any(|l| l == "pen_motion vp=20,20 canon=20,20 pressure=1"),
|
||||
"pen_motion not recorded with pen-down state: {lines:?}"
|
||||
);
|
||||
assert!(
|
||||
lines.iter().any(|l| l == "pen_down vp=20,20 canon=20,20 pressure=1"),
|
||||
lines
|
||||
.iter()
|
||||
.any(|l| l == "pen_down vp=20,20 canon=20,20 pressure=1"),
|
||||
"pen_down not recorded: {lines:?}"
|
||||
);
|
||||
assert!(
|
||||
lines.iter().any(|l| l == "pen_up vp=20,20 canon=20,20 pressure=0"),
|
||||
lines
|
||||
.iter()
|
||||
.any(|l| l == "pen_up vp=20,20 canon=20,20 pressure=0"),
|
||||
"pen_up not recorded: {lines:?}"
|
||||
);
|
||||
assert!(
|
||||
@@ -992,7 +1063,11 @@ mod tests {
|
||||
for px in base_samples.chunks_exact_mut(4) {
|
||||
px.copy_from_slice(&[0.0, 0.0, 1.0, 1.0]);
|
||||
}
|
||||
let base = Arc::new(super::super::frames::f32_rgba_to_bgra_image(w, h, &base_samples));
|
||||
let base = Arc::new(super::super::frames::f32_rgba_to_bgra_image(
|
||||
w,
|
||||
h,
|
||||
&base_samples,
|
||||
));
|
||||
let viewport = InteractViewport::at_frame_size(w, h);
|
||||
let composite = draw_interact_composite(handle, &interact, &viewport, 0.0, &base)
|
||||
.expect("GL overlay composite");
|
||||
|
||||
+360
-310
File diff suppressed because it is too large
Load Diff
@@ -179,7 +179,10 @@ fn clip_multicam_media(
|
||||
// The source sequence and the current source's track.
|
||||
let seq = g.connected_output(mc, SEQUENCE_INPUT, -1)?;
|
||||
let kind = {
|
||||
let t = mc_entry.core.standard_value(SEQUENCE_TYPE_INPUT, -1).to_double() as i32;
|
||||
let t = mc_entry
|
||||
.core
|
||||
.standard_value(SEQUENCE_TYPE_INPUT, -1)
|
||||
.to_double() as i32;
|
||||
oak_node::track::TrackType::from_c(t).unwrap_or(oak_node::track::TrackType::Video)
|
||||
};
|
||||
let seq_pos = super::graphops::track_list_of(g, seq, kind)?;
|
||||
@@ -221,7 +224,10 @@ fn clip_multicam_media(
|
||||
/// unchanged). Parameters are the inspector's parameter set (non-hidden,
|
||||
/// non-connection inputs at their standard values; keyframed values are
|
||||
/// not time-resolved on this path).
|
||||
fn clip_effects(g: &oak_node::graph::Graph, block_id: NodeId) -> Vec<oak_render::ticket::MontageEffect> {
|
||||
fn clip_effects(
|
||||
g: &oak_node::graph::Graph,
|
||||
block_id: NodeId,
|
||||
) -> Vec<oak_render::ticket::MontageEffect> {
|
||||
use super::effectchain;
|
||||
effectchain::chain(g, block_id)
|
||||
.into_iter()
|
||||
@@ -483,7 +489,9 @@ pub fn render_multicam_angle_frame(
|
||||
width: i32,
|
||||
height: i32,
|
||||
) -> Result<RenderedFrame, String> {
|
||||
render_video(multicam_angle_frame_params(p, seq, track, frame_ts, tb, width, height)?)
|
||||
render_video(multicam_angle_frame_params(
|
||||
p, seq, track, frame_ts, tb, width, height,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// The audio montage over `range`: every audio clip overlapping the
|
||||
@@ -518,8 +526,7 @@ pub fn audio_montage(p: &ProjectRef, seq: NodeId, range: TimeRange) -> Vec<Monta
|
||||
if out <= range.in_() || in_ >= range.out() {
|
||||
continue;
|
||||
}
|
||||
let Some((filename, stream_index)) =
|
||||
clip_preview_media(&g.graph, block_id, false)
|
||||
let Some((filename, stream_index)) = clip_preview_media(&g.graph, block_id, false)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
@@ -631,23 +638,24 @@ impl RenderedFrame {
|
||||
let (w, h) = (meta.width.max(0) as u32, meta.height.max(0) as u32);
|
||||
let pixels = f.shm.slot_bytes(f.slot);
|
||||
let data = pixels.get(..meta.data_size.max(0) as usize)?;
|
||||
if meta.format == PIXEL_FORMAT_F32 {
|
||||
// M15 S3: the worker rendered F32 (the 10-bit display
|
||||
// path) — repack the padded rows, transform and hand the
|
||||
// samples back for the RGBA16F texture.
|
||||
let mut samples = repack_f32_rows(meta.width, meta.height, meta.linesize, data)?;
|
||||
// Output node: working space → the project's output
|
||||
// colorspace. The scopes below read the output-colorspace
|
||||
// signal (same convention as the BGRA8 slot); the display
|
||||
// policy then decides whether the display ICC is applied on
|
||||
// top (self-managed) or the OS maps the declared content
|
||||
// colorspace (OS-managed).
|
||||
apply_output_node_f32(&mut samples);
|
||||
let scope = analyze_f32_rgba(w, h, &samples);
|
||||
super::displaycolor::apply_f32_rgba(&mut samples, (w * h) as i64);
|
||||
let image = f32_rgba_to_bgra_image(w, h, &samples);
|
||||
Some((image, scope, Some(samples)))
|
||||
} else {
|
||||
if meta.format == PIXEL_FORMAT_F32 {
|
||||
// M15 S3: the worker rendered F32 (the 10-bit display
|
||||
// path) — repack the padded rows, transform and hand the
|
||||
// samples back for the RGBA16F texture.
|
||||
let mut samples =
|
||||
repack_f32_rows(meta.width, meta.height, meta.linesize, data)?;
|
||||
// Output node: working space → the project's output
|
||||
// colorspace. The scopes below read the output-colorspace
|
||||
// signal (same convention as the BGRA8 slot); the display
|
||||
// policy then decides whether the display ICC is applied on
|
||||
// top (self-managed) or the OS maps the declared content
|
||||
// colorspace (OS-managed).
|
||||
apply_output_node_f32(&mut samples);
|
||||
let scope = analyze_f32_rgba(w, h, &samples);
|
||||
super::displaycolor::apply_f32_rgba(&mut samples, (w * h) as i64);
|
||||
let image = f32_rgba_to_bgra_image(w, h, &samples);
|
||||
Some((image, scope, Some(samples)))
|
||||
} else {
|
||||
// BGRA8 slot: the worker already downconverted — wrap the
|
||||
// bytes directly (no 10-bit path available for them).
|
||||
let scope = analyze_bgra8(w, h, data);
|
||||
@@ -687,11 +695,7 @@ impl RenderedFrame {
|
||||
apply_output_node_f32(&mut samples);
|
||||
let scope = analyze_f32_rgba(w, h, &samples);
|
||||
super::displaycolor::apply_f32_rgba(&mut samples, (w * h) as i64);
|
||||
Some((
|
||||
f32_rgba_to_bgra_image(w, h, &samples),
|
||||
scope,
|
||||
Some(samples),
|
||||
))
|
||||
Some((f32_rgba_to_bgra_image(w, h, &samples), scope, Some(samples)))
|
||||
}
|
||||
RenderedFrame::CpuF32 {
|
||||
width,
|
||||
@@ -706,11 +710,7 @@ impl RenderedFrame {
|
||||
apply_output_node_f32(&mut samples);
|
||||
let scope = analyze_f32_rgba(w, h, &samples);
|
||||
super::displaycolor::apply_f32_rgba(&mut samples, (w * h) as i64);
|
||||
Some((
|
||||
f32_rgba_to_bgra_image(w, h, &samples),
|
||||
scope,
|
||||
Some(samples),
|
||||
))
|
||||
Some((f32_rgba_to_bgra_image(w, h, &samples), scope, Some(samples)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -721,9 +721,9 @@ impl RenderedFrame {
|
||||
/// Pass-through in the legacy sRGB working space.
|
||||
fn apply_output_node_f32(samples: &mut [f32]) {
|
||||
oak_core::colormath::working_to_display_target(
|
||||
samples,
|
||||
oak_core::color::pipeline_working_space(),
|
||||
oak_core::color::pipeline_output_spec(),
|
||||
samples,
|
||||
oak_core::color::pipeline_working_space(),
|
||||
oak_core::color::pipeline_output_spec(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -742,7 +742,7 @@ fn repack_f32_rows(width: i32, height: i32, linesize: i32, data: &[u8]) -> Optio
|
||||
let mut samples = vec![0.0f32; (width * height * 4) as usize];
|
||||
for y in 0..height as usize {
|
||||
let row = &data[y * linesize..y * linesize + row_bytes];
|
||||
for (i, px) in row.chunks_exact(4).enumerate() {
|
||||
for (i, px) in row.as_chunks::<4>().0.iter().enumerate() {
|
||||
let v = f32::from_ne_bytes([px[0], px[1], px[2], px[3]]);
|
||||
samples[y * (width as usize) * 4 + i] = v;
|
||||
}
|
||||
@@ -876,7 +876,8 @@ fn proxy_limits_at(p: &ProjectRef, seq: NodeId, time: Rational) -> Vec<(i32, i32
|
||||
/// variant without copying frame bytes — the shm slot is read zero-copy
|
||||
/// by the caller and released after building the display image).
|
||||
fn render_video(params: VideoTicketParams) -> Result<RenderedFrame, String> {
|
||||
let m = RenderManager::global().ok_or_else(|| "render manager is not initialized".to_string())?;
|
||||
let m =
|
||||
RenderManager::global().ok_or_else(|| "render manager is not initialized".to_string())?;
|
||||
let id = m.tickets.next_id();
|
||||
if std::env::var_os("OAK_DEBUG_DISPATCH").is_some() {
|
||||
eprintln!("renderops: sync render submit arena ticket {}", id.0);
|
||||
@@ -959,7 +960,9 @@ pub fn render_sequence_frame(
|
||||
height: i32,
|
||||
format: Option<oak_core::PixelFormat>,
|
||||
) -> Result<RenderedFrame, String> {
|
||||
render_video(sequence_frame_params(p, seq, frame_ts, tb, width, height, format)?)
|
||||
render_video(sequence_frame_params(
|
||||
p, seq, frame_ts, tb, width, height, format,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Build the video ticket params for one single-footage frame (M15 S2:
|
||||
@@ -1025,7 +1028,9 @@ pub fn render_footage_frame(
|
||||
height: i32,
|
||||
format: Option<oak_core::PixelFormat>,
|
||||
) -> Result<RenderedFrame, String> {
|
||||
render_video(footage_frame_params(p, footage, frame_ts, tb, width, height, format)?)
|
||||
render_video(footage_frame_params(
|
||||
p, footage, frame_ts, tb, width, height, format,
|
||||
)?)
|
||||
}
|
||||
|
||||
/// Rendered interleaved f32 audio (the module audio ticket payload).
|
||||
@@ -1057,7 +1062,8 @@ pub fn render_audio_range(
|
||||
Rational::new((start_ts + len_ts) * tb.0, tb.1),
|
||||
);
|
||||
let montage = audio_montage(p, seq, range);
|
||||
let m = RenderManager::global().ok_or_else(|| "render manager is not initialized".to_string())?;
|
||||
let m =
|
||||
RenderManager::global().ok_or_else(|| "render manager is not initialized".to_string())?;
|
||||
let id = m.tickets.next_id();
|
||||
m.tickets.submit_audio_with_id(
|
||||
id,
|
||||
@@ -1178,21 +1184,23 @@ pub fn encoding_params(
|
||||
// Export range: the work area when enabled, otherwise the whole
|
||||
// sequence. Frames -> seconds rationals in the sequence's frame-rate
|
||||
// timebase (frame duration = rate_den / rate_num).
|
||||
let (has_custom_range, range_in, range_out, length) =
|
||||
match workarea.filter(|(s, e)| e > s) {
|
||||
Some((in_ts, out_ts)) => (
|
||||
true,
|
||||
Rational::new(in_ts * i64::from(rate_den), i64::from(rate_num)),
|
||||
Rational::new(out_ts * i64::from(rate_den), i64::from(rate_num)),
|
||||
Rational::new((out_ts - in_ts) * i64::from(rate_den), i64::from(rate_num)),
|
||||
let (has_custom_range, range_in, range_out, length) = match workarea.filter(|(s, e)| e > s) {
|
||||
Some((in_ts, out_ts)) => (
|
||||
true,
|
||||
Rational::new(in_ts * i64::from(rate_den), i64::from(rate_num)),
|
||||
Rational::new(out_ts * i64::from(rate_den), i64::from(rate_num)),
|
||||
Rational::new((out_ts - in_ts) * i64::from(rate_den), i64::from(rate_num)),
|
||||
),
|
||||
None => (
|
||||
false,
|
||||
Rational::new(0, 1),
|
||||
Rational::new(0, 1),
|
||||
Rational::new(
|
||||
length_frames.max(0) * i64::from(rate_den),
|
||||
i64::from(rate_num),
|
||||
),
|
||||
None => (
|
||||
false,
|
||||
Rational::new(0, 1),
|
||||
Rational::new(0, 1),
|
||||
Rational::new(length_frames.max(0) * i64::from(rate_den), i64::from(rate_num)),
|
||||
),
|
||||
};
|
||||
),
|
||||
};
|
||||
Ok(oak_task::export::EncodingParams {
|
||||
filename: path.to_string_lossy().into_owned(),
|
||||
format,
|
||||
@@ -1245,12 +1253,22 @@ pub fn encoding_params_with_settings(
|
||||
.iter()
|
||||
.find(|c| **c as i32 == settings.video_codec)
|
||||
.copied()
|
||||
.ok_or_else(|| format!("codec {} not supported by {container:?}", settings.video_codec))?;
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"codec {} not supported by {container:?}",
|
||||
settings.video_codec
|
||||
)
|
||||
})?;
|
||||
let audio_codec = oak_codec::exportformat::Format::get_audio_codecs(container)
|
||||
.iter()
|
||||
.find(|c| **c as i32 == settings.audio_codec)
|
||||
.copied()
|
||||
.ok_or_else(|| format!("audio codec {} not supported by {container:?}", settings.audio_codec))?;
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"audio codec {} not supported by {container:?}",
|
||||
settings.audio_codec
|
||||
)
|
||||
})?;
|
||||
let (mut width, mut height, rate) = {
|
||||
let g = lock(p);
|
||||
super::graphops::sequence_video_params(&g.graph, seq)
|
||||
@@ -1281,24 +1299,36 @@ pub fn encoding_params_with_settings(
|
||||
_ => match workarea.filter(|(s, e)| e > s) {
|
||||
Some((in_ts, out_ts)) => (
|
||||
true,
|
||||
Rational::new(in_ts * i64::from(rate_den.max(1)), i64::from(rate_num.max(1))),
|
||||
Rational::new(out_ts * i64::from(rate_den.max(1)), i64::from(rate_num.max(1))),
|
||||
Rational::new((out_ts - in_ts) * i64::from(rate_den.max(1)), i64::from(rate_num.max(1))),
|
||||
Rational::new(
|
||||
in_ts * i64::from(rate_den.max(1)),
|
||||
i64::from(rate_num.max(1)),
|
||||
),
|
||||
Rational::new(
|
||||
out_ts * i64::from(rate_den.max(1)),
|
||||
i64::from(rate_num.max(1)),
|
||||
),
|
||||
Rational::new(
|
||||
(out_ts - in_ts) * i64::from(rate_den.max(1)),
|
||||
i64::from(rate_num.max(1)),
|
||||
),
|
||||
),
|
||||
None => (
|
||||
false,
|
||||
Rational::new(0, 1),
|
||||
Rational::new(0, 1),
|
||||
Rational::new(length_frames.max(0) * i64::from(rate_den.max(1)), i64::from(rate_num.max(1))),
|
||||
Rational::new(
|
||||
length_frames.max(0) * i64::from(rate_den.max(1)),
|
||||
i64::from(rate_num.max(1)),
|
||||
),
|
||||
),
|
||||
},
|
||||
};
|
||||
let bit_depth = settings.bit_depth;
|
||||
let pixel_format = match bit_depth {
|
||||
10 => 1, // PixelFormat::U10 (only when the codec supports it; the
|
||||
// dialog gates 10-bit behind HDR — codecs without 10-bit
|
||||
// stay at the 8-bit default and the dialog disables HDR).
|
||||
_ => 0, // PixelFormat::U8
|
||||
// dialog gates 10-bit behind HDR — codecs without 10-bit
|
||||
// stay at the 8-bit default and the dialog disables HDR).
|
||||
_ => 0, // PixelFormat::U8
|
||||
};
|
||||
Ok(oak_task::export::EncodingParams {
|
||||
filename: path.to_string_lossy().into_owned(),
|
||||
@@ -1394,7 +1424,8 @@ mod tests {
|
||||
fn project_with_clip(media: &std::path::Path) -> (ProjectRef, NodeId, NodeId) {
|
||||
let project = graphops::create_project();
|
||||
let seq = graphops::create_sequence(&project, "Montage Test");
|
||||
let footage = graphops::import_footage(&project, media).expect("import the generated media");
|
||||
let footage =
|
||||
graphops::import_footage(&project, media).expect("import the generated media");
|
||||
graphops::add_track(&project, seq, TrackType::Video).expect("add a video track");
|
||||
graphops::place_footage_clip(&project, seq, footage, TrackType::Video, 0, 0, 10, 0)
|
||||
.expect("place the clip");
|
||||
@@ -1416,9 +1447,12 @@ mod tests {
|
||||
queue,
|
||||
oak_core::backend::BackendKind::Auto,
|
||||
);
|
||||
let mut frame =
|
||||
oak_render::eval::generate_frame(Rational::new(0, 1), (2, 1), oak_core::PixelFormat::F32)
|
||||
.unwrap();
|
||||
let mut frame = oak_render::eval::generate_frame(
|
||||
Rational::new(0, 1),
|
||||
(2, 1),
|
||||
oak_core::PixelFormat::F32,
|
||||
)
|
||||
.unwrap();
|
||||
for px in frame.data.chunks_exact_mut(16) {
|
||||
for (c, v) in px.chunks_exact_mut(4).zip([0.25f32, 0.5, 0.75, 1.0]) {
|
||||
c.copy_from_slice(&v.to_le_bytes());
|
||||
@@ -1446,7 +1480,13 @@ mod tests {
|
||||
// the display function takes the single explicit readback.
|
||||
let token = base.create_texture(2, 1).unwrap();
|
||||
base.upload(token, &frame).unwrap();
|
||||
let private = RenderedFrame::Gpu(Texture::gpu(base.clone(), token, 2, 1, oak_core::PixelFormat::F32));
|
||||
let private = RenderedFrame::Gpu(Texture::gpu(
|
||||
base.clone(),
|
||||
token,
|
||||
2,
|
||||
1,
|
||||
oak_core::PixelFormat::F32,
|
||||
));
|
||||
oak_core::backend::reset_gpu_transfer_counters();
|
||||
let displayed = private.to_display().expect("fallback display");
|
||||
assert!(displayed.2.is_some(), "the fallback hands CPU samples");
|
||||
@@ -1521,7 +1561,10 @@ mod tests {
|
||||
let montage = video_montage(&project, seq, at(0));
|
||||
assert_eq!(montage.len(), 1, "one clip covers frame 0");
|
||||
assert_eq!(montage[0].filename, media.to_string_lossy());
|
||||
assert!(video_montage(&project, seq, at(9)).len() == 1, "frame 9 is still covered");
|
||||
assert!(
|
||||
video_montage(&project, seq, at(9)).len() == 1,
|
||||
"frame 9 is still covered"
|
||||
);
|
||||
assert!(
|
||||
video_montage(&project, seq, at(10)).is_empty(),
|
||||
"frame 10 is past the clip's out point"
|
||||
@@ -1543,8 +1586,7 @@ mod tests {
|
||||
fn video_montage_stacks_highest_track_on_top() {
|
||||
let _media = media_lock();
|
||||
oak_undo::global::clear().unwrap();
|
||||
let red =
|
||||
std::env::temp_dir().join(format!("oakapp_stack_red_{}.mp4", std::process::id()));
|
||||
let red = std::env::temp_dir().join(format!("oakapp_stack_red_{}.mp4", std::process::id()));
|
||||
let blue =
|
||||
std::env::temp_dir().join(format!("oakapp_stack_blue_{}.mp4", std::process::id()));
|
||||
oak_codec::testmedia::write_test_clip_solid(&red, 64, 64, 10, 10, [0.9, 0.1, 0.1, 1.0])
|
||||
@@ -1555,7 +1597,8 @@ mod tests {
|
||||
let project = graphops::create_project();
|
||||
let seq = graphops::create_sequence(&project, "Stack Montage");
|
||||
let red_footage = graphops::import_footage(&project, &red).expect("import the red media");
|
||||
let blue_footage = graphops::import_footage(&project, &blue).expect("import the blue media");
|
||||
let blue_footage =
|
||||
graphops::import_footage(&project, &blue).expect("import the blue media");
|
||||
graphops::place_footage_clip(&project, seq, red_footage, TrackType::Video, 0, 0, 10, 0)
|
||||
.expect("place the V1 (red) clip");
|
||||
graphops::place_footage_clip(&project, seq, blue_footage, TrackType::Video, 1, 0, 10, 0)
|
||||
@@ -1566,7 +1609,11 @@ mod tests {
|
||||
// Bottom-to-top: V1's (red) clip first, V2's (blue) last.
|
||||
let montage = video_montage(&project, seq, time);
|
||||
assert_eq!(montage.len(), 2, "both clips cover frame 0");
|
||||
assert_eq!(montage[0].filename, red.to_string_lossy(), "V1's clip is the bottom of the stack");
|
||||
assert_eq!(
|
||||
montage[0].filename,
|
||||
red.to_string_lossy(),
|
||||
"V1's clip is the bottom of the stack"
|
||||
);
|
||||
assert_eq!(
|
||||
montage[1].filename,
|
||||
blue.to_string_lossy(),
|
||||
@@ -1595,7 +1642,10 @@ mod tests {
|
||||
let off = (8 * 64 + 8) * 16;
|
||||
let r = f32::from_le_bytes(dst[off..off + 4].try_into().unwrap());
|
||||
let b = f32::from_le_bytes(dst[off + 8..off + 12].try_into().unwrap());
|
||||
assert!(b > 0.5 && r < 0.4, "V2's blue covers V1's red (r={r}, b={b})");
|
||||
assert!(
|
||||
b > 0.5 && r < 0.4,
|
||||
"V2's blue covers V1's red (r={r}, b={b})"
|
||||
);
|
||||
|
||||
oak_undo::global::clear().unwrap();
|
||||
let _ = std::fs::remove_file(&red);
|
||||
@@ -1614,8 +1664,8 @@ mod tests {
|
||||
// legacy sRGB pass-through so the pixel-value assertions hold
|
||||
// regardless of the ACEScg default.
|
||||
oak_core::color::set_pipeline_color_settings(
|
||||
oak_core::colormath::WorkingColorSpace::SrgbLegacy,
|
||||
oak_core::colormath::OutputColorSpec::default(),
|
||||
oak_core::colormath::WorkingColorSpace::SrgbLegacy,
|
||||
oak_core::colormath::OutputColorSpec::default(),
|
||||
);
|
||||
oak_undo::global::clear().unwrap();
|
||||
let media =
|
||||
@@ -1624,10 +1674,12 @@ mod tests {
|
||||
|
||||
let project = graphops::create_project();
|
||||
let seq = graphops::create_sequence(&project, "Effect Montage");
|
||||
let footage = graphops::import_footage(&project, &media).expect("import the generated media");
|
||||
let footage =
|
||||
graphops::import_footage(&project, &media).expect("import the generated media");
|
||||
graphops::add_track(&project, seq, TrackType::Video).expect("add a video track");
|
||||
let block = graphops::place_footage_clip(&project, seq, footage, TrackType::Video, 0, 0, 10, 0)
|
||||
.expect("place the clip");
|
||||
let block =
|
||||
graphops::place_footage_clip(&project, seq, footage, TrackType::Video, 0, 0, 10, 0)
|
||||
.expect("place the clip");
|
||||
|
||||
// The effect-library double-click flow: append a 50% Opacity to the
|
||||
// footage-backed clip (the insert rewires footage -> effect -> clip).
|
||||
@@ -1674,7 +1726,9 @@ mod tests {
|
||||
// A left-half pixel of the test pattern (known content, r ~= 0.9).
|
||||
let pixel = |frame: &[u8]| {
|
||||
let off = (8 * 64 + 8) * 16;
|
||||
[0, 1, 2, 3].map(|i| f32::from_le_bytes(frame[off + i * 4..off + i * 4 + 4].try_into().unwrap()))
|
||||
[0, 1, 2, 3].map(|i| {
|
||||
f32::from_le_bytes(frame[off + i * 4..off + i * 4 + 4].try_into().unwrap())
|
||||
})
|
||||
};
|
||||
|
||||
let with_fx = video_montage(&project, seq, time);
|
||||
@@ -1684,7 +1738,10 @@ mod tests {
|
||||
1,
|
||||
"the montage carries the clip's effect stack (not the footage source node)"
|
||||
);
|
||||
assert_eq!(with_fx[0].effects[0].type_id, "org.olivevideoeditor.Olive.opacity");
|
||||
assert_eq!(
|
||||
with_fx[0].effects[0].type_id,
|
||||
"org.olivevideoeditor.Olive.opacity"
|
||||
);
|
||||
assert!(with_fx[0].effects[0].enabled);
|
||||
|
||||
let plain = render(
|
||||
@@ -1699,7 +1756,11 @@ mod tests {
|
||||
);
|
||||
let effected = render(with_fx);
|
||||
let (p, e) = (pixel(&plain), pixel(&effected));
|
||||
assert!(p[0] > 0.6, "the plain render shows the test pattern (r={})", p[0]);
|
||||
assert!(
|
||||
p[0] > 0.6,
|
||||
"the plain render shows the test pattern (r={})",
|
||||
p[0]
|
||||
);
|
||||
// 50% opacity: the shader halves every channel, then the composite
|
||||
// over transparent black halves it again via the halved alpha.
|
||||
assert!(
|
||||
@@ -1746,7 +1807,8 @@ mod tests {
|
||||
) -> (NodeId, NodeId) {
|
||||
let (tb, in_r) = {
|
||||
let g = lock(p);
|
||||
let tb = graphops::sequence_time_base(&g.graph, seq).expect("the sequence has a timebase");
|
||||
let tb =
|
||||
graphops::sequence_time_base(&g.graph, seq).expect("the sequence has a timebase");
|
||||
(tb, graphops::ts_to_rational(in_ts, tb))
|
||||
};
|
||||
let block = {
|
||||
@@ -1824,7 +1886,8 @@ mod tests {
|
||||
let project = graphops::create_project();
|
||||
let seq = graphops::create_sequence(&project, "Adjustment Montage");
|
||||
let red_footage = graphops::import_footage(&project, &red).expect("import the red media");
|
||||
let blue_footage = graphops::import_footage(&project, &blue).expect("import the blue media");
|
||||
let blue_footage =
|
||||
graphops::import_footage(&project, &blue).expect("import the blue media");
|
||||
graphops::place_footage_clip(&project, seq, red_footage, TrackType::Video, 0, 0, 10, 0)
|
||||
.expect("place the V1 (red) clip");
|
||||
graphops::place_footage_clip(&project, seq, blue_footage, TrackType::Video, 1, 0, 10, 0)
|
||||
@@ -1839,36 +1902,38 @@ mod tests {
|
||||
|
||||
let tb = graphops::sequence_time_base(&lock(&project).graph, seq).unwrap();
|
||||
let at = |frame: i64| graphops::ts_to_rational(frame, tb);
|
||||
let render = |montage: Vec<MontageClip>, adjustments: Vec<AdjustmentSpan>, at_time: Rational| {
|
||||
let params = VideoTicketParams {
|
||||
viewer: 0,
|
||||
project: String::new(),
|
||||
time: at_time,
|
||||
force_size: Some((64, 64)),
|
||||
force_format: Some(oak_core::PixelFormat::F32),
|
||||
cache: None,
|
||||
cache_dir: None,
|
||||
cache_id: None,
|
||||
cache_timebase: None,
|
||||
footage: None,
|
||||
montage,
|
||||
adjustments,
|
||||
let render =
|
||||
|montage: Vec<MontageClip>, adjustments: Vec<AdjustmentSpan>, at_time: Rational| {
|
||||
let params = VideoTicketParams {
|
||||
viewer: 0,
|
||||
project: String::new(),
|
||||
time: at_time,
|
||||
force_size: Some((64, 64)),
|
||||
force_format: Some(oak_core::PixelFormat::F32),
|
||||
cache: None,
|
||||
cache_dir: None,
|
||||
cache_id: None,
|
||||
cache_timebase: None,
|
||||
footage: None,
|
||||
montage,
|
||||
adjustments,
|
||||
};
|
||||
let mut dst = vec![0u8; 64 * 64 * 16];
|
||||
oak_render::eval::render_montage_frame_into(
|
||||
params.time,
|
||||
¶ms,
|
||||
(64, 64),
|
||||
&mut dst,
|
||||
64 * 16,
|
||||
)
|
||||
.expect("montage render");
|
||||
dst
|
||||
};
|
||||
let mut dst = vec![0u8; 64 * 64 * 16];
|
||||
oak_render::eval::render_montage_frame_into(
|
||||
params.time,
|
||||
¶ms,
|
||||
(64, 64),
|
||||
&mut dst,
|
||||
64 * 16,
|
||||
)
|
||||
.expect("montage render");
|
||||
dst
|
||||
};
|
||||
let pixel = |frame: &[u8]| {
|
||||
let off = (8 * 64 + 8) * 16;
|
||||
[0, 1, 2, 3]
|
||||
.map(|i| f32::from_le_bytes(frame[off + i * 4..off + i * 4 + 4].try_into().unwrap()))
|
||||
[0, 1, 2, 3].map(|i| {
|
||||
f32::from_le_bytes(frame[off + i * 4..off + i * 4 + 4].try_into().unwrap())
|
||||
})
|
||||
};
|
||||
|
||||
// Inside the layer's range the span rides the ticket, above the stack.
|
||||
@@ -1880,8 +1945,15 @@ mod tests {
|
||||
montage.len(),
|
||||
"the layer sits above every clip (it grades the whole composite)"
|
||||
);
|
||||
assert_eq!(spans[0].effects.len(), 1, "the layer's stack rides the span");
|
||||
assert_eq!(spans[0].effects[0].type_id, "org.olivevideoeditor.Olive.opacity");
|
||||
assert_eq!(
|
||||
spans[0].effects.len(),
|
||||
1,
|
||||
"the layer's stack rides the span"
|
||||
);
|
||||
assert_eq!(
|
||||
spans[0].effects[0].type_id,
|
||||
"org.olivevideoeditor.Olive.opacity"
|
||||
);
|
||||
|
||||
let baseline = render(montage.clone(), Vec::new(), at(5));
|
||||
let graded = render(montage.clone(), spans, at(5));
|
||||
@@ -1948,7 +2020,8 @@ mod tests {
|
||||
oak_core::colormath::OutputColorSpec::default(),
|
||||
);
|
||||
oak_undo::global::clear().unwrap();
|
||||
let red = std::env::temp_dir().join(format!("oakapp_adjlow_red_{}.mp4", std::process::id()));
|
||||
let red =
|
||||
std::env::temp_dir().join(format!("oakapp_adjlow_red_{}.mp4", std::process::id()));
|
||||
let blue =
|
||||
std::env::temp_dir().join(format!("oakapp_adjlow_blue_{}.mp4", std::process::id()));
|
||||
oak_codec::testmedia::write_test_clip_solid(&red, 64, 64, 10, 10, [0.9, 0.1, 0.1, 1.0])
|
||||
@@ -1959,7 +2032,8 @@ mod tests {
|
||||
let project = graphops::create_project();
|
||||
let seq = graphops::create_sequence(&project, "Mid Adjustment Montage");
|
||||
let red_footage = graphops::import_footage(&project, &red).expect("import the red media");
|
||||
let blue_footage = graphops::import_footage(&project, &blue).expect("import the blue media");
|
||||
let blue_footage =
|
||||
graphops::import_footage(&project, &blue).expect("import the blue media");
|
||||
graphops::place_footage_clip(&project, seq, red_footage, TrackType::Video, 0, 0, 10, 0)
|
||||
.expect("place the V1 (red) clip");
|
||||
graphops::add_track(&project, seq, TrackType::Video).expect("add the adjustment track");
|
||||
@@ -1970,9 +2044,17 @@ mod tests {
|
||||
let (_adj, _fx) = adjustment_layer_with_opacity(&project, seq, adj_track, 0, 5, 0.5);
|
||||
// V3's clip carries its own 50% Opacity so the composite keeps both
|
||||
// contributions visible (an opaque top clip would hide the grading).
|
||||
let blue_clip =
|
||||
graphops::place_footage_clip(&project, seq, blue_footage, TrackType::Video, 2, 0, 10, 0)
|
||||
.expect("place the V3 (blue) clip");
|
||||
let blue_clip = graphops::place_footage_clip(
|
||||
&project,
|
||||
seq,
|
||||
blue_footage,
|
||||
TrackType::Video,
|
||||
2,
|
||||
0,
|
||||
10,
|
||||
0,
|
||||
)
|
||||
.expect("place the V3 (blue) clip");
|
||||
let clip_fx = crate::oakui::effectchain::insert(
|
||||
&project,
|
||||
blue_clip,
|
||||
@@ -1990,36 +2072,38 @@ mod tests {
|
||||
|
||||
let tb = graphops::sequence_time_base(&lock(&project).graph, seq).unwrap();
|
||||
let time = graphops::ts_to_rational(0, tb);
|
||||
let render = |montage: Vec<MontageClip>, adjustments: Vec<AdjustmentSpan>, at_time: Rational| {
|
||||
let params = VideoTicketParams {
|
||||
viewer: 0,
|
||||
project: String::new(),
|
||||
time: at_time,
|
||||
force_size: Some((64, 64)),
|
||||
force_format: Some(oak_core::PixelFormat::F32),
|
||||
cache: None,
|
||||
cache_dir: None,
|
||||
cache_id: None,
|
||||
cache_timebase: None,
|
||||
footage: None,
|
||||
montage,
|
||||
adjustments,
|
||||
let render =
|
||||
|montage: Vec<MontageClip>, adjustments: Vec<AdjustmentSpan>, at_time: Rational| {
|
||||
let params = VideoTicketParams {
|
||||
viewer: 0,
|
||||
project: String::new(),
|
||||
time: at_time,
|
||||
force_size: Some((64, 64)),
|
||||
force_format: Some(oak_core::PixelFormat::F32),
|
||||
cache: None,
|
||||
cache_dir: None,
|
||||
cache_id: None,
|
||||
cache_timebase: None,
|
||||
footage: None,
|
||||
montage,
|
||||
adjustments,
|
||||
};
|
||||
let mut dst = vec![0u8; 64 * 64 * 16];
|
||||
oak_render::eval::render_montage_frame_into(
|
||||
at_time,
|
||||
¶ms,
|
||||
(64, 64),
|
||||
&mut dst,
|
||||
64 * 16,
|
||||
)
|
||||
.expect("montage render");
|
||||
dst
|
||||
};
|
||||
let mut dst = vec![0u8; 64 * 64 * 16];
|
||||
oak_render::eval::render_montage_frame_into(
|
||||
at_time,
|
||||
¶ms,
|
||||
(64, 64),
|
||||
&mut dst,
|
||||
64 * 16,
|
||||
)
|
||||
.expect("montage render");
|
||||
dst
|
||||
};
|
||||
let pixel = |frame: &[u8]| {
|
||||
let off = (8 * 64 + 8) * 16;
|
||||
[0, 1, 2, 3]
|
||||
.map(|i| f32::from_le_bytes(frame[off + i * 4..off + i * 4 + 4].try_into().unwrap()))
|
||||
[0, 1, 2, 3].map(|i| {
|
||||
f32::from_le_bytes(frame[off + i * 4..off + i * 4 + 4].try_into().unwrap())
|
||||
})
|
||||
};
|
||||
|
||||
let (montage, spans) = video_montage_with_adjustments(&project, seq, time);
|
||||
@@ -2027,10 +2111,17 @@ mod tests {
|
||||
// V1's red then V3's blue, and the boundary sits between them.
|
||||
assert_eq!(montage.len(), 2, "the two clips cover frame 0");
|
||||
assert!(montage[0].effects.is_empty(), "the red clip has no stack");
|
||||
assert_eq!(montage[1].effects.len(), 1, "the blue clip's own stack rides it");
|
||||
assert_eq!(
|
||||
montage[1].effects.len(),
|
||||
1,
|
||||
"the blue clip's own stack rides it"
|
||||
);
|
||||
assert_eq!(spans.len(), 1, "the layer emits one span");
|
||||
assert_eq!(spans[0].track_index, 1, "the layer grades only V1's clip");
|
||||
assert!(!spans[0].effects.is_empty(), "the layer carries its opacity");
|
||||
assert!(
|
||||
!spans[0].effects.is_empty(),
|
||||
"the layer carries its opacity"
|
||||
);
|
||||
|
||||
let baseline = pixel(&render(montage.clone(), Vec::new(), time));
|
||||
let graded = pixel(&render(montage.clone(), spans.clone(), time));
|
||||
@@ -2082,8 +2173,7 @@ mod tests {
|
||||
#[test]
|
||||
fn clip_multicam_media_resolves_the_current_angle() {
|
||||
let _media = media_lock();
|
||||
let media =
|
||||
std::env::temp_dir().join(format!("oakapp_mcmedia_{}.mp4", std::process::id()));
|
||||
let media = std::env::temp_dir().join(format!("oakapp_mcmedia_{}.mp4", std::process::id()));
|
||||
oak_codec::testmedia::write_test_clip(&media, 64, 64, 10, 10).expect("generate test media");
|
||||
|
||||
let (project, seq, footage) = project_with_clip(&media);
|
||||
@@ -2114,19 +2204,20 @@ mod tests {
|
||||
-1,
|
||||
oak_node::value::NodeValue::Combo(0),
|
||||
);
|
||||
}
|
||||
}
|
||||
g.graph
|
||||
.connect(source_seq, id, oak_node::nodes::multicamnode::SEQUENCE_INPUT, -1)
|
||||
.connect(
|
||||
source_seq,
|
||||
id,
|
||||
oak_node::nodes::multicamnode::SEQUENCE_INPUT,
|
||||
-1,
|
||||
)
|
||||
.unwrap();
|
||||
// The angle CLIP feeds the source array at element 0 (the
|
||||
// traverser's `active_elements_at_time` keeps only the current
|
||||
// source, so multi `value()` forwards exactly it).
|
||||
g.graph
|
||||
.input_array_insert(
|
||||
id,
|
||||
oak_node::nodes::multicamnode::SOURCES_INPUT,
|
||||
0,
|
||||
)
|
||||
.input_array_insert(id, oak_node::nodes::multicamnode::SOURCES_INPUT, 0)
|
||||
.unwrap();
|
||||
g.graph
|
||||
.connect(
|
||||
@@ -2140,15 +2231,24 @@ mod tests {
|
||||
};
|
||||
// A host clip on the host sequence (default layout V1) fed from the
|
||||
// multicam output; the clip's own input must first be connected.
|
||||
let host_clip = graphops::place_footage_clip(&project, seq, footage, TrackType::Video, 0, 0, 10, 0)
|
||||
.expect("place the host clip");
|
||||
let host_clip =
|
||||
graphops::place_footage_clip(&project, seq, footage, TrackType::Video, 0, 0, 10, 0)
|
||||
.expect("place the host clip");
|
||||
{
|
||||
let mut g = graphops::lock(&project);
|
||||
let _ = g
|
||||
.graph
|
||||
.disconnect(footage, host_clip, oak_node::block::clip_input::TEXTURE_INPUT, -1);
|
||||
g.graph.disconnect(
|
||||
footage,
|
||||
host_clip,
|
||||
oak_node::block::clip_input::TEXTURE_INPUT,
|
||||
-1,
|
||||
);
|
||||
g.graph
|
||||
.connect(mc, host_clip, oak_node::block::clip_input::TEXTURE_INPUT, -1)
|
||||
.connect(
|
||||
mc,
|
||||
host_clip,
|
||||
oak_node::block::clip_input::TEXTURE_INPUT,
|
||||
-1,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
// The montage at host time 0 resolves to the angle's media.
|
||||
@@ -2263,8 +2363,9 @@ mod tests {
|
||||
.copied()
|
||||
.expect("red clip")
|
||||
};
|
||||
let blue_clip = graphops::place_footage_clip(&project, seq, bfoot, TrackType::Video, 1, 0, 10, 0)
|
||||
.expect("blue angle slot");
|
||||
let blue_clip =
|
||||
graphops::place_footage_clip(&project, seq, bfoot, TrackType::Video, 1, 0, 10, 0)
|
||||
.expect("blue angle slot");
|
||||
let mc = {
|
||||
let mut g = graphops::lock(&project);
|
||||
let (core, behavior) = oak_node::nodes::multicamnode::create();
|
||||
@@ -2280,13 +2381,23 @@ mod tests {
|
||||
.input_array_insert(id, oak_node::nodes::multicamnode::SOURCES_INPUT, 0)
|
||||
.unwrap();
|
||||
g.graph
|
||||
.connect(red_clip, id, oak_node::nodes::multicamnode::SOURCES_INPUT, 0)
|
||||
.connect(
|
||||
red_clip,
|
||||
id,
|
||||
oak_node::nodes::multicamnode::SOURCES_INPUT,
|
||||
0,
|
||||
)
|
||||
.unwrap();
|
||||
g.graph
|
||||
.input_array_insert(id, oak_node::nodes::multicamnode::SOURCES_INPUT, 1)
|
||||
.unwrap();
|
||||
g.graph
|
||||
.connect(blue_clip, id, oak_node::nodes::multicamnode::SOURCES_INPUT, 1)
|
||||
.connect(
|
||||
blue_clip,
|
||||
id,
|
||||
oak_node::nodes::multicamnode::SOURCES_INPUT,
|
||||
1,
|
||||
)
|
||||
.unwrap();
|
||||
id
|
||||
};
|
||||
@@ -2296,14 +2407,19 @@ mod tests {
|
||||
.expect("host clip");
|
||||
{
|
||||
let mut g = graphops::lock(&project);
|
||||
let _ = g.graph.disconnect(
|
||||
g.graph.disconnect(
|
||||
_rfoot,
|
||||
host_clip,
|
||||
oak_node::block::clip_input::TEXTURE_INPUT,
|
||||
-1,
|
||||
);
|
||||
g.graph
|
||||
.connect(mc, host_clip, oak_node::block::clip_input::TEXTURE_INPUT, -1)
|
||||
.connect(
|
||||
mc,
|
||||
host_clip,
|
||||
oak_node::block::clip_input::TEXTURE_INPUT,
|
||||
-1,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
@@ -2332,7 +2448,10 @@ mod tests {
|
||||
};
|
||||
// Source 0 = red angle.
|
||||
let (r0, b0) = render_rgb(0);
|
||||
assert!(r0 > 0.4 && b0 < 0.4, "source 0 is the red angle (r={r0} b={b0})");
|
||||
assert!(
|
||||
r0 > 0.4 && b0 < 0.4,
|
||||
"source 0 is the red angle (r={r0} b={b0})"
|
||||
);
|
||||
|
||||
// Switch to source 1: current_in drives the element the multi
|
||||
// `value()` forwards — the frame must turn blue.
|
||||
@@ -2490,7 +2609,8 @@ mod tests {
|
||||
#[test]
|
||||
fn audio_montage_overlaps_the_range() {
|
||||
let _media = media_lock();
|
||||
let media = std::env::temp_dir().join(format!("oakapp_montage_a_{}.mp4", std::process::id()));
|
||||
let media =
|
||||
std::env::temp_dir().join(format!("oakapp_montage_a_{}.mp4", std::process::id()));
|
||||
oak_codec::testmedia::write_test_clip(&media, 64, 64, 10, 10).expect("generate test media");
|
||||
|
||||
let (project, seq, footage) = project_with_clip(&media);
|
||||
@@ -2502,9 +2622,15 @@ mod tests {
|
||||
|
||||
let overlapping = audio_montage(&project, seq, TimeRange::new(at(0), at(5)));
|
||||
assert_eq!(overlapping.len(), 1, "the range overlaps the audio clip");
|
||||
assert_eq!(overlapping[0].stream_index, 1, "the audio stream is selected");
|
||||
assert_eq!(
|
||||
overlapping[0].stream_index, 1,
|
||||
"the audio stream is selected"
|
||||
);
|
||||
let disjoint = audio_montage(&project, seq, TimeRange::new(at(10), at(20)));
|
||||
assert!(disjoint.is_empty(), "a range past the clip overlaps nothing");
|
||||
assert!(
|
||||
disjoint.is_empty(),
|
||||
"a range past the clip overlaps nothing"
|
||||
);
|
||||
oak_undo::global::clear().unwrap();
|
||||
let _ = std::fs::remove_file(&media);
|
||||
}
|
||||
@@ -2515,7 +2641,8 @@ mod tests {
|
||||
#[test]
|
||||
fn montages_skip_muted_tracks() {
|
||||
let _media = media_lock();
|
||||
let media = std::env::temp_dir().join(format!("oakapp_montage_m_{}.mp4", std::process::id()));
|
||||
let media =
|
||||
std::env::temp_dir().join(format!("oakapp_montage_m_{}.mp4", std::process::id()));
|
||||
oak_codec::testmedia::write_test_clip(&media, 64, 64, 10, 10).expect("generate test media");
|
||||
|
||||
let (project, seq, footage) = project_with_clip(&media);
|
||||
@@ -2548,8 +2675,16 @@ mod tests {
|
||||
|
||||
oak_undo::global::undo().unwrap();
|
||||
oak_undo::global::undo().unwrap();
|
||||
assert_eq!(video_montage(&project, seq, at(0)).len(), 1, "undo restores the video clip");
|
||||
assert_eq!(audio_montage(&project, seq, range).len(), 1, "undo restores the audio clip");
|
||||
assert_eq!(
|
||||
video_montage(&project, seq, at(0)).len(),
|
||||
1,
|
||||
"undo restores the video clip"
|
||||
);
|
||||
assert_eq!(
|
||||
audio_montage(&project, seq, range).len(),
|
||||
1,
|
||||
"undo restores the audio clip"
|
||||
);
|
||||
oak_undo::global::clear().unwrap();
|
||||
let _ = std::fs::remove_file(&media);
|
||||
}
|
||||
@@ -2569,7 +2704,11 @@ mod tests {
|
||||
// underflows the renderable minimum.
|
||||
assert_eq!(full_res_render_size(0, 0, 1), (2, 2));
|
||||
assert_eq!(full_res_render_size(1, 1, 8), (2, 2));
|
||||
assert_eq!(full_res_render_size(1920, 1080, 0), (1920, 1080), "a zero divider means Full");
|
||||
assert_eq!(
|
||||
full_res_render_size(1920, 1080, 0),
|
||||
(1920, 1080),
|
||||
"a zero divider means Full"
|
||||
);
|
||||
}
|
||||
|
||||
/// Clamping only shrinks: a render that already fits a limit is left
|
||||
@@ -2583,7 +2722,11 @@ mod tests {
|
||||
(960, 540),
|
||||
"a smaller request is not upscaled to the proxy"
|
||||
);
|
||||
assert_eq!(clamp_render_size(1920, 1080, &[]), (1920, 1080), "no limits = no clamping");
|
||||
assert_eq!(
|
||||
clamp_render_size(1920, 1080, &[]),
|
||||
(1920, 1080),
|
||||
"no limits = no clamping"
|
||||
);
|
||||
assert_eq!(
|
||||
clamp_render_size(1920, 1080, &[(640, 640)]),
|
||||
(640, 360),
|
||||
@@ -2609,7 +2752,8 @@ mod tests {
|
||||
std::env::temp_dir().join(format!("oakapp_proxy_src_{}.mp4", std::process::id()));
|
||||
let proxy =
|
||||
std::env::temp_dir().join(format!("oakapp_proxy_32_{}.mp4", std::process::id()));
|
||||
oak_codec::testmedia::write_test_clip(&orig, 64, 64, 10, 10).expect("generate the source media");
|
||||
oak_codec::testmedia::write_test_clip(&orig, 64, 64, 10, 10)
|
||||
.expect("generate the source media");
|
||||
oak_codec::testmedia::write_test_clip_solid(&proxy, 32, 32, 10, 10, [0.1, 0.1, 0.9, 1.0])
|
||||
.expect("generate the proxy media");
|
||||
|
||||
@@ -2626,7 +2770,11 @@ mod tests {
|
||||
let f = g
|
||||
.graph
|
||||
.get_mut(footage)
|
||||
.and_then(|e| e.behavior.as_any_mut()?.downcast_mut::<oak_node::footage::FootageBehavior>())
|
||||
.and_then(|e| {
|
||||
e.behavior
|
||||
.as_any_mut()?
|
||||
.downcast_mut::<oak_node::footage::FootageBehavior>()
|
||||
})
|
||||
.expect("the footage behavior");
|
||||
f.proxy = proxy.to_string_lossy().into_owned();
|
||||
f.proxy_enabled = true;
|
||||
@@ -2644,22 +2792,39 @@ mod tests {
|
||||
let f = graphops::footage_behavior(&g.graph, footage).expect("the footage behavior");
|
||||
preview_footage_media(f, true).0
|
||||
};
|
||||
assert_eq!(selected, proxy.to_string_lossy(), "the proxy stands in for the source");
|
||||
assert_eq!(
|
||||
selected,
|
||||
proxy.to_string_lossy(),
|
||||
"the proxy stands in for the source"
|
||||
);
|
||||
|
||||
// …and the sequence frame ticket is clamped to its 32x32 size.
|
||||
let tb = graphops::sequence_time_base(&lock(&project).graph, seq).unwrap();
|
||||
let params =
|
||||
sequence_frame_params(&project, seq, 0, tb, 64, 64, None).expect("sequence frame params");
|
||||
assert_eq!(params.force_size, Some((32, 32)), "the proxy bounds the render size");
|
||||
let params = sequence_frame_params(&project, seq, 0, tb, 64, 64, None)
|
||||
.expect("sequence frame params");
|
||||
assert_eq!(
|
||||
params.force_size,
|
||||
Some((32, 32)),
|
||||
"the proxy bounds the render size"
|
||||
);
|
||||
|
||||
// The decoded pixels are the proxy's (solid blue), not the source's.
|
||||
let mut dst = vec![0u8; 32 * 32 * 16];
|
||||
oak_render::eval::render_montage_frame_into(params.time, ¶ms, (32, 32), &mut dst, 32 * 16)
|
||||
.expect("montage render at the proxy size");
|
||||
oak_render::eval::render_montage_frame_into(
|
||||
params.time,
|
||||
¶ms,
|
||||
(32, 32),
|
||||
&mut dst,
|
||||
32 * 16,
|
||||
)
|
||||
.expect("montage render at the proxy size");
|
||||
let off = (8 * 32 + 8) * 16;
|
||||
let r = f32::from_le_bytes(dst[off..off + 4].try_into().unwrap());
|
||||
let b = f32::from_le_bytes(dst[off + 8..off + 12].try_into().unwrap());
|
||||
assert!(b > 0.5 && r < 0.4, "the proxy's blue covers the frame (r={r}, b={b})");
|
||||
assert!(
|
||||
b > 0.5 && r < 0.4,
|
||||
"the proxy's blue covers the frame (r={r}, b={b})"
|
||||
);
|
||||
|
||||
store.set(None, "UseProxyMedia", &old);
|
||||
oak_undo::global::clear().unwrap();
|
||||
|
||||
@@ -97,7 +97,7 @@ pub(crate) fn analyze_bgra8(width: u32, height: u32, bytes: &[u8]) -> ScopeData
|
||||
let mut luma = Vec::with_capacity(pixels);
|
||||
let mut chroma = Vec::with_capacity(pixels);
|
||||
let src = bytes.get(..pixels * 4).unwrap_or_default();
|
||||
for px in src.chunks_exact(4) {
|
||||
for px in src.as_chunks::<4>().0 {
|
||||
let b = f32::from(px[0]) / 255.0;
|
||||
let g = f32::from(px[1]) / 255.0;
|
||||
let r = f32::from(px[2]) / 255.0;
|
||||
|
||||
@@ -90,7 +90,11 @@ impl WaveformCache {
|
||||
|
||||
/// The cached waveform of `clip`, if extracted.
|
||||
pub fn get(&self, clip: u64) -> Option<Arc<ClipWaveform>> {
|
||||
self.map.lock().unwrap_or_else(|e| e.into_inner()).get(&clip).cloned()
|
||||
self.map
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.get(&clip)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// Ensure `clip` (with media `filename`, `duration_frames` long) has a
|
||||
@@ -105,16 +109,17 @@ impl WaveformCache {
|
||||
return;
|
||||
};
|
||||
let mut map = self.map.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if !map.contains_key(&clip) {
|
||||
map.insert(clip, Arc::new(waveform));
|
||||
self.version.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
if let std::collections::hash_map::Entry::Vacant(e) = map.entry(clip) {
|
||||
e.insert(Arc::new(waveform));
|
||||
self.version
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract a clip's waveform (first channel) via oakaudio's waveform
|
||||
/// extractor.
|
||||
fn extract(filename: &str, duration_frames: i64, _fps: f32) -> Option<ClipWaveform> {
|
||||
fn extract(filename: &str, duration_frames: i64, _fps: f32) -> Option<ClipWaveform> {
|
||||
let cname = std::ffi::CString::new(filename).ok()?;
|
||||
const SAMPLES_PER_POINT: i32 = 256;
|
||||
// Audio-stream index 0 (probe numbering).
|
||||
|
||||
@@ -106,7 +106,7 @@ mod tests {
|
||||
let peaks: Vec<MinMax> = (0..points)
|
||||
.map(|i| {
|
||||
let sample = i64::from(i) * i64::from(SPP);
|
||||
let inside = sample >= 48_000 && sample < 96_000;
|
||||
let inside = (48_000..96_000).contains(&sample);
|
||||
if inside {
|
||||
MinMax { min: -1.0, max: 1.0 }
|
||||
} else {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::oakui::component::text_input;
|
||||
use gpui::colors::DefaultColors;
|
||||
use gpui::dock::{DockPanel, PanelEvent};
|
||||
use gpui::{
|
||||
@@ -30,7 +31,6 @@ use gpui::{
|
||||
Render, SharedString, Window,
|
||||
};
|
||||
use gpui_elements::editable_text::{EditableTextState, StringStorage, TextChanged};
|
||||
use crate::oakui::component::text_input;
|
||||
|
||||
use crate::i18n;
|
||||
use crate::oakui::effectchain::group_label;
|
||||
@@ -60,7 +60,8 @@ impl<E: AppEngine> EffectLibraryPanel<E> {
|
||||
pub fn new(engine: Entity<E>, _window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
// Re-read the effect table whenever the engine notifies (the table
|
||||
// itself is static, but the selection hint depends on the target).
|
||||
cx.observe(&engine, |_this, _engine, cx| cx.notify()).detach();
|
||||
cx.observe(&engine, |_this, _engine, cx| cx.notify())
|
||||
.detach();
|
||||
let search = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx));
|
||||
cx.subscribe(&search, |_this, _state, _event: &TextChanged, cx| {
|
||||
cx.notify();
|
||||
@@ -212,7 +213,7 @@ impl<E: AppEngine> Render for EffectLibraryPanel<E> {
|
||||
list = list.child(
|
||||
div()
|
||||
.id(SharedString::from(format!("effect-library-{type_id}")))
|
||||
.debug_selector(move || format!("effect-library-row-{row_id}").into())
|
||||
.debug_selector(move || format!("effect-library-row-{row_id}"))
|
||||
.cursor_pointer()
|
||||
.px_2()
|
||||
.py_1()
|
||||
@@ -296,7 +297,9 @@ fn group_header(
|
||||
on_toggle: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> impl IntoElement {
|
||||
div()
|
||||
.id(SharedString::from(format!("effect-library-group-{group_key}")))
|
||||
.id(SharedString::from(format!(
|
||||
"effect-library-group-{group_key}"
|
||||
)))
|
||||
.debug_selector({
|
||||
let key = group_key.to_string();
|
||||
move || format!("effect-library-group-row-{key}")
|
||||
@@ -397,7 +400,10 @@ mod tests {
|
||||
let mut collapsed = HashSet::new();
|
||||
|
||||
assert!(!hidden_by_collapse(&color, "", &collapsed));
|
||||
assert!(toggle_collapsed(&mut collapsed, "color"), "first toggle collapses");
|
||||
assert!(
|
||||
toggle_collapsed(&mut collapsed, "color"),
|
||||
"first toggle collapses"
|
||||
);
|
||||
assert!(hidden_by_collapse(&color, "", &collapsed));
|
||||
assert!(
|
||||
!hidden_by_collapse(&color, "blur", &collapsed),
|
||||
@@ -407,7 +413,10 @@ mod tests {
|
||||
!hidden_by_collapse(&color, "", &HashSet::new()),
|
||||
"an unlisted group stays open"
|
||||
);
|
||||
assert!(!toggle_collapsed(&mut collapsed, "color"), "second toggle expands");
|
||||
assert!(
|
||||
!toggle_collapsed(&mut collapsed, "color"),
|
||||
"second toggle expands"
|
||||
);
|
||||
assert!(collapsed.is_empty());
|
||||
}
|
||||
|
||||
@@ -440,9 +449,14 @@ mod tests {
|
||||
#[test]
|
||||
fn collapsed_state_persists_in_the_config() {
|
||||
let _guard = collapsed_config_lock();
|
||||
assert!(load_collapsed().is_empty(), "the lock starts from a clear key");
|
||||
let collapsed: HashSet<String> =
|
||||
["distort", "general"].iter().map(|k| k.to_string()).collect();
|
||||
assert!(
|
||||
load_collapsed().is_empty(),
|
||||
"the lock starts from a clear key"
|
||||
);
|
||||
let collapsed: HashSet<String> = ["distort", "general"]
|
||||
.iter()
|
||||
.map(|k| k.to_string())
|
||||
.collect();
|
||||
store_collapsed(&collapsed);
|
||||
assert_eq!(load_collapsed(), collapsed);
|
||||
store_collapsed(&HashSet::new());
|
||||
@@ -456,7 +470,7 @@ mod tests {
|
||||
let _guard = collapsed_config_lock();
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(400.0), px(600.0)), |window, cx| {
|
||||
let engine = cx.new(|cx| MockEngine::demo(cx));
|
||||
let engine = cx.new(MockEngine::demo);
|
||||
EffectLibraryPanel::new(engine, window, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
@@ -492,7 +506,7 @@ mod tests {
|
||||
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(400.0), px(600.0)), |window, cx| {
|
||||
let engine = cx.new(|cx| MockEngine::demo(cx));
|
||||
let engine = cx.new(MockEngine::demo);
|
||||
let mut panel = EffectLibraryPanel::new(engine, window, cx);
|
||||
// The state the toggle writes (and the config reloads on the
|
||||
// next start), without touching the process-global key.
|
||||
@@ -546,7 +560,7 @@ mod tests {
|
||||
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(400.0), px(600.0)), |window, cx| {
|
||||
let engine = cx.new(|cx| MockEngine::demo(cx));
|
||||
let engine = cx.new(MockEngine::demo);
|
||||
EffectLibraryPanel::new(engine, window, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -268,7 +268,7 @@ mod tests {
|
||||
) {
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(320.0), px(400.0)), |window, cx| {
|
||||
let engine = cx.new(|cx| MockEngine::demo(cx));
|
||||
let engine = cx.new(MockEngine::demo);
|
||||
HistoryPanel::new(engine, window, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
//! 输出) with add / remove / reorder — no engine, the mock applies the edits
|
||||
//! to its own model.
|
||||
|
||||
use crate::oakui::component::menu::{Menu, MenuItem};
|
||||
use gpui::colors::DefaultColors;
|
||||
use gpui::dock::{DockPanel, PanelEvent};
|
||||
use gpui::effect_stack::{EffectCardKind, EffectId, EffectStackEvent, EffectStackView};
|
||||
@@ -26,7 +27,6 @@ use gpui::{
|
||||
div, prelude::*, AnyElement, App, Context, Entity, EventEmitter, MouseButton, Render,
|
||||
SharedString, Window,
|
||||
};
|
||||
use crate::oakui::component::menu::{Menu, MenuItem};
|
||||
|
||||
use crate::oakui::component::menu::{ContextMenuHandle, ContextMenuTriggered};
|
||||
use crate::oakui::effectchain::group_label;
|
||||
@@ -57,15 +57,13 @@ impl<E: AppEngine> InspectorPanel<E> {
|
||||
pub fn new(engine: Entity<E>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let stack = cx.new(|cx| {
|
||||
let engine_for_params = engine.clone();
|
||||
EffectStackView::new(engine.clone(), cx).params_renderer(
|
||||
move |effect, window, cx| {
|
||||
// The OFX parameter view: auto-generated controls for the
|
||||
// effect's inputs (empty state for effects without a
|
||||
// parameter UI).
|
||||
cx.new(|cx| OfxParamsView::new(*effect, engine_for_params.clone(), window, cx))
|
||||
.into()
|
||||
},
|
||||
)
|
||||
EffectStackView::new(engine.clone(), cx).params_renderer(move |effect, window, cx| {
|
||||
// The OFX parameter view: auto-generated controls for the
|
||||
// effect's inputs (empty state for effects without a
|
||||
// parameter UI).
|
||||
cx.new(|cx| OfxParamsView::new(*effect, engine_for_params.clone(), window, cx))
|
||||
.into()
|
||||
})
|
||||
});
|
||||
// The "edits are requests" loop: forward each request to the engine,
|
||||
// which applies it to its model and notifies. `AddRequested` carries
|
||||
@@ -98,7 +96,8 @@ impl<E: AppEngine> InspectorPanel<E> {
|
||||
// The stack re-renders whenever the engine changes (selection-driven
|
||||
// highlight, effect edits, project drops) — the stack itself does not
|
||||
// subscribe to the engine entity.
|
||||
cx.observe(&engine, |_this, _engine, cx| cx.notify()).detach();
|
||||
cx.observe(&engine, |_this, _engine, cx| cx.notify())
|
||||
.detach();
|
||||
|
||||
let context_menu = ContextMenuHandle::new(Self::on_local_menu_item, window, cx);
|
||||
|
||||
@@ -174,7 +173,7 @@ impl<E: AppEngine> InspectorPanel<E> {
|
||||
list = list.child(
|
||||
div()
|
||||
.id(SharedString::from(format!("add-effect-group-{key}")))
|
||||
.debug_selector(move || format!("add-effect-group-row-{key}").into())
|
||||
.debug_selector(move || format!("add-effect-group-row-{key}"))
|
||||
.pt_2()
|
||||
.pb_1()
|
||||
.px_2()
|
||||
|
||||
@@ -538,7 +538,7 @@ mod tests {
|
||||
async fn panel_renders_the_mock_grid(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(640.0), px(360.0)), |window, cx| {
|
||||
let engine = cx.new(|cx| crate::oakui::MockEngine::demo(cx));
|
||||
let engine = cx.new(crate::oakui::MockEngine::demo);
|
||||
let clock = engine.read(cx).program_clock().clone();
|
||||
MulticamPanel::new(engine, clock, window, cx)
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crate::oakui::component::menu::{Menu, MenuItem};
|
||||
use gpui::colors::DefaultColors;
|
||||
use gpui::dock::{DockPanel, PanelEvent};
|
||||
use gpui::node_graph::{
|
||||
@@ -34,13 +35,12 @@ use gpui::node_graph::{
|
||||
NodeVisualState, MAX_ZOOM, MIN_ZOOM,
|
||||
};
|
||||
use gpui::{
|
||||
div, point, prelude::*, px, AnyElement, App, Bounds, ClickEvent, Context, Entity,
|
||||
EventEmitter, MouseButton, Pixels, Point, Render, SharedString, Window,
|
||||
div, point, prelude::*, px, AnyElement, App, Bounds, ClickEvent, Context, Entity, EventEmitter,
|
||||
MouseButton, Pixels, Point, Render, SharedString, Window,
|
||||
};
|
||||
use crate::oakui::component::menu::{Menu, MenuItem};
|
||||
|
||||
use crate::oakui::component::menu::{ContextMenuHandle, ContextMenuTriggered};
|
||||
use crate::oakui::component::menu;
|
||||
use crate::oakui::component::menu::{ContextMenuHandle, ContextMenuTriggered};
|
||||
use crate::oakui::{AppEngine, NodeLibraryEntry};
|
||||
use crate::panels::commands::PanelCommandHandler;
|
||||
use crate::panels::ids::NODE_EDITOR;
|
||||
@@ -203,8 +203,9 @@ impl<E: AppEngine> NodeEditorPanel<E> {
|
||||
let Some(node) = node else {
|
||||
return;
|
||||
};
|
||||
self.graph
|
||||
.update(cx, |graph, cx| graph.set_selection(BTreeSet::from([NodeId(node)]), cx));
|
||||
self.graph.update(cx, |graph, cx| {
|
||||
graph.set_selection(BTreeSet::from([NodeId(node)]), cx)
|
||||
});
|
||||
}
|
||||
|
||||
/// Handles the node editor's local (non-registry) context-menu items.
|
||||
@@ -221,9 +222,10 @@ impl<E: AppEngine> NodeEditorPanel<E> {
|
||||
.map(|entry| entry.1.clone());
|
||||
if let Some(type_id) = type_id {
|
||||
let position = self.add_node_position.unwrap_or_default();
|
||||
if let Err(err) = self.engine.update(cx, |engine, cx| {
|
||||
engine.add_node_at(&type_id, position, cx)
|
||||
}) {
|
||||
if let Err(err) = self
|
||||
.engine
|
||||
.update(cx, |engine, cx| engine.add_node_at(&type_id, position, cx))
|
||||
{
|
||||
println!("[node editor] add node failed: {err}");
|
||||
}
|
||||
}
|
||||
@@ -394,8 +396,7 @@ impl<E: AppEngine> Render for NodeEditorPanel<E> {
|
||||
.graph_position_at(window.mouse_position());
|
||||
let type_id = payload.type_id.to_string();
|
||||
this.engine.update(cx, |engine, cx| {
|
||||
if let Err(err) = engine.add_node_at(&type_id, graph_position, cx)
|
||||
{
|
||||
if let Err(err) = engine.add_node_at(&type_id, graph_position, cx) {
|
||||
println!("[node editor] add node failed: {err}");
|
||||
}
|
||||
});
|
||||
@@ -508,12 +509,18 @@ pub(crate) fn node_menu(protected: bool) -> Menu {
|
||||
items.push(MenuItem::new(LOCAL_GROUP, tr("node.context.group")));
|
||||
items.push(MenuItem::new(LOCAL_UNGROUP, tr("node.context.ungroup")));
|
||||
items.push(menu::color_label_item(None).separated());
|
||||
items.push(MenuItem::new(LOCAL_OPEN_IN_VIEWER, tr("node.context.open_in_viewer")));
|
||||
items.push(MenuItem::new(
|
||||
LOCAL_OPEN_IN_VIEWER,
|
||||
tr("node.context.open_in_viewer"),
|
||||
));
|
||||
items.push(MenuItem::new(
|
||||
LOCAL_SHOW_IN_PARAM_EDITOR,
|
||||
tr("node.context.show_in_param_editor"),
|
||||
));
|
||||
items.push(MenuItem::new(LOCAL_NODE_PROPERTIES, tr("menu.context.properties")));
|
||||
items.push(MenuItem::new(
|
||||
LOCAL_NODE_PROPERTIES,
|
||||
tr("menu.context.properties"),
|
||||
));
|
||||
Menu::new(items)
|
||||
}
|
||||
|
||||
@@ -538,27 +545,21 @@ pub(crate) fn background_menu(
|
||||
let mut add_items: Vec<MenuItem> = Vec::new();
|
||||
let mut next_id = LOCAL_ADD_NODE_BASE;
|
||||
for (category_key, mut entries) in groups {
|
||||
entries.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
||||
entries.sort_by_key(|a| a.name.to_lowercase());
|
||||
let mut submenu = Vec::with_capacity(entries.len());
|
||||
for entry in entries {
|
||||
submenu.push(MenuItem::new(next_id, entry.name.clone()));
|
||||
add_menu_ids.push((next_id, entry.type_id));
|
||||
next_id += 1;
|
||||
}
|
||||
add_items.push(
|
||||
MenuItem::new(0, tr(category_key)).with_submenu(Menu::new(submenu)),
|
||||
);
|
||||
add_items.push(MenuItem::new(0, tr(category_key)).with_submenu(Menu::new(submenu)));
|
||||
}
|
||||
|
||||
let direction_menu = Menu::new(vec![
|
||||
MenuItem::new(LOCAL_DIR_TOP_BOTTOM, tr("node.context.dir_top_bottom"))
|
||||
.with_checked(true),
|
||||
MenuItem::new(LOCAL_DIR_BOTTOM_TOP, tr("node.context.dir_bottom_top"))
|
||||
.with_checked(false),
|
||||
MenuItem::new(LOCAL_DIR_LEFT_RIGHT, tr("node.context.dir_left_right"))
|
||||
.with_checked(false),
|
||||
MenuItem::new(LOCAL_DIR_RIGHT_LEFT, tr("node.context.dir_right_left"))
|
||||
.with_checked(false),
|
||||
MenuItem::new(LOCAL_DIR_TOP_BOTTOM, tr("node.context.dir_top_bottom")).with_checked(true),
|
||||
MenuItem::new(LOCAL_DIR_BOTTOM_TOP, tr("node.context.dir_bottom_top")).with_checked(false),
|
||||
MenuItem::new(LOCAL_DIR_LEFT_RIGHT, tr("node.context.dir_left_right")).with_checked(false),
|
||||
MenuItem::new(LOCAL_DIR_RIGHT_LEFT, tr("node.context.dir_right_left")).with_checked(false),
|
||||
]);
|
||||
|
||||
Menu::new(vec![
|
||||
@@ -592,7 +593,7 @@ mod tests {
|
||||
) {
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(640.0), px(480.0)), |window, cx| {
|
||||
let engine = cx.new(|cx| crate::oakui::MockEngine::demo(cx));
|
||||
let engine = cx.new(crate::oakui::MockEngine::demo);
|
||||
NodeEditorPanel::new(engine, window, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
@@ -638,7 +639,9 @@ mod tests {
|
||||
fn node_menu_carries_grouping_and_reveals() {
|
||||
// The color-label lookup below matches on a localized label: pin
|
||||
// en-US under the shared language lock.
|
||||
let _guard = crate::i18n::lang_test_lock().lock().unwrap_or_else(|e| e.into_inner());
|
||||
let _guard = crate::i18n::lang_test_lock()
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
crate::i18n::set_language_code("en-US");
|
||||
let menu = node_menu(false);
|
||||
let ids: Vec<usize> = menu.items.iter().map(|item| item.id).collect();
|
||||
@@ -761,7 +764,12 @@ mod tests {
|
||||
cx.update(|_window, app| {
|
||||
let engine = panel.read(app).engine.clone();
|
||||
engine.update(app, |engine, cx| {
|
||||
engine.apply_effect_event(&EffectStackEvent::CardSelected { effect: EffectId(1) }, cx);
|
||||
engine.apply_effect_event(
|
||||
&EffectStackEvent::CardSelected {
|
||||
effect: EffectId(1),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -138,45 +138,55 @@ impl<E: AppEngine> ProgramViewerPanel<E> {
|
||||
// Route every transport request to the engine's program monitor, and
|
||||
// forward the picture's pointer/key events to the active OFX interact
|
||||
// (no-op when none is live).
|
||||
cx.subscribe(&viewer, |this, _viewer, event: &ViewerEvent, cx| match event {
|
||||
ViewerEvent::InteractPointer {
|
||||
kind,
|
||||
position,
|
||||
button,
|
||||
pressed,
|
||||
} => this.forward_interact_pointer(*kind, *position, *button, *pressed, cx),
|
||||
ViewerEvent::InteractKey { down, keystroke } => {
|
||||
this.forward_interact_key(*down, keystroke, cx)
|
||||
}
|
||||
// The widget already toggled its own overlay state; nothing to
|
||||
// forward to the engine.
|
||||
ViewerEvent::ToggleSafeFramesRequested { .. } | ViewerEvent::ToggleZoomRequested { .. } => {}
|
||||
// The loop in/out range is the shell-owned program workarea, so
|
||||
// the panel re-emits the request.
|
||||
ViewerEvent::InPointRequested { .. } => cx.emit(menu::ViewerPanelEvent::SetInPoint),
|
||||
ViewerEvent::OutPointRequested { .. } => cx.emit(menu::ViewerPanelEvent::SetOutPoint),
|
||||
ViewerEvent::ClearRangeRequested { .. } => cx.emit(menu::ViewerPanelEvent::ClearRange),
|
||||
// The OFX color picker armed the eyedropper and the user clicked
|
||||
// the frame: hand the sampled colour back to the engine's mailbox.
|
||||
ViewerEvent::EyedropperPick { color } => {
|
||||
this.engine
|
||||
.update(cx, |engine, cx| engine.eyedropper_picked(*color, cx))
|
||||
}
|
||||
event => {
|
||||
let monitor = Monitor::Program;
|
||||
this.engine.update(cx, |engine, cx| match event {
|
||||
ViewerEvent::PlayRequested { .. } => engine.play(monitor, cx),
|
||||
ViewerEvent::PauseRequested { .. } => engine.pause(monitor, cx),
|
||||
ViewerEvent::StepRequested { delta, .. } => engine.step(monitor, *delta, cx),
|
||||
other => println!("[program viewer] request: {other:?}"),
|
||||
});
|
||||
}
|
||||
})
|
||||
cx.subscribe(
|
||||
&viewer,
|
||||
|this, _viewer, event: &ViewerEvent, cx| match event {
|
||||
ViewerEvent::InteractPointer {
|
||||
kind,
|
||||
position,
|
||||
button,
|
||||
pressed,
|
||||
} => this.forward_interact_pointer(*kind, *position, *button, *pressed, cx),
|
||||
ViewerEvent::InteractKey { down, keystroke } => {
|
||||
this.forward_interact_key(*down, keystroke, cx)
|
||||
}
|
||||
// The widget already toggled its own overlay state; nothing to
|
||||
// forward to the engine.
|
||||
ViewerEvent::ToggleSafeFramesRequested { .. }
|
||||
| ViewerEvent::ToggleZoomRequested { .. } => {}
|
||||
// The loop in/out range is the shell-owned program workarea, so
|
||||
// the panel re-emits the request.
|
||||
ViewerEvent::InPointRequested { .. } => cx.emit(menu::ViewerPanelEvent::SetInPoint),
|
||||
ViewerEvent::OutPointRequested { .. } => {
|
||||
cx.emit(menu::ViewerPanelEvent::SetOutPoint)
|
||||
}
|
||||
ViewerEvent::ClearRangeRequested { .. } => {
|
||||
cx.emit(menu::ViewerPanelEvent::ClearRange)
|
||||
}
|
||||
// The OFX color picker armed the eyedropper and the user clicked
|
||||
// the frame: hand the sampled colour back to the engine's mailbox.
|
||||
ViewerEvent::EyedropperPick { color } => this
|
||||
.engine
|
||||
.update(cx, |engine, cx| engine.eyedropper_picked(*color, cx)),
|
||||
event => {
|
||||
let monitor = Monitor::Program;
|
||||
this.engine.update(cx, |engine, cx| match event {
|
||||
ViewerEvent::PlayRequested { .. } => engine.play(monitor, cx),
|
||||
ViewerEvent::PauseRequested { .. } => engine.pause(monitor, cx),
|
||||
ViewerEvent::StepRequested { delta, .. } => {
|
||||
engine.step(monitor, *delta, cx)
|
||||
}
|
||||
other => println!("[program viewer] request: {other:?}"),
|
||||
});
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
// Re-render whenever the engine's global eyedropper arm state flips,
|
||||
// so `sync_frame` mirrors it into the viewer widget.
|
||||
cx.observe(&engine, |_this, _engine, cx| cx.notify()).detach();
|
||||
cx.observe(&engine, |_this, _engine, cx| cx.notify())
|
||||
.detach();
|
||||
|
||||
// A throttled idle pump for the OFX interact: the plugin's UI work
|
||||
// loop is served on the viewer's own timer (the app tick is
|
||||
@@ -198,8 +208,7 @@ impl<E: AppEngine> ProgramViewerPanel<E> {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let context_menu =
|
||||
ContextMenuHandle::new(Self::on_local_menu_item, window, cx);
|
||||
let context_menu = ContextMenuHandle::new(Self::on_local_menu_item, window, cx);
|
||||
|
||||
let scope_state = cx.new(|_cx| ScopeState {
|
||||
luma: Vec::new(),
|
||||
@@ -246,11 +255,13 @@ impl<E: AppEngine> ProgramViewerPanel<E> {
|
||||
match action {
|
||||
menu::ViewerMenuAction::ZoomFit => {
|
||||
let zoom = ViewerZoom::Fit;
|
||||
self.viewer.update(cx, |viewer, cx| viewer.set_zoom(zoom, cx));
|
||||
self.viewer
|
||||
.update(cx, |viewer, cx| viewer.set_zoom(zoom, cx));
|
||||
}
|
||||
menu::ViewerMenuAction::ZoomLevel(index) => {
|
||||
let zoom = ViewerZoom::Level(index);
|
||||
self.viewer.update(cx, |viewer, cx| viewer.set_zoom(zoom, cx));
|
||||
self.viewer
|
||||
.update(cx, |viewer, cx| viewer.set_zoom(zoom, cx));
|
||||
}
|
||||
menu::ViewerMenuAction::Resolution(divider) => self
|
||||
.engine
|
||||
@@ -280,7 +291,8 @@ impl<E: AppEngine> ProgramViewerPanel<E> {
|
||||
}),
|
||||
menu::ViewerMenuAction::ShowFps => {
|
||||
let show = self.viewer.read(cx).show_fps();
|
||||
self.viewer.update(cx, |viewer, cx| viewer.set_show_fps(!show, cx));
|
||||
self.viewer
|
||||
.update(cx, |viewer, cx| viewer.set_show_fps(!show, cx));
|
||||
}
|
||||
menu::ViewerMenuAction::SaveFrame => self.save_frame(cx),
|
||||
menu::ViewerMenuAction::FullScreen => {
|
||||
@@ -306,7 +318,11 @@ impl<E: AppEngine> ProgramViewerPanel<E> {
|
||||
.filter(|dir| std::fs::create_dir_all(dir).is_ok())
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("."));
|
||||
let path = dir.join(name);
|
||||
match self.engine.read(cx).save_frame(Monitor::Program, path.clone(), cx) {
|
||||
match self
|
||||
.engine
|
||||
.read(cx)
|
||||
.save_frame(Monitor::Program, path.clone(), cx)
|
||||
{
|
||||
Ok(path) => println!("[program viewer] saved frame to {}", path.display()),
|
||||
Err(error) => println!("[program viewer] save frame failed: {error}"),
|
||||
}
|
||||
@@ -335,7 +351,8 @@ impl<E: AppEngine> ProgramViewerPanel<E> {
|
||||
// cursor (the setter no-ops when the state is unchanged, so a paused
|
||||
// viewer stays inert).
|
||||
let armed = self.engine.read(cx).eyedropper_armed(cx);
|
||||
self.viewer.update(cx, |viewer, cx| viewer.set_eyedropper_armed(armed, cx));
|
||||
self.viewer
|
||||
.update(cx, |viewer, cx| viewer.set_eyedropper_armed(armed, cx));
|
||||
|
||||
// Keep the main-process interact in sync with the inspector's
|
||||
// current selection (creates/destroys the interact as the target
|
||||
@@ -380,7 +397,7 @@ impl<E: AppEngine> ProgramViewerPanel<E> {
|
||||
if let Some(bytes) = displayed.as_bytes(0) {
|
||||
let (w, h) = (sz.width.0 as usize, sz.height.0 as usize);
|
||||
let mut ppm = format!("P6\n{w} {h}\n255\n").into_bytes();
|
||||
for px in bytes[..w * h * 4].chunks_exact(4) {
|
||||
for px in bytes[..w * h * 4].as_chunks::<4>().0 {
|
||||
ppm.extend_from_slice(&[px[2], px[1], px[0]]);
|
||||
}
|
||||
let _ = std::fs::write("/tmp/oak_viewer_frame.ppm", ppm);
|
||||
@@ -406,10 +423,8 @@ impl<E: AppEngine> ProgramViewerPanel<E> {
|
||||
return frame.clone();
|
||||
};
|
||||
let frame_size = frame.size(0);
|
||||
let viewport = InteractViewport::at_frame_size(
|
||||
frame_size.width.0 as u32,
|
||||
frame_size.height.0 as u32,
|
||||
);
|
||||
let viewport =
|
||||
InteractViewport::at_frame_size(frame_size.width.0 as u32, frame_size.height.0 as u32);
|
||||
let time = self.playhead_seconds(cx);
|
||||
|
||||
// Redraw when the base frame, the target instance, the playhead, or
|
||||
@@ -428,11 +443,7 @@ impl<E: AppEngine> ProgramViewerPanel<E> {
|
||||
});
|
||||
if stale {
|
||||
if let Some(image) = crate::oakui::ofx::draw_interact_composite(
|
||||
instance,
|
||||
&interact,
|
||||
&viewport,
|
||||
time,
|
||||
frame,
|
||||
instance, &interact, &viewport, time, frame,
|
||||
) {
|
||||
self.overlay = Some(OverlayComposite {
|
||||
frame: frame.clone(),
|
||||
@@ -480,14 +491,9 @@ impl<E: AppEngine> ProgramViewerPanel<E> {
|
||||
return;
|
||||
};
|
||||
let area = size(f32::from(bounds.size.width), f32::from(bounds.size.height));
|
||||
let frame_size = self
|
||||
.engine
|
||||
.read(cx)
|
||||
.cpu_frame(Monitor::Program, cx)
|
||||
.size(0);
|
||||
let frame_size = self.engine.read(cx).cpu_frame(Monitor::Program, cx).size(0);
|
||||
let frame = size(frame_size.width.0 as f32, frame_size.height.0 as f32);
|
||||
let Some((px, py)) = crate::oakui::ofx::viewport_pixel_to_pen(position, area, frame)
|
||||
else {
|
||||
let Some((px, py)) = crate::oakui::ofx::viewport_pixel_to_pen(position, area, frame) else {
|
||||
// The pointer is in the letterbox (outside the frame rect).
|
||||
return;
|
||||
};
|
||||
@@ -519,7 +525,12 @@ impl<E: AppEngine> ProgramViewerPanel<E> {
|
||||
/// consumed by any binding. The plugin's return status is deliberately
|
||||
/// not acted on: the app does not steal key repeat or other listeners,
|
||||
/// so the interact is a passive consumer of otherwise-unused keys.
|
||||
fn forward_interact_key(&mut self, down: bool, keystroke: &gpui::Keystroke, cx: &mut Context<Self>) {
|
||||
fn forward_interact_key(
|
||||
&mut self,
|
||||
down: bool,
|
||||
keystroke: &gpui::Keystroke,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some((_, interact, _)) = crate::oakui::ofx::active_interact() else {
|
||||
return;
|
||||
};
|
||||
@@ -790,7 +801,7 @@ mod tests {
|
||||
async fn scopes_tab_renders_from_the_current_frame(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(640.0), px(360.0)), |window, cx| {
|
||||
let engine = cx.new(|cx| MockEngine::demo(cx));
|
||||
let engine = cx.new(MockEngine::demo);
|
||||
let clock = engine.read(cx).program_clock().clone();
|
||||
let meter = cx.new(|cx| AudioLevelMeter::new(30, engine.clone(), window, cx));
|
||||
ProgramViewerPanel::new(engine, clock, meter, window, cx)
|
||||
|
||||
@@ -1546,7 +1546,7 @@ mod tests {
|
||||
) {
|
||||
cx.update(|cx| cx.init_colors());
|
||||
let window = cx.open_window(size(px(width), px(height)), |window, cx| {
|
||||
let engine = cx.new(|cx| crate::oakui::MockEngine::demo(cx));
|
||||
let engine = cx.new(crate::oakui::MockEngine::demo);
|
||||
let timeline = cx.new(|cx| TimelineView::new(engine.clone(), window, cx).zoom(2.0));
|
||||
TimelinePanel::new(engine, timeline, window, cx)
|
||||
});
|
||||
|
||||
@@ -24,15 +24,15 @@
|
||||
//! Recording goes through the oakcodec encoder C ABI ([`crate::bridge`]);
|
||||
//! device/config lookups go through oak_core.
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
|
||||
use cpal::Device;
|
||||
use cpal::traits::{DeviceTrait, HostTrait};
|
||||
use oak_codec::encoder::Encoder;
|
||||
use oak_codec::encodingparams::EncodingParams;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::params::AudioParams;
|
||||
use crate::previewdevice::PreviewAudioDevice;
|
||||
use cpal::traits::{DeviceTrait, HostTrait};
|
||||
use cpal::Device;
|
||||
use oak_codec::encoder::Encoder;
|
||||
use oak_codec::encodingparams::EncodingParams;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
|
||||
|
||||
/// `paNoDevice` (PortAudio "no device" sentinel; also the default when no
|
||||
/// device is configured).
|
||||
@@ -178,7 +178,9 @@ impl ManagerInner {
|
||||
let rate = params.sample_rate;
|
||||
let channels = params.channel_count();
|
||||
let sink = self.output_buffer.clone();
|
||||
let _ = self.output_device_stream.ensure_open(device, rate, channels, sink);
|
||||
let _ = self
|
||||
.output_device_stream
|
||||
.ensure_open(device, rate, channels, sink);
|
||||
}
|
||||
self.output_buffer.write(samples);
|
||||
self.output_started = true;
|
||||
@@ -339,8 +341,6 @@ impl ManagerInner {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Peak level (linear, 0..1 and above) of each channel of the buffered,
|
||||
/// not-yet-consumed output, written to `peaks` in channel order.
|
||||
/// Returns the channel count (0 when no output is configured or the
|
||||
@@ -394,7 +394,7 @@ impl ManagerInner {
|
||||
} else {
|
||||
for (ch, plane) in planes.iter_mut().enumerate() {
|
||||
let start = ch * frame_count * 4;
|
||||
for b in buf[start..start + frame_count * 4].chunks_exact(4) {
|
||||
for b in buf[start..start + frame_count * 4].as_chunks::<4>().0 {
|
||||
plane.push(f32::from_le_bytes([b[0], b[1], b[2], b[3]]));
|
||||
}
|
||||
}
|
||||
@@ -406,7 +406,6 @@ impl ManagerInner {
|
||||
}
|
||||
Ok(channels)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Lock the process-wide manager singleton (the direct-Rust replacement
|
||||
@@ -441,7 +440,7 @@ pub fn find_config_device_by_name_s(is_output_device: bool) -> Result<Device> {
|
||||
pub fn find_device_by_name_s_or_default(name: &String, _is_output_device: bool) -> Result<Device> {
|
||||
let host = cpal::default_host();
|
||||
let device = host.devices()?.find(|d| {
|
||||
if d.id().is_err(){
|
||||
if d.id().is_err() {
|
||||
return false;
|
||||
}
|
||||
if d.supports_output() && d.id().unwrap().id() == name {
|
||||
@@ -451,16 +450,13 @@ pub fn find_device_by_name_s_or_default(name: &String, _is_output_device: bool)
|
||||
});
|
||||
if let Some(device) = device {
|
||||
Ok(device)
|
||||
}
|
||||
else{
|
||||
} else {
|
||||
if let Some(device) = host.default_output_device() {
|
||||
Ok(device)
|
||||
}
|
||||
else{
|
||||
} else {
|
||||
Err(Box::new(Error::NotFound))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// The host's output device names in enumeration order; the list index is
|
||||
@@ -505,7 +501,6 @@ pub fn device_index_by_name(name: &str, output: bool) -> Option<i32> {
|
||||
names.iter().position(|n| n == name).map(|i| i as i32)
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -527,22 +522,19 @@ mod tests {
|
||||
output_callback_body();
|
||||
let _ = tx.send(());
|
||||
});
|
||||
if rx
|
||||
.recv_timeout(std::time::Duration::from_secs(60))
|
||||
.is_err()
|
||||
{
|
||||
if rx.recv_timeout(std::time::Duration::from_secs(60)).is_err() {
|
||||
eprintln!("audio host wedged (headless/CI); skipping");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// The test body (see the wrapping test for the watchdog rationale).
|
||||
fn output_callback_body() { // Skip when the audio system cannot actually run a stream: open a
|
||||
fn output_callback_body() {
|
||||
// Skip when the audio system cannot actually run a stream: open a
|
||||
// silent stream and require at least one callback within 2 s. A
|
||||
// device existing is not enough — headless sessions report the
|
||||
// stream running while delivering zero callbacks.
|
||||
use std::sync::atomic::AtomicI64 as A;
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use std::sync::atomic::AtomicI64 as A;
|
||||
static PROBE: A = A::new(0);
|
||||
PROBE.store(0, Ordering::Relaxed);
|
||||
let can_play = (|| {
|
||||
@@ -556,12 +548,7 @@ mod tests {
|
||||
}
|
||||
};
|
||||
let stream = device
|
||||
.build_output_stream(
|
||||
config.config(),
|
||||
cb,
|
||||
|_| {},
|
||||
None,
|
||||
)
|
||||
.build_output_stream(config.config(), cb, |_| {}, None)
|
||||
.ok()?;
|
||||
stream.play().ok()?;
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
|
||||
@@ -593,16 +580,12 @@ mod tests {
|
||||
samples.push(v);
|
||||
samples.push(v);
|
||||
}
|
||||
let bytes: Vec<u8> = samples
|
||||
.iter()
|
||||
.flat_map(|s| s.to_le_bytes())
|
||||
.collect();
|
||||
manager.lock().unwrap().push_to_output(
|
||||
params,
|
||||
&bytes,
|
||||
&mut vec![0u8; 256],
|
||||
)
|
||||
.expect("push succeeds even without an explicit device");
|
||||
let bytes: Vec<u8> = samples.iter().flat_map(|s| s.to_le_bytes()).collect();
|
||||
manager
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push_to_output(params, &bytes, &mut vec![0u8; 256])
|
||||
.expect("push succeeds even without an explicit device");
|
||||
|
||||
// Give the audio thread time to consume. PortAudio/CoreAudio
|
||||
// stream startup can take SECONDS in some environments (audio HAL
|
||||
@@ -614,7 +597,11 @@ mod tests {
|
||||
let target = frames as i64 - 1024;
|
||||
let mut consumed = 0i64;
|
||||
while std::time::Instant::now() < deadline {
|
||||
consumed = manager.lock().unwrap().output_buffer.output_frames_consumed();
|
||||
consumed = manager
|
||||
.lock()
|
||||
.unwrap()
|
||||
.output_buffer
|
||||
.output_frames_consumed();
|
||||
if consumed >= target {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -28,12 +28,9 @@
|
||||
use std::cell::RefCell;
|
||||
use std::sync::Arc;
|
||||
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use cpal::{
|
||||
BufferSize, Device, Host, SampleFormat, Stream, StreamConfig,
|
||||
SupportedBufferSize,
|
||||
};
|
||||
use crate::previewdevice::PreviewAudioDevice;
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use cpal::{BufferSize, Device, Host, SampleFormat, Stream, StreamConfig, SupportedBufferSize};
|
||||
|
||||
/// The default frames-per-buffer requested for the preview stream
|
||||
/// (clamped to the device's supported range).
|
||||
@@ -100,21 +97,19 @@ impl PortAudioOutput {
|
||||
}
|
||||
self.close();
|
||||
|
||||
if self.host.is_none(){
|
||||
if self.host.is_none() {
|
||||
self.host = Some(cpal::default_host())
|
||||
}
|
||||
|
||||
let host = self.host.as_ref().unwrap();
|
||||
|
||||
let output_device = resolve_device(&host, device)
|
||||
.ok_or_else(|| "no output device available".to_string())?;
|
||||
let output_device =
|
||||
resolve_device(host, device).ok_or_else(|| "no output device available".to_string())?;
|
||||
let config = pick_config(&output_device, rate, channels)?;
|
||||
if stream_dbg_enabled() {
|
||||
eprintln!(
|
||||
"[audio-stream] config: {} Hz, {} ch, buffer {:?}",
|
||||
u32::from(config.sample_rate),
|
||||
config.channels,
|
||||
config.buffer_size
|
||||
config.sample_rate, config.channels, config.buffer_size
|
||||
);
|
||||
}
|
||||
|
||||
@@ -217,8 +212,8 @@ fn pick_config(device: &Device, rate: i32, channels: i32) -> Result<StreamConfig
|
||||
let want_rate = rate as u32;
|
||||
let want_channels = channels as u16;
|
||||
|
||||
if let Ok(mut configs) = device.supported_output_configs() {
|
||||
while let Some(c) = configs.next() {
|
||||
if let Ok(configs) = device.supported_output_configs() {
|
||||
for c in configs {
|
||||
if c.sample_format() != SampleFormat::F32 || c.channels() != want_channels {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -89,12 +89,12 @@ impl PreviewAudioDevice {
|
||||
if copy_length > 0 {
|
||||
let new_bytes_read = inner.bytes_read + copy_length;
|
||||
|
||||
if inner.notify_interval > 0 && inner.notify_callback.is_some() {
|
||||
if (inner.bytes_read / inner.notify_interval)
|
||||
if inner.notify_interval > 0
|
||||
&& inner.notify_callback.is_some()
|
||||
&& (inner.bytes_read / inner.notify_interval)
|
||||
!= (new_bytes_read / inner.notify_interval)
|
||||
{
|
||||
notify = true;
|
||||
}
|
||||
{
|
||||
notify = true;
|
||||
}
|
||||
|
||||
inner.bytes_read = new_bytes_read;
|
||||
|
||||
@@ -135,7 +135,9 @@ fn fix_channel_layout(params: AudioParams) -> AudioParams {
|
||||
fn build_graph(from: &AudioParams, to: &AudioParams, speed: f64) -> Result<ffmpeg::filter::Graph> {
|
||||
let in_format = to_ffmpeg_sample_format(from.format);
|
||||
if in_format == Sample::None {
|
||||
return Err(Box::new(Error::Failed("invalid input sample format".to_string())));
|
||||
return Err(Box::new(Error::Failed(
|
||||
"invalid input sample format".to_string(),
|
||||
)));
|
||||
}
|
||||
|
||||
let abuffer = ffmpeg::filter::find("abuffer")
|
||||
@@ -257,10 +259,19 @@ impl Processor {
|
||||
/// Push planar float input and pull converted output. Returns the number
|
||||
/// of output frames written.
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// `in_planar` must be null or point to at least `from.channel_count()`
|
||||
/// readable `*const f32` plane pointers, each valid for
|
||||
/// `in_frame_count` samples; `out_planar` must be null or point to at
|
||||
/// least `to.channel_count()` writable `*mut f32` plane pointers, each
|
||||
/// valid for `out_capacity_frames` samples (NULL plane entries are
|
||||
/// skipped).
|
||||
///
|
||||
/// `// CPP-PARITY: src/audio/c_api/processor.cpp:91` (validation, state
|
||||
/// check, null `out_planar` short-circuit) and
|
||||
/// `src/audio/src/audioprocessor.cpp:141` (push/pull loop, byte counting).
|
||||
pub fn convert(
|
||||
pub unsafe fn convert(
|
||||
&self,
|
||||
in_planar: *const *const f32,
|
||||
in_frame_count: i32,
|
||||
@@ -273,7 +284,9 @@ impl Processor {
|
||||
if inner.graph.is_none() {
|
||||
return Err(Box::from(Error::State));
|
||||
}
|
||||
if in_frame_count < 0 || out_capacity_frames < 0 || (in_frame_count > 0 && in_planar.is_null())
|
||||
if in_frame_count < 0
|
||||
|| out_capacity_frames < 0
|
||||
|| (in_frame_count > 0 && in_planar.is_null())
|
||||
{
|
||||
return Err(Box::from(Error::Invalid));
|
||||
}
|
||||
@@ -294,7 +307,8 @@ impl Processor {
|
||||
let nb = in_frame_count as usize;
|
||||
let in_channels = from.channel_count().max(0) as usize;
|
||||
let layout = channel_layout_from_mask(from.channel_layout);
|
||||
let mut frame = ffmpeg::frame::Audio::new(to_ffmpeg_sample_format(from.format), nb, layout);
|
||||
let mut frame =
|
||||
ffmpeg::frame::Audio::new(to_ffmpeg_sample_format(from.format), nb, layout);
|
||||
frame.set_rate(from.sample_rate as u32);
|
||||
let planar = from.format.is_planar();
|
||||
// `plane_mut::<T>` requires the exact sample type of the frame
|
||||
@@ -317,7 +331,9 @@ impl Processor {
|
||||
// samples.
|
||||
let src = unsafe { *in_planar } as *const $t;
|
||||
let dst = frame.plane_mut::<$t>(0);
|
||||
unsafe { ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), nb * in_channels) };
|
||||
unsafe {
|
||||
ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), nb * in_channels)
|
||||
};
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
mod common;
|
||||
|
||||
use common::lock_manager;
|
||||
use oak_codec::encodingparams::EncodingParams;
|
||||
use oak_audio::error::{Error, OAKAUDIO_E_INVALID};
|
||||
use oak_audio::manager::instance;
|
||||
use oak_audio::params::{AudioParams, SampleFormat};
|
||||
use oak_codec::encodingparams::EncodingParams;
|
||||
|
||||
/// Audio params for the tests: stereo f32 at 48 kHz.
|
||||
fn stereo() -> AudioParams {
|
||||
@@ -97,11 +97,13 @@ fn push_output_starts_clock() {
|
||||
// With no explicit device the push still succeeds (M12 P1).
|
||||
m.set_output_device(-1).unwrap();
|
||||
let samples = vec![0u8; 480 * 2 * 4];
|
||||
m.push_to_output(stereo(), &samples, &mut vec![0u8; 64]).unwrap();
|
||||
m.push_to_output(stereo(), &samples, &mut [0u8; 64])
|
||||
.unwrap();
|
||||
|
||||
// After selecting a device the push succeeds and the clock starts at 0.
|
||||
m.set_output_device(0).unwrap();
|
||||
m.push_to_output(stereo(), &samples, &mut vec![0u8; 64]).unwrap();
|
||||
m.push_to_output(stereo(), &samples, &mut [0u8; 64])
|
||||
.unwrap();
|
||||
m.seconds(&mut secs).unwrap();
|
||||
assert!(secs >= 0.0, "clock started (got {secs})");
|
||||
}
|
||||
@@ -142,7 +144,8 @@ fn output_control_flags() {
|
||||
m.set_output_notify_interval(1024).unwrap();
|
||||
let err = m.set_output_notify_interval(-1).unwrap_err();
|
||||
assert_eq!(
|
||||
err.downcast_ref::<oak_audio::error::Error>().map(|e| e.code()),
|
||||
err.downcast_ref::<oak_audio::error::Error>()
|
||||
.map(|e| e.code()),
|
||||
Some(OAKAUDIO_E_INVALID)
|
||||
);
|
||||
m.clear_buffered_output().unwrap();
|
||||
@@ -151,7 +154,8 @@ fn output_control_flags() {
|
||||
// Push starts the stream, then stop_output halts it (clock -> -1).
|
||||
m.set_output_device(0).unwrap();
|
||||
let samples = vec![0u8; 480 * 2 * 4];
|
||||
m.push_to_output(stereo(), &samples, &mut vec![0u8; 64]).unwrap();
|
||||
m.push_to_output(stereo(), &samples, &mut [0u8; 64])
|
||||
.unwrap();
|
||||
m.stop_output().unwrap();
|
||||
let mut secs = 1.0f64;
|
||||
m.seconds(&mut secs).unwrap();
|
||||
@@ -171,7 +175,9 @@ fn recording_start_stop() {
|
||||
|
||||
// No input device -> explainable failure, no crash.
|
||||
m.set_input_device(-1).unwrap();
|
||||
assert!(m.start_recording(&wav_params("unused.wav"), &mut vec![0u8; 64]).is_err());
|
||||
assert!(m
|
||||
.start_recording(&wav_params("unused.wav"), &mut [0u8; 64])
|
||||
.is_err());
|
||||
|
||||
m.set_input_device(0).unwrap();
|
||||
let path = std::env::temp_dir().join(format!("oakaudio_rec_{}.wav", std::process::id()));
|
||||
@@ -193,8 +199,14 @@ fn recording_start_stop() {
|
||||
#[test]
|
||||
fn config_defaults() {
|
||||
assert_eq!(oak_audio::config::output_buffer_size(), 0);
|
||||
assert!(oak_audio::config::device_name(true).is_err(), "no configured output device");
|
||||
assert!(oak_audio::config::device_name(false).is_err(), "no configured input device");
|
||||
assert!(
|
||||
oak_audio::config::device_name(true).is_err(),
|
||||
"no configured output device"
|
||||
);
|
||||
assert!(
|
||||
oak_audio::config::device_name(false).is_err(),
|
||||
"no configured input device"
|
||||
);
|
||||
}
|
||||
|
||||
/// PreviewAudioDevice pull-side plumbing (read/notify callback/clock).
|
||||
@@ -262,7 +274,7 @@ fn output_levels_reports_buffered_peaks() {
|
||||
packed.extend_from_slice(&(0.05f32 * t).to_le_bytes());
|
||||
packed.extend_from_slice(&(0.2f32 * t).to_le_bytes());
|
||||
}
|
||||
m.push_to_output(stereo(), &packed, &mut vec![0u8; 64]).unwrap();
|
||||
m.push_to_output(stereo(), &packed, &mut [0u8; 64]).unwrap();
|
||||
|
||||
let mut peaks = [0.0f32; 4];
|
||||
let n = m.output_levels(&mut peaks).unwrap();
|
||||
@@ -288,7 +300,10 @@ fn output_levels_reports_buffered_peaks() {
|
||||
|
||||
// The error mapping is intact.
|
||||
assert_eq!(Error::Invalid.code(), OAKAUDIO_E_INVALID);
|
||||
assert_eq!(Error::Failed("x".to_string()).code(), oak_audio::error::OAKAUDIO_E_FAILED);
|
||||
assert_eq!(
|
||||
Error::Failed("x".to_string()).code(),
|
||||
oak_audio::error::OAKAUDIO_E_FAILED
|
||||
);
|
||||
|
||||
// Leave the singleton as we found it: the push flipped
|
||||
// `output_started`, which other tests' seconds() assertions depend on.
|
||||
|
||||
@@ -54,6 +54,19 @@ fn plane_mut_ptrs(planes: &mut [Vec<f32>]) -> Vec<*mut f32> {
|
||||
planes.iter_mut().map(|p| p.as_mut_ptr()).collect()
|
||||
}
|
||||
|
||||
/// [`Processor::convert`] with its raw-pointer contract upheld by the
|
||||
/// plane helpers above (the pointer arrays outlive the call). The single
|
||||
/// `unsafe` block keeps the call sites readable.
|
||||
fn convert(
|
||||
p: &Processor,
|
||||
in_ptrs: *const *const f32,
|
||||
in_frames: i32,
|
||||
out_ptrs: *const *mut f32,
|
||||
out_capacity: i32,
|
||||
) -> oak_audio::error::Result<i32> {
|
||||
unsafe { p.convert(in_ptrs, in_frames, out_ptrs, out_capacity) }
|
||||
}
|
||||
|
||||
/// The crate error code of a failed call (the API surfaces
|
||||
/// `Box<dyn std::error::Error>`).
|
||||
fn code(err: Box<dyn std::error::Error>) -> i32 {
|
||||
@@ -67,11 +80,11 @@ fn code(err: Box<dyn std::error::Error>) -> i32 {
|
||||
#[test]
|
||||
fn processor_open_isopen_close() {
|
||||
let p = Processor::init();
|
||||
assert_eq!(p.is_open().unwrap(), false);
|
||||
assert!(!p.is_open().unwrap());
|
||||
open_identity(&p).unwrap();
|
||||
assert_eq!(p.is_open().unwrap(), true);
|
||||
assert!(p.is_open().unwrap());
|
||||
p.close().unwrap();
|
||||
assert_eq!(p.is_open().unwrap(), false);
|
||||
assert!(!p.is_open().unwrap());
|
||||
}
|
||||
|
||||
/// open with matching in/out rate and format is an identity passthrough:
|
||||
@@ -86,7 +99,7 @@ fn identity_convert_passthrough() {
|
||||
let mut out = vec![vec![0f32; 32]; 2];
|
||||
let out_ptrs = plane_mut_ptrs(&mut out);
|
||||
|
||||
let n = p.convert(in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 32).unwrap();
|
||||
let n = convert(&p, in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 32).unwrap();
|
||||
assert_eq!(n, 32);
|
||||
for ch in 0..2 {
|
||||
for i in 0..32 {
|
||||
@@ -112,7 +125,7 @@ fn convert_capacity_truncation() {
|
||||
let mut out = vec![vec![9.9f32; 10]; 2];
|
||||
let out_ptrs = plane_mut_ptrs(&mut out);
|
||||
|
||||
let n = p.convert(in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 10).unwrap();
|
||||
let n = convert(&p, in_ptrs.as_ptr(), 32, out_ptrs.as_ptr(), 10).unwrap();
|
||||
assert_eq!(n, 10);
|
||||
for ch in 0..2 {
|
||||
for i in 0..10 {
|
||||
@@ -123,7 +136,7 @@ fn convert_capacity_truncation() {
|
||||
// The graph has already drained; nothing further to pull.
|
||||
let mut out2 = vec![vec![0f32; 32]; 2];
|
||||
let out2_ptrs = plane_mut_ptrs(&mut out2);
|
||||
let n = p.convert(in_ptrs.as_ptr(), 0, out2_ptrs.as_ptr(), 32).unwrap();
|
||||
let n = convert(&p, in_ptrs.as_ptr(), 0, out2_ptrs.as_ptr(), 32).unwrap();
|
||||
assert_eq!(n, 0);
|
||||
}
|
||||
|
||||
@@ -136,7 +149,7 @@ fn open_invalid_params() {
|
||||
code(p.open(params(0), params(48000), 1.0).unwrap_err()),
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
assert_eq!(p.is_open().unwrap(), false);
|
||||
assert!(!p.is_open().unwrap());
|
||||
// The output format is forced to planar f32 by the processor contract.
|
||||
let mut wrong_out = params(48000);
|
||||
wrong_out.format = SampleFormat::F32;
|
||||
@@ -144,12 +157,12 @@ fn open_invalid_params() {
|
||||
code(p.open(params(48000), wrong_out, 1.0).unwrap_err()),
|
||||
OAKAUDIO_E_INVALID
|
||||
);
|
||||
assert_eq!(p.is_open().unwrap(), false);
|
||||
assert!(!p.is_open().unwrap());
|
||||
|
||||
// convert before open is a state error; a non-positive speed is
|
||||
// rejected on open.
|
||||
assert_eq!(
|
||||
code(p.convert(std::ptr::null(), 0, std::ptr::null(), 0).unwrap_err()),
|
||||
code(convert(&p, std::ptr::null(), 0, std::ptr::null(), 0).unwrap_err()),
|
||||
OAKAUDIO_E_STATE
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -173,13 +186,18 @@ fn resample_and_flush() {
|
||||
let mut out = vec![vec![0f32; frames]; 2];
|
||||
let out_ptrs = plane_mut_ptrs(&mut out);
|
||||
|
||||
let mut total = p
|
||||
.convert(in_ptrs.as_ptr(), frames as i32, out_ptrs.as_ptr(), frames as i32)
|
||||
.unwrap();
|
||||
let mut total = convert(
|
||||
&p,
|
||||
in_ptrs.as_ptr(),
|
||||
frames as i32,
|
||||
out_ptrs.as_ptr(),
|
||||
frames as i32,
|
||||
)
|
||||
.unwrap();
|
||||
p.flush().unwrap();
|
||||
// Drain the resampler delay after end-of-input.
|
||||
while total < frames as i32 {
|
||||
let n = p.convert(std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32).unwrap();
|
||||
let n = convert(&p, std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32).unwrap();
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
@@ -189,7 +207,7 @@ fn resample_and_flush() {
|
||||
(total - 22050).abs() <= 2,
|
||||
"half-rate output must halve the frame count (got {total})"
|
||||
);
|
||||
assert_eq!(p.is_open().unwrap(), true);
|
||||
assert!(p.is_open().unwrap());
|
||||
}
|
||||
|
||||
/// A tempo factor != 1.0 time-stretches: tempo 2.0 halves the frame count
|
||||
@@ -207,12 +225,17 @@ fn tempo_stretch() {
|
||||
let mut out = vec![vec![0f32; frames]; 2];
|
||||
let out_ptrs = plane_mut_ptrs(&mut out);
|
||||
|
||||
let mut total = p
|
||||
.convert(in_ptrs.as_ptr(), frames as i32, out_ptrs.as_ptr(), frames as i32)
|
||||
.unwrap();
|
||||
let mut total = convert(
|
||||
&p,
|
||||
in_ptrs.as_ptr(),
|
||||
frames as i32,
|
||||
out_ptrs.as_ptr(),
|
||||
frames as i32,
|
||||
)
|
||||
.unwrap();
|
||||
p.flush().unwrap();
|
||||
while total < frames as i32 {
|
||||
let n = p.convert(std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32).unwrap();
|
||||
let n = convert(&p, std::ptr::null(), 0, out_ptrs.as_ptr(), frames as i32).unwrap();
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
@@ -222,7 +245,7 @@ fn tempo_stretch() {
|
||||
(total - 24000).abs() <= 2400,
|
||||
"tempo 2.0 must halve the frame count (got {total})"
|
||||
);
|
||||
assert_eq!(p.is_open().unwrap(), true);
|
||||
assert!(p.is_open().unwrap());
|
||||
p.close().unwrap();
|
||||
|
||||
// Re-opening a closed processor works; opening an open one is a state
|
||||
|
||||
@@ -250,7 +250,7 @@ impl CodecStream {
|
||||
|
||||
/// Associated timeline block (borrowed; only compared, never used).
|
||||
pub fn block(&self) -> Option<OakNodeBlock> {
|
||||
self.block.clone()
|
||||
self.block
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -558,7 +558,7 @@ impl<'a> Cursor<'a> {
|
||||
}
|
||||
|
||||
fn skip_ws(&mut self) {
|
||||
while self.peek().map_or(false, char::is_whitespace) {
|
||||
while self.peek().is_some_and(char::is_whitespace) {
|
||||
self.i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1178,12 +1178,12 @@ impl DecoderState {
|
||||
ctx.run(&f, &mut out).map_err(ffmpeg_err)?;
|
||||
let bytes = if f32_ok {
|
||||
let stride = out.stride(0);
|
||||
convert_rgba_f32_le(&out.data(0), w, h, stride)
|
||||
convert_rgba_f32_le(out.data(0), w, h, stride)
|
||||
} else if out_fmt == Pixel::YUV444P16LE {
|
||||
convert_yuv444p16_to_rgba_f32(&out, w, h, yuv_matrix_for(raw_space, src_w, src_h), full_range)
|
||||
} else {
|
||||
let stride = out.stride(0);
|
||||
convert_rgba8_to_f32(&out.data(0), w, h, stride)
|
||||
convert_rgba8_to_f32(out.data(0), w, h, stride)
|
||||
};
|
||||
let meta = crate::decoder::DecodedColorMeta {
|
||||
color_primaries: raw_primaries,
|
||||
@@ -2084,9 +2084,9 @@ fn probe_file(filename: &str, cancelled: Option<&CancelAtom>) -> Option<FootageD
|
||||
vp.set_channel_count(VIDEO_CHANNELS);
|
||||
vp.set_interlacing(Interlacing::None);
|
||||
vp.set_pixel_aspect_ratio(1, 1);
|
||||
vp.set_frame_rate(frame_rate.0 as i32, frame_rate.1 as i32);
|
||||
vp.set_frame_rate(frame_rate.0, frame_rate.1);
|
||||
vp.set_start_time(stream.start_time());
|
||||
vp.set_time_base(tb.0 as i32, tb.1 as i32);
|
||||
vp.set_time_base(tb.0, tb.1);
|
||||
vp.set_duration(stream.duration());
|
||||
vp.set_premultiplied_alpha(false);
|
||||
// Stream colorimetry (drives the input→working transform
|
||||
@@ -2134,7 +2134,7 @@ fn probe_file(filename: &str, cancelled: Option<&CancelAtom>) -> Option<FootageD
|
||||
format: 0,
|
||||
stream_index: i as i32,
|
||||
duration: stream_duration,
|
||||
time_base: (tb.0 as i32, tb.1 as i32),
|
||||
time_base: (tb.0, tb.1),
|
||||
}));
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -243,7 +243,7 @@ mod tests {
|
||||
assert!(!d.is_open());
|
||||
|
||||
// Already open -> refuse.
|
||||
assert!(d.open(&[ok.clone()], OpenMode::WriteOnly));
|
||||
assert!(d.open(std::slice::from_ref(&ok), OpenMode::WriteOnly));
|
||||
assert!(!d.open(&[ok], OpenMode::WriteOnly));
|
||||
}
|
||||
|
||||
|
||||
@@ -317,11 +317,10 @@ impl ProxyManager {
|
||||
/// Locate an ffmpeg executable (empty string when none found).
|
||||
pub fn find_ffmpeg(configured_path: &str) -> String {
|
||||
// An explicitly configured path takes precedence if it is usable.
|
||||
if !configured_path.is_empty() {
|
||||
if is_executable_file(Path::new(configured_path)) {
|
||||
if !configured_path.is_empty()
|
||||
&& is_executable_file(Path::new(configured_path)) {
|
||||
return absolute(configured_path);
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to searching the system PATH (split per platform:
|
||||
// `;` on Windows, `:` elsewhere — see [`split_path_env`]).
|
||||
|
||||
@@ -335,8 +335,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn task_kind_values_match_abi() {
|
||||
assert_eq!(TaskKind::Conform as i32, OAKCODEC_TASK_CONFORM as i32);
|
||||
assert_eq!(TaskKind::Proxy as i32, OAKCODEC_TASK_PROXY as i32);
|
||||
assert_eq!(TaskKind::Conform as i32, OAKCODEC_TASK_CONFORM);
|
||||
assert_eq!(TaskKind::Proxy as i32, OAKCODEC_TASK_PROXY);
|
||||
}
|
||||
|
||||
// ABI constants mirrored from include/codec/task.h.
|
||||
|
||||
@@ -66,7 +66,7 @@ fn frame_rate(timebase: &Rational) -> f64 {
|
||||
/// `drop_frame` is true when the (already trimmed) string contains a `;`.
|
||||
/// Returns `None` on any parse failure.
|
||||
fn timecode_to_time(timecode: &str, timebase: &Rational, drop_frame: bool) -> Option<Rational> {
|
||||
let mut tokens: Vec<&str> = timecode.split(|c| c == ':' || c == ';').collect();
|
||||
let mut tokens: Vec<&str> = timecode.split([':', ';']).collect();
|
||||
let element_count = 4;
|
||||
|
||||
// Keep only the leading `HH:MM:SS:FF` tokens.
|
||||
|
||||
+162
-95
@@ -40,9 +40,9 @@ use std::sync::{Arc, Mutex, MutexGuard};
|
||||
|
||||
use crate::PixelFormat;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::frame::VideoParamsPod;
|
||||
use crate::texture::{Frame, Texture};
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
/// Backend selection preference (mapped onto wgpu backends).
|
||||
///
|
||||
@@ -183,9 +183,8 @@ impl DisplayBitDepth {
|
||||
/// Read the user's persisted choice through the oak_core config C ABI
|
||||
/// ("DisplayBitDepth").
|
||||
pub fn from_user_config() -> DisplayBitDepth {
|
||||
let configured =
|
||||
crate::commonutil::config_get_string(None, CONFIG_KEY_DISPLAY_BIT_DEPTH)
|
||||
.unwrap_or_default();
|
||||
let configured = crate::commonutil::config_get_string(None, CONFIG_KEY_DISPLAY_BIT_DEPTH)
|
||||
.unwrap_or_default();
|
||||
DisplayBitDepth::from_config_string(&configured)
|
||||
}
|
||||
|
||||
@@ -243,10 +242,10 @@ pub trait GpuContextLike: Send + Sync {
|
||||
fn download(&self, token: u64) -> Result<Frame>;
|
||||
/// Blit texture → texture (plain copy; color-managed deferred).
|
||||
fn blit(
|
||||
&self,
|
||||
src: u64,
|
||||
dst: u64,
|
||||
processor: Option<&crate::color::ColorProcessor>,
|
||||
&self,
|
||||
src: u64,
|
||||
dst: u64,
|
||||
processor: Option<&crate::color::ColorProcessor>,
|
||||
) -> Result<()>;
|
||||
|
||||
/// Concrete-context downcast hook (M2). The present path uses it to
|
||||
@@ -432,7 +431,9 @@ impl GpuContext {
|
||||
// (widely available on desktop GPUs); without it effect
|
||||
// shaders sample nearest — a quality degradation, not a
|
||||
// failure (logged once by the shaderfx runner).
|
||||
let filterable = adapter.features().contains(wgpu::Features::FLOAT32_FILTERABLE);
|
||||
let filterable = adapter
|
||||
.features()
|
||||
.contains(wgpu::Features::FLOAT32_FILTERABLE);
|
||||
let mut required_features = wgpu::Features::empty();
|
||||
if filterable {
|
||||
required_features |= wgpu::Features::FLOAT32_FILTERABLE;
|
||||
@@ -789,12 +790,7 @@ impl GpuContext {
|
||||
/// nodes (`ColorTransformJob`, M2). The LUT texture is uploaded once
|
||||
/// per key (the caller passes a stable processor cache id); applying
|
||||
/// it is GPU→GPU.
|
||||
pub fn apply_color_lut(
|
||||
&self,
|
||||
src: u64,
|
||||
key: &str,
|
||||
lut: &crate::lut::Lut3d,
|
||||
) -> Result<u64> {
|
||||
pub fn apply_color_lut(&self, src: u64, key: &str, lut: &crate::lut::Lut3d) -> Result<u64> {
|
||||
let token = {
|
||||
let mut cache = lock(&self.color_luts);
|
||||
if let Some((_, token)) = cache.iter().find(|(k, _)| k == key) {
|
||||
@@ -873,7 +869,8 @@ impl GpuContext {
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
self.queue.write_buffer(&uniform, 0, &f32_uniform_bytes(¶ms));
|
||||
self.queue
|
||||
.write_buffer(&uniform, 0, &f32_uniform_bytes(¶ms));
|
||||
let src_view = src_tex
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
@@ -1023,12 +1020,11 @@ impl GpuContext {
|
||||
cache: None,
|
||||
});
|
||||
if let Some(err) = pollster_block_on(scope.pop()) {
|
||||
return Err(Error::Failed(format!("present pipeline validation failed: {err}")));
|
||||
return Err(Error::Failed(format!(
|
||||
"present pipeline validation failed: {err}"
|
||||
)));
|
||||
}
|
||||
let program = PresentPipeline {
|
||||
pipeline,
|
||||
layout,
|
||||
};
|
||||
let program = PresentPipeline { pipeline, layout };
|
||||
cache.push((format, program.clone()));
|
||||
Ok(program)
|
||||
}
|
||||
@@ -1090,7 +1086,8 @@ impl GpuContext {
|
||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||
mapped_at_creation: false,
|
||||
});
|
||||
self.queue.write_buffer(&uniform, 0, &f32_uniform_bytes(¶ms));
|
||||
self.queue
|
||||
.write_buffer(&uniform, 0, &f32_uniform_bytes(¶ms));
|
||||
let y_view = y_tex
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
@@ -1239,12 +1236,11 @@ impl GpuContext {
|
||||
cache: None,
|
||||
});
|
||||
if let Some(err) = pollster_block_on(scope.pop()) {
|
||||
return Err(Error::Failed(format!("YUV pipeline validation failed: {err}")));
|
||||
return Err(Error::Failed(format!(
|
||||
"YUV pipeline validation failed: {err}"
|
||||
)));
|
||||
}
|
||||
let program = PresentPipeline {
|
||||
pipeline,
|
||||
layout,
|
||||
};
|
||||
let program = PresentPipeline { pipeline, layout };
|
||||
*cache = Some(program.clone());
|
||||
Ok(program)
|
||||
}
|
||||
@@ -1438,10 +1434,10 @@ impl GpuContext {
|
||||
/// of this pass (see README §4), so a `Some` processor returns
|
||||
/// `Error::Failed` and the plain-copy WGSL pipeline is used for `None`.
|
||||
pub fn blit(
|
||||
&self,
|
||||
src: u64,
|
||||
dst: u64,
|
||||
processor: Option<&crate::color::ColorProcessor>,
|
||||
&self,
|
||||
src: u64,
|
||||
dst: u64,
|
||||
processor: Option<&crate::color::ColorProcessor>,
|
||||
) -> Result<()> {
|
||||
if processor.is_some() {
|
||||
return Err(Error::Failed(
|
||||
@@ -1767,7 +1763,9 @@ impl GpuContext {
|
||||
cache: None,
|
||||
});
|
||||
if let Some(err) = pollster_block_on(scope.pop()) {
|
||||
return Err(Error::Failed(format!("effect pipeline validation failed: {err}")));
|
||||
return Err(Error::Failed(format!(
|
||||
"effect pipeline validation failed: {err}"
|
||||
)));
|
||||
}
|
||||
|
||||
let program = Arc::new(ShaderProgram {
|
||||
@@ -2121,7 +2119,6 @@ fn vs_main(@builtin(vertex_index) vi: u32) -> VsOut {
|
||||
}
|
||||
"#;
|
||||
|
||||
|
||||
impl GpuContextLike for GpuContext {
|
||||
fn kind(&self) -> BackendKind {
|
||||
self.kind()
|
||||
@@ -2140,10 +2137,10 @@ impl GpuContextLike for GpuContext {
|
||||
}
|
||||
|
||||
fn blit(
|
||||
&self,
|
||||
src: u64,
|
||||
dst: u64,
|
||||
processor: Option<&crate::color::ColorProcessor>,
|
||||
&self,
|
||||
src: u64,
|
||||
dst: u64,
|
||||
processor: Option<&crate::color::ColorProcessor>,
|
||||
) -> Result<()> {
|
||||
self.blit(src, dst, processor)
|
||||
}
|
||||
@@ -2197,11 +2194,11 @@ fn pollster_block_on<F: std::future::Future>(future: F) -> F::Output {
|
||||
// Minimal futures executor (wgpu brings futures-core transitively; a tiny
|
||||
// block_on is enough for the immediately-ready adapter/device futures).
|
||||
mod futures_executor {
|
||||
use std::future::Future;
|
||||
use std::pin::pin;
|
||||
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
|
||||
use std::future::Future;
|
||||
use std::pin::pin;
|
||||
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
|
||||
|
||||
fn noop_raw_waker() -> RawWaker {
|
||||
fn noop_raw_waker() -> RawWaker {
|
||||
fn no_op(_: *const ()) {}
|
||||
fn clone(_: *const ()) -> RawWaker {
|
||||
noop_raw_waker()
|
||||
@@ -2353,7 +2350,13 @@ impl DisplayRenderer {
|
||||
}
|
||||
|
||||
/// Upload pixels into a texture (GPU: backend upload; CPU: buffer copy).
|
||||
pub fn upload_texture(
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// `pixels` must point to at least `linesize * height` readable bytes
|
||||
/// of the texture's pixel format, and `linesize` must equal the
|
||||
/// frame's line size.
|
||||
pub unsafe fn upload_texture(
|
||||
&self,
|
||||
texture: &mut Texture,
|
||||
pixels: *const u8,
|
||||
@@ -2362,7 +2365,7 @@ impl DisplayRenderer {
|
||||
let size = texture.size();
|
||||
match texture {
|
||||
Texture::Gpu { token, ctx, .. } => {
|
||||
let frame = frame_from_pixels_for_upload(size, pixels, linesize)?;
|
||||
let frame = unsafe { frame_from_pixels_for_upload(size, pixels, linesize) }?;
|
||||
ctx.upload(*token, &frame)
|
||||
}
|
||||
Texture::Cpu(frame) => {
|
||||
@@ -2380,7 +2383,17 @@ impl DisplayRenderer {
|
||||
}
|
||||
|
||||
/// Download a texture's pixels into `dst` (with `linesize` stride).
|
||||
pub fn download_texture(&self, texture: &Texture, dst: *mut u8, linesize: usize) -> Result<()> {
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// `dst` must point to at least `linesize * height` writable bytes,
|
||||
/// and `linesize` must equal the frame's line size.
|
||||
pub unsafe fn download_texture(
|
||||
&self,
|
||||
texture: &Texture,
|
||||
dst: *mut u8,
|
||||
linesize: usize,
|
||||
) -> Result<()> {
|
||||
let frame = texture.to_frame()?;
|
||||
let stride = frame.linesize_bytes();
|
||||
if linesize != stride {
|
||||
@@ -2397,10 +2410,10 @@ impl DisplayRenderer {
|
||||
/// GPU path: plain-copy WGSL blit; a color processor on the GPU path is
|
||||
/// deferred (`Error::Failed`, see [`GpuContext::blit`]).
|
||||
pub fn blit_color_managed(
|
||||
&self,
|
||||
src: Option<&Texture>,
|
||||
dst: &mut Texture,
|
||||
processor: Option<&crate::color::ColorProcessor>,
|
||||
&self,
|
||||
src: Option<&Texture>,
|
||||
dst: &mut Texture,
|
||||
processor: Option<&crate::color::ColorProcessor>,
|
||||
) -> Result<()> {
|
||||
match (src, dst) {
|
||||
(
|
||||
@@ -2430,7 +2443,12 @@ impl DisplayRenderer {
|
||||
|
||||
/// Cross-backend texture download by id (GPU registry only; CPU
|
||||
/// textures have no id registry — documented).
|
||||
pub fn download_from_texture(
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// `dst` must point to at least `linesize * height` writable bytes,
|
||||
/// and `linesize` must equal the F32 line size.
|
||||
pub unsafe fn download_from_texture(
|
||||
&self,
|
||||
texture_id: i32,
|
||||
params: &VideoParamsPod,
|
||||
@@ -2464,7 +2482,12 @@ pub fn texture_id_of(t: &Texture) -> i32 {
|
||||
}
|
||||
|
||||
/// Build an F32 frame from raw pixels (for GPU upload).
|
||||
pub fn frame_from_pixels_for_upload(
|
||||
///
|
||||
/// # Safety
|
||||
///
|
||||
/// `pixels` must point to `linesize * height` readable bytes of F32 RGBA
|
||||
/// data (a null pointer is rejected with `Error::Invalid`).
|
||||
pub unsafe fn frame_from_pixels_for_upload(
|
||||
size: (i32, i32),
|
||||
pixels: *const u8,
|
||||
linesize: usize,
|
||||
@@ -2489,14 +2512,14 @@ pub fn frame_from_pixels_for_upload(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::*;
|
||||
|
||||
/// Serializes the tests that assert on the process-global GPU
|
||||
/// transfer counters (and install a display LUT): parallel tests would
|
||||
/// otherwise see each other's transfers.
|
||||
static GPU_COUNTER_LOCK: Mutex<()> = Mutex::new(());
|
||||
/// Serializes the tests that assert on the process-global GPU
|
||||
/// transfer counters (and install a display LUT): parallel tests would
|
||||
/// otherwise see each other's transfers.
|
||||
static GPU_COUNTER_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
#[test]
|
||||
#[test]
|
||||
fn backend_string_roundtrip() {
|
||||
for (s, kind) in [
|
||||
("auto", BackendKind::Auto),
|
||||
@@ -2534,13 +2557,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn display_bit_depth_string_roundtrip() {
|
||||
assert_eq!(DisplayBitDepth::from_config_string("8"), DisplayBitDepth::Bit8);
|
||||
assert_eq!(
|
||||
DisplayBitDepth::from_config_string("8"),
|
||||
DisplayBitDepth::Bit8
|
||||
);
|
||||
assert_eq!(
|
||||
DisplayBitDepth::from_config_string("10"),
|
||||
DisplayBitDepth::Bit10
|
||||
);
|
||||
// Unknown / missing → the 10-bit default.
|
||||
assert_eq!(DisplayBitDepth::from_config_string(""), DisplayBitDepth::Bit10);
|
||||
assert_eq!(
|
||||
DisplayBitDepth::from_config_string(""),
|
||||
DisplayBitDepth::Bit10
|
||||
);
|
||||
assert_eq!(
|
||||
DisplayBitDepth::from_config_string("bogus"),
|
||||
DisplayBitDepth::Bit10
|
||||
@@ -2560,12 +2589,18 @@ mod tests {
|
||||
// 10-bit → the 10-bit (RGB10A2) swapchain format, HDR float fallback.
|
||||
assert_eq!(
|
||||
DisplayBitDepth::Bit10.present_formats(),
|
||||
&[wgpu::TextureFormat::Rgb10a2Unorm, wgpu::TextureFormat::Rgba16Float]
|
||||
&[
|
||||
wgpu::TextureFormat::Rgb10a2Unorm,
|
||||
wgpu::TextureFormat::Rgba16Float
|
||||
]
|
||||
);
|
||||
// 8-bit → the engine's current default preference list.
|
||||
assert_eq!(
|
||||
DisplayBitDepth::Bit8.present_formats(),
|
||||
&[wgpu::TextureFormat::Bgra8Unorm, wgpu::TextureFormat::Rgba8Unorm]
|
||||
&[
|
||||
wgpu::TextureFormat::Bgra8Unorm,
|
||||
wgpu::TextureFormat::Rgba8Unorm
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2590,8 +2625,7 @@ mod tests {
|
||||
assert!(!GpuContext::shared().unwrap().is_adopted());
|
||||
|
||||
let (device, queue) = base.device_queue();
|
||||
let adopted =
|
||||
GpuContext::adopt(device, queue, BackendKind::Auto);
|
||||
let adopted = GpuContext::adopt(device, queue, BackendKind::Auto);
|
||||
assert!(
|
||||
GpuContext::install_shared(Some(adopted.clone())),
|
||||
"an unused engine context is replaceable by the UI device"
|
||||
@@ -2605,8 +2639,7 @@ mod tests {
|
||||
// pulled out from under in-flight work.
|
||||
let _texture = adopted.create_texture(2, 2).unwrap();
|
||||
let (device, queue) = adopted.device_queue();
|
||||
let second =
|
||||
GpuContext::adopt(device, queue, BackendKind::Auto);
|
||||
let second = GpuContext::adopt(device, queue, BackendKind::Auto);
|
||||
assert!(
|
||||
!GpuContext::install_shared(Some(second)),
|
||||
"a used context is not replaceable"
|
||||
@@ -2728,8 +2761,7 @@ mod tests {
|
||||
for i in 0..(w * h) as usize {
|
||||
let px = sample(i);
|
||||
for (c, v) in px.iter().enumerate() {
|
||||
frame.data[i * 16 + c * 4..i * 16 + c * 4 + 4]
|
||||
.copy_from_slice(&v.to_le_bytes());
|
||||
frame.data[i * 16 + c * 4..i * 16 + c * 4 + 4].copy_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
}
|
||||
ctx.upload(src, &frame).unwrap();
|
||||
@@ -2859,10 +2891,10 @@ mod tests {
|
||||
|
||||
let mut expected = vec![0.0f32; w * h * 4];
|
||||
let mut compare = |ctx: &GpuContext,
|
||||
tag: &str,
|
||||
matrix: crate::colormath::YuvMatrix,
|
||||
transform: YuvTransform,
|
||||
full_range: bool| {
|
||||
tag: &str,
|
||||
matrix: crate::colormath::YuvMatrix,
|
||||
transform: YuvTransform,
|
||||
full_range: bool| {
|
||||
crate::colormath::yuv444p16_to_rgb_f32(
|
||||
&y_code_plane,
|
||||
w * 2,
|
||||
@@ -2893,12 +2925,48 @@ mod tests {
|
||||
}
|
||||
};
|
||||
use crate::colormath::YuvMatrix;
|
||||
compare(&ctx, "bt601 limited", YuvMatrix::Bt601, YuvTransform::bt601_limited(), false);
|
||||
compare(&ctx, "bt601 full", YuvMatrix::Bt601, YuvTransform::bt601_full(), true);
|
||||
compare(&ctx, "bt709 limited", YuvMatrix::Bt709, YuvTransform::bt709_limited(), false);
|
||||
compare(&ctx, "bt709 full", YuvMatrix::Bt709, YuvTransform::bt709_full(), true);
|
||||
compare(&ctx, "bt2020 limited", YuvMatrix::Bt2020, YuvTransform::bt2020_limited(), false);
|
||||
compare(&ctx, "bt2020 full", YuvMatrix::Bt2020, YuvTransform::bt2020_full(), true);
|
||||
compare(
|
||||
&ctx,
|
||||
"bt601 limited",
|
||||
YuvMatrix::Bt601,
|
||||
YuvTransform::bt601_limited(),
|
||||
false,
|
||||
);
|
||||
compare(
|
||||
&ctx,
|
||||
"bt601 full",
|
||||
YuvMatrix::Bt601,
|
||||
YuvTransform::bt601_full(),
|
||||
true,
|
||||
);
|
||||
compare(
|
||||
&ctx,
|
||||
"bt709 limited",
|
||||
YuvMatrix::Bt709,
|
||||
YuvTransform::bt709_limited(),
|
||||
false,
|
||||
);
|
||||
compare(
|
||||
&ctx,
|
||||
"bt709 full",
|
||||
YuvMatrix::Bt709,
|
||||
YuvTransform::bt709_full(),
|
||||
true,
|
||||
);
|
||||
compare(
|
||||
&ctx,
|
||||
"bt2020 limited",
|
||||
YuvMatrix::Bt2020,
|
||||
YuvTransform::bt2020_limited(),
|
||||
false,
|
||||
);
|
||||
compare(
|
||||
&ctx,
|
||||
"bt2020 full",
|
||||
YuvMatrix::Bt2020,
|
||||
YuvTransform::bt2020_full(),
|
||||
true,
|
||||
);
|
||||
|
||||
ctx.destroy_texture(y);
|
||||
ctx.destroy_texture(u);
|
||||
@@ -2914,9 +2982,7 @@ mod tests {
|
||||
let Some(ctx) = any_gpu() else {
|
||||
return;
|
||||
};
|
||||
let lut = crate::lut::Lut3d::build(9, [0.0; 3], [1.0; 3], |c| {
|
||||
[c[1], c[2], c[0]]
|
||||
});
|
||||
let lut = crate::lut::Lut3d::build(9, [0.0; 3], [1.0; 3], |c| [c[1], c[2], c[0]]);
|
||||
let (w, h) = (4, 2);
|
||||
let src = ctx.create_texture(w, h).unwrap();
|
||||
let mut frame = Frame::new();
|
||||
@@ -2933,8 +2999,7 @@ mod tests {
|
||||
1.0,
|
||||
];
|
||||
for (c, v) in px.iter().enumerate() {
|
||||
frame.data[i * 16 + c * 4..i * 16 + c * 4 + 4]
|
||||
.copy_from_slice(&v.to_le_bytes());
|
||||
frame.data[i * 16 + c * 4..i * 16 + c * 4 + 4].copy_from_slice(&v.to_le_bytes());
|
||||
}
|
||||
}
|
||||
ctx.upload(src, &frame).unwrap();
|
||||
@@ -2960,7 +3025,9 @@ mod tests {
|
||||
let want = lut.eval(src_px);
|
||||
for c in 0..3 {
|
||||
let g = f32::from_le_bytes(
|
||||
got.data[i * 16 + c * 4..i * 16 + c * 4 + 4].try_into().unwrap(),
|
||||
got.data[i * 16 + c * 4..i * 16 + c * 4 + 4]
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
);
|
||||
assert!(
|
||||
(g - want[c]).abs() < 1e-4,
|
||||
@@ -3008,7 +3075,7 @@ mod tests {
|
||||
#[test]
|
||||
fn display_renderer_rejects_foreign_gl_context() {
|
||||
let mut r = DisplayRenderer::new(BackendKind::Gl);
|
||||
let fake = 0x1 as *mut std::ffi::c_void;
|
||||
let fake = std::ptr::dangling_mut::<std::ffi::c_void>();
|
||||
assert_eq!(r.init(fake).unwrap_err().code(), Error::Invalid.code());
|
||||
}
|
||||
|
||||
@@ -3039,20 +3106,20 @@ mod tests {
|
||||
frame.allocate();
|
||||
frame.data[0] = 0x77;
|
||||
let mut upload_target = r3.create_texture(&pod, None).unwrap();
|
||||
r3.upload_texture(
|
||||
&mut upload_target,
|
||||
frame.data.as_ptr(),
|
||||
frame.linesize_bytes(),
|
||||
)
|
||||
unsafe {
|
||||
r3.upload_texture(
|
||||
&mut upload_target,
|
||||
frame.data.as_ptr(),
|
||||
frame.linesize_bytes(),
|
||||
)
|
||||
}
|
||||
.unwrap();
|
||||
let mut buf = vec![0u8; frame.linesize_bytes() * 4];
|
||||
r3.download_texture(&upload_target, buf.as_mut_ptr(), frame.linesize_bytes())
|
||||
unsafe { r3.download_texture(&upload_target, buf.as_mut_ptr(), frame.linesize_bytes()) }
|
||||
.unwrap();
|
||||
assert_eq!(buf[0], 0x77);
|
||||
// Stride mismatch rejected.
|
||||
assert!(r3
|
||||
.upload_texture(&mut upload_target, frame.data.as_ptr(), 1)
|
||||
.is_err());
|
||||
assert!(unsafe { r3.upload_texture(&mut upload_target, frame.data.as_ptr(), 1) }.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3077,9 +3144,9 @@ mod tests {
|
||||
assert_eq!(df.data[4], 0x22);
|
||||
// Pass-through processor is a no-op.
|
||||
r.blit_color_managed(
|
||||
Some(&src),
|
||||
&mut dst,
|
||||
Some(&crate::color::ColorProcessor::pass_through()),
|
||||
Some(&src),
|
||||
&mut dst,
|
||||
Some(&crate::color::ColorProcessor::pass_through()),
|
||||
)
|
||||
.unwrap();
|
||||
// Size mismatch rejected.
|
||||
|
||||
@@ -318,7 +318,8 @@ impl ColorProcessor {
|
||||
}
|
||||
|
||||
/// Convert a whole F32 frame in place (row-major RGBA).
|
||||
pub fn convert_frame(&self, frame: &mut Frame) -> Result<()> { let Some(cpu) = &self.cpu else {
|
||||
pub fn convert_frame(&self, frame: &mut Frame) -> Result<()> {
|
||||
let Some(cpu) = &self.cpu else {
|
||||
return Ok(()); // pass-through
|
||||
};
|
||||
if frame.format != PixelFormat::F32 {
|
||||
@@ -366,8 +367,7 @@ impl ColorProcessor {
|
||||
if data.len() < count {
|
||||
return Err(Error::Invalid);
|
||||
}
|
||||
let mut f32s: Vec<f32> =
|
||||
data[..count].iter().map(|&v| v as f32 / 255.0).collect();
|
||||
let mut f32s: Vec<f32> = data[..count].iter().map(|&v| v as f32 / 255.0).collect();
|
||||
cpu.try_apply_rgba_pixels(&mut f32s, pixels, 4)
|
||||
.map_err(|e| Error::Failed(format!("OCIO f32 apply (bgra8 detour): {e}")))?;
|
||||
for (dst, v) in data[..count].iter_mut().zip(f32s) {
|
||||
@@ -409,7 +409,7 @@ impl GradingStyle {
|
||||
/// Reinterpret a byte buffer as f32 when its length is a multiple of 4
|
||||
/// (the pipeline guarantees F32 RGBA frames, so alignment is exact).
|
||||
fn bytemuck_f32_slice(data: &mut [u8]) -> Option<&mut [f32]> {
|
||||
if data.len() % 4 != 0 {
|
||||
if !data.len().is_multiple_of(4) {
|
||||
return None;
|
||||
}
|
||||
// SAFETY: length is a multiple of 4 and `data` is byte-aligned; the
|
||||
@@ -427,11 +427,17 @@ fn bytemuck_f32_slice(data: &mut [u8]) -> Option<&mut [f32]> {
|
||||
/// project-properties commit). Render and export paths read it — a single
|
||||
/// project is open at a time, so a process global is the same shape as the
|
||||
/// OCIO default config above.
|
||||
static PIPELINE_COLOR: LazyLock<Mutex<(crate::colormath::WorkingColorSpace, crate::colormath::OutputColorSpec)>> =
|
||||
LazyLock::new(|| Mutex::new((
|
||||
static PIPELINE_COLOR: LazyLock<
|
||||
Mutex<(
|
||||
crate::colormath::WorkingColorSpace,
|
||||
crate::colormath::OutputColorSpec,
|
||||
)>,
|
||||
> = LazyLock::new(|| {
|
||||
Mutex::new((
|
||||
crate::colormath::WorkingColorSpace::default(),
|
||||
crate::colormath::OutputColorSpec::default(),
|
||||
)));
|
||||
))
|
||||
});
|
||||
|
||||
/// Set the pipeline color settings (working space + output spec).
|
||||
pub fn set_pipeline_color_settings(
|
||||
@@ -594,9 +600,7 @@ pub fn config_path() -> Option<String> {
|
||||
return Some(path);
|
||||
}
|
||||
}
|
||||
if default_config().is_none() {
|
||||
return None;
|
||||
}
|
||||
default_config()?;
|
||||
Some(format!(
|
||||
"{}/ocioconf/config.ocio",
|
||||
crate::commonutil::configuration_location()
|
||||
@@ -836,8 +840,8 @@ mod tests {
|
||||
let _lock = config_lock();
|
||||
if set_up_default_config().is_err() {
|
||||
return;
|
||||
} // Create via the built-in config; a valid processor must exist for
|
||||
// the ACES scene→display-encoded pairing and must preserve alpha.
|
||||
} // Create via the built-in config; a valid processor must exist for
|
||||
// the ACES scene→display-encoded pairing and must preserve alpha.
|
||||
let p = ColorProcessor::create("ACEScg", "sRGB Encoded Rec.709 (sRGB)", Direction::Normal);
|
||||
let p = p.expect("processor handle always returned");
|
||||
assert!(
|
||||
@@ -879,7 +883,8 @@ mod tests {
|
||||
return;
|
||||
}
|
||||
let display = config.display_all(0).unwrap();
|
||||
let n = config.num_views_by_reference_space(ocio_rs::SearchReferenceSpaceType::Scene, &display);
|
||||
let n =
|
||||
config.num_views_by_reference_space(ocio_rs::SearchReferenceSpaceType::Scene, &display);
|
||||
assert!(n >= 0);
|
||||
if n > 0 {
|
||||
let view = config
|
||||
@@ -913,7 +918,7 @@ mod tests {
|
||||
let f32s: &mut [f32] = unsafe {
|
||||
std::slice::from_raw_parts_mut(f.data.as_mut_ptr() as *mut f32, f.pixel_count() * 4)
|
||||
};
|
||||
for px in f32s.chunks_exact_mut(4) {
|
||||
for px in f32s.as_chunks_mut::<4>().0 {
|
||||
px[0] = 0.18;
|
||||
px[1] = 0.18;
|
||||
px[2] = 0.18;
|
||||
@@ -960,8 +965,10 @@ mod tests {
|
||||
"0.18 linear grey should land near 0.5 sRGB (got {})",
|
||||
out[0]
|
||||
);
|
||||
assert!((out[0] - out[1]).abs() < 1e-3 && (out[1] - out[2]).abs() < 1e-3,
|
||||
"grey stays grey: {out:?}");
|
||||
assert!(
|
||||
(out[0] - out[1]).abs() < 1e-3 && (out[1] - out[2]).abs() < 1e-3,
|
||||
"grey stays grey: {out:?}"
|
||||
);
|
||||
assert!((out[3] - 1.0).abs() < 1e-5, "alpha preserved");
|
||||
}
|
||||
|
||||
@@ -1073,7 +1080,7 @@ mod tests {
|
||||
let f32_result = p32.convert_f32_rgba(&mut f32s, 4);
|
||||
eprintln!("f32 convert: {f32_result:?} -> {f32s:?}");
|
||||
bgra_result.expect("convert");
|
||||
for (i, px) in data.chunks_exact(4).enumerate() {
|
||||
for (i, px) in data.as_chunks::<4>().0.iter().enumerate() {
|
||||
assert_eq!(px[3], 255, "pixel {i}: alpha preserved");
|
||||
let rgb: u32 = px[0] as u32 + px[1] as u32 + px[2] as u32;
|
||||
assert!(rgb > 0, "pixel {i} must not be crushed to black: {px:?}");
|
||||
@@ -1085,7 +1092,7 @@ mod tests {
|
||||
"grey stays grey through the display ICC: {b} {g} {r}"
|
||||
);
|
||||
f32_result.expect("convert f32");
|
||||
for (i, px) in f32s.chunks_exact(4).enumerate() {
|
||||
for (i, px) in f32s.as_chunks::<4>().0.iter().enumerate() {
|
||||
assert!((px[3] - 1.0).abs() < 1e-3, "pixel {i}: alpha preserved");
|
||||
let rgb = px[0] + px[1] + px[2];
|
||||
assert!(rgb > 0.0, "pixel {i} must not be crushed to black: {px:?}");
|
||||
@@ -1171,7 +1178,10 @@ mod tests {
|
||||
stub.contains("ove_grading_primary"),
|
||||
"function name present"
|
||||
);
|
||||
assert!(!stub.contains("sampler"), "no LUT upload expected for grading");
|
||||
assert!(
|
||||
!stub.contains("sampler"),
|
||||
"no LUT upload expected for grading"
|
||||
);
|
||||
// Cache hit: a repeated call returns the same text.
|
||||
let again = grading_primary_function_shader(style).unwrap();
|
||||
assert_eq!(stub, again);
|
||||
@@ -1200,8 +1210,14 @@ mod tests {
|
||||
"cie_xyz_d65_interchange",
|
||||
)
|
||||
.expect("default config generates an analytic shader");
|
||||
assert!(stub.contains("SceneLinearToCIEXYZ_d65"), "function name present");
|
||||
assert!(!stub.contains("sampler"), "no LUT upload expected in the default config");
|
||||
assert!(
|
||||
stub.contains("SceneLinearToCIEXYZ_d65"),
|
||||
"function name present"
|
||||
);
|
||||
assert!(
|
||||
!stub.contains("sampler"),
|
||||
"no LUT upload expected in the default config"
|
||||
);
|
||||
// Cache hit: a repeated call returns the same text.
|
||||
let again = ocio_function_shader(
|
||||
"SceneLinearToCIEXYZ_d65",
|
||||
@@ -1217,9 +1233,9 @@ mod tests {
|
||||
let _lock = config_lock();
|
||||
// studio-config's Rec.709 display is a CLF LUT chain (unlike the
|
||||
// analytic sRGB one); without a LUT upload path it must be refused.
|
||||
let Ok(cfg) = ocio_rs::Config::create_from_builtin_config(
|
||||
"studio-config-v2.1.0_aces-v1.3_ocio-v2.3",
|
||||
) else {
|
||||
let Ok(cfg) =
|
||||
ocio_rs::Config::create_from_builtin_config("studio-config-v2.1.0_aces-v1.3_ocio-v2.3")
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let cfg = SafeConfig(cfg);
|
||||
@@ -1234,4 +1250,3 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -495,15 +495,14 @@ const LUT_N: usize = 4096;
|
||||
/// Declare one lazily-built transfer LUT over [0, 1].
|
||||
macro_rules! tf_lut {
|
||||
($name:ident, $f:expr) => {
|
||||
static $name: std::sync::LazyLock<[f32; LUT_N + 1]> =
|
||||
std::sync::LazyLock::new(|| {
|
||||
let f: fn(f32) -> f32 = $f;
|
||||
let mut t = [0.0f32; LUT_N + 1];
|
||||
for (i, e) in t.iter_mut().enumerate() {
|
||||
*e = f(i as f32 / LUT_N as f32);
|
||||
}
|
||||
t
|
||||
});
|
||||
static $name: std::sync::LazyLock<[f32; LUT_N + 1]> = std::sync::LazyLock::new(|| {
|
||||
let f: fn(f32) -> f32 = $f;
|
||||
let mut t = [0.0f32; LUT_N + 1];
|
||||
for (i, e) in t.iter_mut().enumerate() {
|
||||
*e = f(i as f32 / LUT_N as f32);
|
||||
}
|
||||
t
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -663,8 +662,8 @@ pub enum SourceTransfer {
|
||||
/// (AVCOL_PRI_* numbering, H.273 ISO codes.)
|
||||
pub fn source_primaries_from_av(color_primaries: i32) -> SourcePrimaries {
|
||||
match color_primaries {
|
||||
1 => SourcePrimaries::Bt709, // BT.709
|
||||
9 => SourcePrimaries::Bt2020, // BT.2020
|
||||
1 => SourcePrimaries::Bt709, // BT.709
|
||||
9 => SourcePrimaries::Bt2020, // BT.2020
|
||||
11 => SourcePrimaries::DisplayP3, // SMPTE RP 431-2 (DCI-P3)
|
||||
12 => SourcePrimaries::DisplayP3, // SMPTE EG 432-1 (Display P3)
|
||||
_ => SourcePrimaries::Unknown,
|
||||
@@ -675,16 +674,16 @@ pub fn source_primaries_from_av(color_primaries: i32) -> SourcePrimaries {
|
||||
/// (AVCOL_TRC_* numbering, H.273 ISO codes.)
|
||||
pub fn source_transfer_from_av(color_trc: i32) -> SourceTransfer {
|
||||
match color_trc {
|
||||
1 => SourceTransfer::SdrGamma, // BT.709 (see the enum docs: OETF inverse ≈ 2.2)
|
||||
4 => SourceTransfer::Gamma22, // gamma 2.2
|
||||
5 => SourceTransfer::Gamma28, // gamma 2.8
|
||||
6 => SourceTransfer::SdrGamma, // SMPTE 170M
|
||||
1 => SourceTransfer::SdrGamma, // BT.709 (see the enum docs: OETF inverse ≈ 2.2)
|
||||
4 => SourceTransfer::Gamma22, // gamma 2.2
|
||||
5 => SourceTransfer::Gamma28, // gamma 2.8
|
||||
6 => SourceTransfer::SdrGamma, // SMPTE 170M
|
||||
13 => SourceTransfer::SdrGamma, // sRGB (its EOTF is applied at decode)
|
||||
14 => SourceTransfer::SdrGamma, // BT.2020 10-bit
|
||||
15 => SourceTransfer::SdrGamma, // BT.2020 12-bit
|
||||
16 => SourceTransfer::Pq, // SMPTE ST 2084
|
||||
18 => SourceTransfer::Hlg, // ARIB STD-B67
|
||||
8 => SourceTransfer::Linear, // linear
|
||||
16 => SourceTransfer::Pq, // SMPTE ST 2084
|
||||
18 => SourceTransfer::Hlg, // ARIB STD-B67
|
||||
8 => SourceTransfer::Linear, // linear
|
||||
_ => SourceTransfer::Unknown,
|
||||
}
|
||||
}
|
||||
@@ -708,15 +707,11 @@ impl SourcePrimaries {
|
||||
/// own primaries) → ACEScg linear. `samples` is an F32 RGBA buffer,
|
||||
/// transformed in place. Linearize and the primaries matrix are fused
|
||||
/// into a single pass (LUT'd transfer, row-band parallel).
|
||||
pub fn decode_to_acescg(
|
||||
samples: &mut [f32],
|
||||
primaries: SourcePrimaries,
|
||||
transfer: SourceTransfer,
|
||||
) {
|
||||
pub fn decode_to_acescg(samples: &mut [f32], primaries: SourcePrimaries, transfer: SourceTransfer) {
|
||||
let linearize = decode_transfer_fn(transfer);
|
||||
let matrix = rgb_to_rgb_matrix(primaries.primaries(), PRIMARIES_AP1);
|
||||
par_pixels_f32(samples, move |band| {
|
||||
for px in band.chunks_exact_mut(4) {
|
||||
for px in band.as_chunks_mut::<4>().0 {
|
||||
for c in 0..3 {
|
||||
px[c] = linearize(px[c]);
|
||||
}
|
||||
@@ -740,7 +735,7 @@ pub fn decode_to_acescg_bytes(
|
||||
let linearize = decode_transfer_fn(transfer);
|
||||
let matrix = rgb_to_rgb_matrix(primaries.primaries(), PRIMARIES_AP1);
|
||||
par_pixels_bytes(bytes, move |band| {
|
||||
for px in band.chunks_exact_mut(16) {
|
||||
for px in band.as_chunks_mut::<16>().0 {
|
||||
let mut v = [
|
||||
f32::from_le_bytes(px[0..4].try_into().unwrap()),
|
||||
f32::from_le_bytes(px[4..8].try_into().unwrap()),
|
||||
@@ -772,7 +767,7 @@ pub fn acescg_to_output(samples: &mut [f32], spec: OutputColorSpec) {
|
||||
let matrix = rgb_to_rgb_matrix(PRIMARIES_AP1, spec.gamut.primaries());
|
||||
let encode = output_transfer_fn(spec.transfer);
|
||||
par_pixels_f32(samples, move |band| {
|
||||
for px in band.chunks_exact_mut(4) {
|
||||
for px in band.as_chunks_mut::<4>().0 {
|
||||
apply_mat(matrix, px);
|
||||
for c in 0..3 {
|
||||
px[c] = encode(px[c].clamp(0.0, 1.0));
|
||||
@@ -792,7 +787,7 @@ pub fn acescg_to_output_bytes(bytes: &mut [u8], pixels: usize, spec: OutputColor
|
||||
let matrix = rgb_to_rgb_matrix(PRIMARIES_AP1, spec.gamut.primaries());
|
||||
let encode = output_transfer_fn(spec.transfer);
|
||||
par_pixels_bytes(bytes, move |band| {
|
||||
for px in band.chunks_exact_mut(16) {
|
||||
for px in band.as_chunks_mut::<16>().0 {
|
||||
let mut v = [
|
||||
f32::from_le_bytes(px[0..4].try_into().unwrap()),
|
||||
f32::from_le_bytes(px[4..8].try_into().unwrap()),
|
||||
@@ -816,7 +811,7 @@ pub fn acescg_to_output_bytes(bytes: &mut [u8], pixels: usize, spec: OutputColor
|
||||
pub fn apply_transfer_oetf(samples: &mut [f32], transfer: OutputTransfer) {
|
||||
let encode = output_transfer_fn(transfer);
|
||||
par_pixels_f32(samples, move |band| {
|
||||
for px in band.chunks_exact_mut(4) {
|
||||
for px in band.as_chunks_mut::<4>().0 {
|
||||
for c in 0..3 {
|
||||
px[c] = encode(px[c]);
|
||||
}
|
||||
@@ -860,7 +855,7 @@ pub fn output_spec_to_xyz_d65(samples: &mut [f32], spec: OutputColorSpec) {
|
||||
});
|
||||
let matrix = rgb_to_xyz_matrix(spec.gamut.primaries());
|
||||
par_pixels_f32(samples, move |band| {
|
||||
for px in band.chunks_exact_mut(4) {
|
||||
for px in band.as_chunks_mut::<4>().0 {
|
||||
for c in 0..3 {
|
||||
px[c] = linearize(px[c]);
|
||||
}
|
||||
@@ -931,9 +926,18 @@ pub fn yuv444p16_to_rgb_f32(
|
||||
// `width` samples.
|
||||
let row_bytes = width.saturating_mul(2);
|
||||
let rows_after = height.saturating_sub(1);
|
||||
if y_plane.len() < rows_after.saturating_mul(y_stride).saturating_add(row_bytes)
|
||||
|| u_plane.len() < rows_after.saturating_mul(u_stride).saturating_add(row_bytes)
|
||||
|| v_plane.len() < rows_after.saturating_mul(v_stride).saturating_add(row_bytes)
|
||||
if y_plane.len()
|
||||
< rows_after
|
||||
.saturating_mul(y_stride)
|
||||
.saturating_add(row_bytes)
|
||||
|| u_plane.len()
|
||||
< rows_after
|
||||
.saturating_mul(u_stride)
|
||||
.saturating_add(row_bytes)
|
||||
|| v_plane.len()
|
||||
< rows_after
|
||||
.saturating_mul(v_stride)
|
||||
.saturating_add(row_bytes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -946,7 +950,11 @@ pub fn yuv444p16_to_rgb_f32(
|
||||
} else {
|
||||
(4096.0f32, 32768.0f32, 1.0 / 57344.0)
|
||||
};
|
||||
let y_scale = if full_range { 1.0 / 65535.0 } else { 1.0 / 56064.0 };
|
||||
let y_scale = if full_range {
|
||||
1.0 / 65535.0
|
||||
} else {
|
||||
1.0 / 56064.0
|
||||
};
|
||||
for row in 0..height {
|
||||
let y_row = row * y_stride;
|
||||
let u_row = row * u_stride;
|
||||
@@ -1002,9 +1010,9 @@ mod tests {
|
||||
// 0.0206157712 0.1095697056 0.8698145232
|
||||
let m = rgb_to_rgb_matrix(PRIMARIES_SRGB, PRIMARIES_AP1);
|
||||
let expected: Mat3 = [
|
||||
[0.6131324224, 0.3395230762, 0.0473445014],
|
||||
[0.0701922769, 0.9163536767, 0.0134540464],
|
||||
[0.0206157712, 0.1095697056, 0.8698145232],
|
||||
[0.613_132_4, 0.339_523_08, 0.047_344_502],
|
||||
[0.070_192_28, 0.916_353_7, 0.013_454_046],
|
||||
[0.020_615_771, 0.109_569_706, 0.869_814_5],
|
||||
];
|
||||
for r in 0..3 {
|
||||
for c in 0..3 {
|
||||
@@ -1034,7 +1042,11 @@ mod tests {
|
||||
// → sRGB linear → (OETF) → the original codes.
|
||||
let original = [0.25f32, 0.5, 0.75, 1.0];
|
||||
let mut samples = original;
|
||||
decode_to_acescg(&mut samples, SourcePrimaries::Bt709, SourceTransfer::SdrGamma);
|
||||
decode_to_acescg(
|
||||
&mut samples,
|
||||
SourcePrimaries::Bt709,
|
||||
SourceTransfer::SdrGamma,
|
||||
);
|
||||
acescg_to_output(
|
||||
&mut samples,
|
||||
OutputColorSpec {
|
||||
@@ -1056,7 +1068,11 @@ mod tests {
|
||||
fn round_trip_bt2020_acescg_bt2020() {
|
||||
let original = [0.2f32, 0.6, 0.9, 1.0];
|
||||
let mut samples = original;
|
||||
decode_to_acescg(&mut samples, SourcePrimaries::Bt2020, SourceTransfer::SdrGamma);
|
||||
decode_to_acescg(
|
||||
&mut samples,
|
||||
SourcePrimaries::Bt2020,
|
||||
SourceTransfer::SdrGamma,
|
||||
);
|
||||
acescg_to_output(
|
||||
&mut samples,
|
||||
OutputColorSpec {
|
||||
@@ -1081,7 +1097,11 @@ mod tests {
|
||||
let mut samples = [1.0f32, 1.0, 1.0, 1.0];
|
||||
decode_to_acescg(&mut samples, SourcePrimaries::Bt709, SourceTransfer::Linear);
|
||||
for i in 0..3 {
|
||||
assert!(approx(samples[i], 1.0, 1e-3), "white channel {i} = {}", samples[i]);
|
||||
assert!(
|
||||
approx(samples[i], 1.0, 1e-3),
|
||||
"white channel {i} = {}",
|
||||
samples[i]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1093,7 +1113,11 @@ mod tests {
|
||||
assert!(approx(srgb_eotf(0.0), 0.0, 1e-6), "eotf(0)");
|
||||
assert!(approx(srgb_eotf(1.0), 1.0, 1e-6), "eotf(1)");
|
||||
// Mid-grey: linear 0.18 → ~0.461 sRGB code (the classic check).
|
||||
assert!(approx(srgb_oetf(0.18), 0.4613, 1e-3), "oetf(0.18) = {}", srgb_oetf(0.18));
|
||||
assert!(
|
||||
approx(srgb_oetf(0.18), 0.4613, 1e-3),
|
||||
"oetf(0.18) = {}",
|
||||
srgb_oetf(0.18)
|
||||
);
|
||||
// Round trip.
|
||||
for v in [0.0f32, 0.01, 0.18, 0.5, 0.99, 1.0] {
|
||||
assert!(approx(srgb_eotf(srgb_oetf(v)), v, 1e-4), "round trip {v}");
|
||||
@@ -1103,9 +1127,17 @@ mod tests {
|
||||
#[test]
|
||||
fn pq_transfer_anchors() {
|
||||
// PQ code 1.0 = 10 000 nits = normalized 1.0.
|
||||
assert!(approx(pq_eotf(1.0), 1.0, 1e-4), "pq eotf(1) = {}", pq_eotf(1.0));
|
||||
assert!(
|
||||
approx(pq_eotf(1.0), 1.0, 1e-4),
|
||||
"pq eotf(1) = {}",
|
||||
pq_eotf(1.0)
|
||||
);
|
||||
// PQ code 0.5 ≈ 100 nits (normalized 0.01).
|
||||
assert!(approx(pq_eotf(0.5), 0.01008, 2e-3), "pq eotf(0.5) = {}", pq_eotf(0.5));
|
||||
assert!(
|
||||
approx(pq_eotf(0.5), 0.01008, 2e-3),
|
||||
"pq eotf(0.5) = {}",
|
||||
pq_eotf(0.5)
|
||||
);
|
||||
// Round trip.
|
||||
for v in [0.0f32, 0.25, 0.5, 0.75, 1.0] {
|
||||
assert!(approx(pq_eotf(pq_oetf(v)), v, 1e-3), "pq round trip {v}");
|
||||
@@ -1115,9 +1147,17 @@ mod tests {
|
||||
#[test]
|
||||
fn hlg_transfer_anchors() {
|
||||
assert!(approx(hlg_eotf(0.0), 0.0, 1e-6), "hlg eotf(0)");
|
||||
assert!(approx(hlg_eotf(1.0), 1.0, 1e-3), "hlg eotf(1) = {}", hlg_eotf(1.0));
|
||||
assert!(
|
||||
approx(hlg_eotf(1.0), 1.0, 1e-3),
|
||||
"hlg eotf(1) = {}",
|
||||
hlg_eotf(1.0)
|
||||
);
|
||||
// The piecewise join: code 0.5 ↔ linear 1/12.
|
||||
assert!(approx(hlg_eotf(0.5), 1.0 / 12.0, 1e-3), "hlg eotf(0.5) = {}", hlg_eotf(0.5));
|
||||
assert!(
|
||||
approx(hlg_eotf(0.5), 1.0 / 12.0, 1e-3),
|
||||
"hlg eotf(0.5) = {}",
|
||||
hlg_eotf(0.5)
|
||||
);
|
||||
// Round trip.
|
||||
for v in [0.0f32, 0.1, 0.5, 0.9, 1.0] {
|
||||
assert!(approx(hlg_eotf(hlg_oetf(v)), v, 1e-3), "hlg round trip {v}");
|
||||
@@ -1146,18 +1186,30 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn setting_round_trips() {
|
||||
assert_eq!(WorkingColorSpace::from_setting("acescg"), WorkingColorSpace::AcesCg);
|
||||
assert_eq!(
|
||||
WorkingColorSpace::from_setting("acescg"),
|
||||
WorkingColorSpace::AcesCg
|
||||
);
|
||||
assert_eq!(
|
||||
WorkingColorSpace::from_setting("srgb_legacy"),
|
||||
WorkingColorSpace::SrgbLegacy
|
||||
);
|
||||
assert_eq!(WorkingColorSpace::from_setting("bogus"), WorkingColorSpace::AcesCg);
|
||||
assert_eq!(OutputGamut::from_setting("displayp3"), OutputGamut::DisplayP3);
|
||||
assert_eq!(
|
||||
WorkingColorSpace::from_setting("bogus"),
|
||||
WorkingColorSpace::AcesCg
|
||||
);
|
||||
assert_eq!(
|
||||
OutputGamut::from_setting("displayp3"),
|
||||
OutputGamut::DisplayP3
|
||||
);
|
||||
assert_eq!(OutputGamut::from_setting("bt2020"), OutputGamut::Bt2020);
|
||||
assert_eq!(OutputGamut::from_setting(""), OutputGamut::Srgb);
|
||||
assert_eq!(OutputTransfer::from_setting("pq"), OutputTransfer::Pq);
|
||||
assert_eq!(OutputTransfer::from_setting("hlg"), OutputTransfer::Hlg);
|
||||
assert_eq!(OutputTransfer::from_setting("gamma22"), OutputTransfer::Gamma22);
|
||||
assert_eq!(
|
||||
OutputTransfer::from_setting("gamma22"),
|
||||
OutputTransfer::Gamma22
|
||||
);
|
||||
assert_eq!(OutputTransfer::from_setting(""), OutputTransfer::Srgb);
|
||||
assert_eq!(
|
||||
OutputColorSpec::from_settings("displayp3", "pq"),
|
||||
@@ -1184,13 +1236,31 @@ mod tests {
|
||||
fn gamma_transfer_anchors() {
|
||||
// Pure-power EOTFs at code 0.5. The 2.4 case is the BT.1886
|
||||
// mid-grey link: 0.5²·⁴ ≈ 0.189 (≈ the classic 18 % grey).
|
||||
assert!(approx(gamma_eotf(0.5, 2.4), 0.1894646, 1e-5), "eotf(0.5, 2.4) = {}", gamma_eotf(0.5, 2.4));
|
||||
assert!(approx(gamma_eotf(0.5, 2.2), 0.2176376, 1e-5), "eotf(0.5, 2.2) = {}", gamma_eotf(0.5, 2.2));
|
||||
assert!(approx(gamma_eotf(0.5, 2.8), 0.1435894, 1e-5), "eotf(0.5, 2.8) = {}", gamma_eotf(0.5, 2.8));
|
||||
assert!(
|
||||
approx(gamma_eotf(0.5, 2.4), 0.1894646, 1e-5),
|
||||
"eotf(0.5, 2.4) = {}",
|
||||
gamma_eotf(0.5, 2.4)
|
||||
);
|
||||
assert!(
|
||||
approx(gamma_eotf(0.5, 2.2), 0.2176376, 1e-5),
|
||||
"eotf(0.5, 2.2) = {}",
|
||||
gamma_eotf(0.5, 2.2)
|
||||
);
|
||||
assert!(
|
||||
approx(gamma_eotf(0.5, 2.8), 0.1435894, 1e-5),
|
||||
"eotf(0.5, 2.8) = {}",
|
||||
gamma_eotf(0.5, 2.8)
|
||||
);
|
||||
// Endpoints and round trips.
|
||||
for gamma in [2.2f32, 2.4, 2.8] {
|
||||
assert!(approx(gamma_eotf(0.0, gamma), 0.0, 1e-6), "eotf(0) g{gamma}");
|
||||
assert!(approx(gamma_eotf(1.0, gamma), 1.0, 1e-6), "eotf(1) g{gamma}");
|
||||
assert!(
|
||||
approx(gamma_eotf(0.0, gamma), 0.0, 1e-6),
|
||||
"eotf(0) g{gamma}"
|
||||
);
|
||||
assert!(
|
||||
approx(gamma_eotf(1.0, gamma), 1.0, 1e-6),
|
||||
"eotf(1) g{gamma}"
|
||||
);
|
||||
for v in [0.0f32, 0.18, 0.5, 0.9, 1.0] {
|
||||
assert!(
|
||||
approx(gamma_eotf(gamma_oetf(v, gamma), gamma), v, 1e-4),
|
||||
@@ -1244,14 +1314,20 @@ mod tests {
|
||||
.map(|i: usize| ((i.wrapping_mul(2654435761)) % 1000) as f32 / 999.0)
|
||||
.collect();
|
||||
let reference: Vec<f32> = samples
|
||||
.chunks_exact(4)
|
||||
.as_chunks::<4>()
|
||||
.0
|
||||
.iter()
|
||||
.flat_map(|px| {
|
||||
let mut v = [srgb_eotf(px[0]), srgb_eotf(px[1]), srgb_eotf(px[2]), px[3]];
|
||||
apply_mat(rgb_to_rgb_matrix(PRIMARIES_SRGB, PRIMARIES_AP1), &mut v);
|
||||
v
|
||||
})
|
||||
.collect();
|
||||
decode_to_acescg(&mut samples, SourcePrimaries::Bt709, SourceTransfer::SdrGamma);
|
||||
decode_to_acescg(
|
||||
&mut samples,
|
||||
SourcePrimaries::Bt709,
|
||||
SourceTransfer::SdrGamma,
|
||||
);
|
||||
for (i, (&got, &want)) in samples.iter().zip(reference.iter()).enumerate() {
|
||||
assert!(approx(got, want, 1e-4), "pixel {i}: {got} vs {want}");
|
||||
}
|
||||
@@ -1267,7 +1343,11 @@ mod tests {
|
||||
// (the ~1.1 system gamma belongs at final display only).
|
||||
for code in [0.05f32, 0.18, 0.5, 0.75, 1.0] {
|
||||
let mut samples = [code, code, code, 1.0];
|
||||
decode_to_acescg(&mut samples, SourcePrimaries::Bt709, SourceTransfer::SdrGamma);
|
||||
decode_to_acescg(
|
||||
&mut samples,
|
||||
SourcePrimaries::Bt709,
|
||||
SourceTransfer::SdrGamma,
|
||||
);
|
||||
acescg_to_output(&mut samples, OutputColorSpec::default());
|
||||
for i in 0..3 {
|
||||
assert!(
|
||||
@@ -1283,7 +1363,12 @@ mod tests {
|
||||
bytes[0..4].copy_from_slice(&0.5f32.to_le_bytes());
|
||||
bytes[4..8].copy_from_slice(&0.5f32.to_le_bytes());
|
||||
bytes[8..12].copy_from_slice(&0.5f32.to_le_bytes());
|
||||
decode_to_acescg_bytes(&mut bytes, 1, SourcePrimaries::Bt709, SourceTransfer::SdrGamma);
|
||||
decode_to_acescg_bytes(
|
||||
&mut bytes,
|
||||
1,
|
||||
SourcePrimaries::Bt709,
|
||||
SourceTransfer::SdrGamma,
|
||||
);
|
||||
let r = f32::from_le_bytes(bytes[0..4].try_into().unwrap());
|
||||
let m = rgb_to_rgb_matrix(PRIMARIES_SRGB, PRIMARIES_AP1);
|
||||
let expected = mat_vec(m, [srgb_eotf(0.5); 3]);
|
||||
@@ -1321,7 +1406,11 @@ mod tests {
|
||||
samples[c],
|
||||
expected[c]
|
||||
);
|
||||
assert!(samples[c] >= 0.0 && samples[c] <= 1.0, "channel {c} escaped [0, 1]: {}", samples[c]);
|
||||
assert!(
|
||||
samples[c] >= 0.0 && samples[c] <= 1.0,
|
||||
"channel {c} escaped [0, 1]: {}",
|
||||
samples[c]
|
||||
);
|
||||
}
|
||||
assert_eq!(samples[3], 0.5, "alpha must be untouched");
|
||||
}
|
||||
@@ -1373,10 +1462,26 @@ mod tests {
|
||||
let u = yuv_plane16(&[&[128 << 8], &[128 << 8]], width, stride);
|
||||
let v = u.clone();
|
||||
let mut out = [0.0f32; 8];
|
||||
yuv444p16_to_rgb_f32(&y, stride, &u, stride, &v, stride, width, height, YuvMatrix::Bt709, false, &mut out);
|
||||
yuv444p16_to_rgb_f32(
|
||||
&y,
|
||||
stride,
|
||||
&u,
|
||||
stride,
|
||||
&v,
|
||||
stride,
|
||||
width,
|
||||
height,
|
||||
YuvMatrix::Bt709,
|
||||
false,
|
||||
&mut out,
|
||||
);
|
||||
for c in 0..3 {
|
||||
assert!(approx(out[c], 1.0, 1e-4), "white channel {c}: {}", out[c]);
|
||||
assert!(approx(out[4 + c], 0.0, 1e-4), "black channel {c}: {}", out[4 + c]);
|
||||
assert!(
|
||||
approx(out[4 + c], 0.0, 1e-4),
|
||||
"black channel {c}: {}",
|
||||
out[4 + c]
|
||||
);
|
||||
}
|
||||
assert_eq!(out[3], 1.0, "alpha");
|
||||
assert_eq!(out[7], 1.0, "alpha");
|
||||
@@ -1394,7 +1499,19 @@ mod tests {
|
||||
let u = yuv_plane16(&[&[11059]], width, stride);
|
||||
let v = yuv_plane16(&[&[5329]], width, stride);
|
||||
let mut out = [0.0f32; 4];
|
||||
yuv444p16_to_rgb_f32(&y, stride, &u, stride, &v, stride, width, height, YuvMatrix::Bt601, true, &mut out);
|
||||
yuv444p16_to_rgb_f32(
|
||||
&y,
|
||||
stride,
|
||||
&u,
|
||||
stride,
|
||||
&v,
|
||||
stride,
|
||||
width,
|
||||
height,
|
||||
YuvMatrix::Bt601,
|
||||
true,
|
||||
&mut out,
|
||||
);
|
||||
assert!(approx(out[0], 0.0, 1e-3), "R = {}", out[0]);
|
||||
assert!(approx(out[1], 1.0, 1e-3), "G = {}", out[1]);
|
||||
assert!(approx(out[2], 0.0, 1e-3), "B = {}", out[2]);
|
||||
@@ -1412,7 +1529,19 @@ mod tests {
|
||||
let u = yuv_plane16(&[&[10666]], width, stride);
|
||||
let v = yuv_plane16(&[&[6725]], width, stride);
|
||||
let mut out = [0.0f32; 4];
|
||||
yuv444p16_to_rgb_f32(&y, stride, &u, stride, &v, stride, width, height, YuvMatrix::Bt709, false, &mut out);
|
||||
yuv444p16_to_rgb_f32(
|
||||
&y,
|
||||
stride,
|
||||
&u,
|
||||
stride,
|
||||
&v,
|
||||
stride,
|
||||
width,
|
||||
height,
|
||||
YuvMatrix::Bt709,
|
||||
false,
|
||||
&mut out,
|
||||
);
|
||||
assert!(approx(out[0], 0.0, 1e-3), "R = {}", out[0]);
|
||||
assert!(approx(out[1], 1.0, 1e-3), "G = {}", out[1]);
|
||||
assert!(approx(out[2], 0.0, 1e-3), "B = {}", out[2]);
|
||||
@@ -1423,11 +1552,35 @@ mod tests {
|
||||
// A 2×2 frame at stride 4 needs (2−1)·4 + 2·2 = 8 bytes per plane;
|
||||
// 4 bytes is too short → early return, `out` untouched.
|
||||
let mut out = [7.0f32; 4];
|
||||
yuv444p16_to_rgb_f32(&[0u8; 4], 4, &[0u8; 4], 4, &[0u8; 4], 4, 2, 2, YuvMatrix::Bt709, false, &mut out);
|
||||
yuv444p16_to_rgb_f32(
|
||||
&[0u8; 4],
|
||||
4,
|
||||
&[0u8; 4],
|
||||
4,
|
||||
&[0u8; 4],
|
||||
4,
|
||||
2,
|
||||
2,
|
||||
YuvMatrix::Bt709,
|
||||
false,
|
||||
&mut out,
|
||||
);
|
||||
assert_eq!(out, [7.0; 4]);
|
||||
// `out` too short for the frame → early return.
|
||||
let mut out2 = [7.0f32; 3];
|
||||
yuv444p16_to_rgb_f32(&[0u8; 8], 4, &[0u8; 8], 4, &[0u8; 8], 4, 2, 2, YuvMatrix::Bt709, false, &mut out2);
|
||||
yuv444p16_to_rgb_f32(
|
||||
&[0u8; 8],
|
||||
4,
|
||||
&[0u8; 8],
|
||||
4,
|
||||
&[0u8; 8],
|
||||
4,
|
||||
2,
|
||||
2,
|
||||
YuvMatrix::Bt709,
|
||||
false,
|
||||
&mut out2,
|
||||
);
|
||||
assert_eq!(out2, [7.0; 3]);
|
||||
}
|
||||
|
||||
|
||||
@@ -308,10 +308,7 @@ impl ConfigStore {
|
||||
Some(slash) => {
|
||||
let group = key[..slash].to_string();
|
||||
let sub = key[slash + 1..].to_string();
|
||||
sections
|
||||
.entry(group)
|
||||
.or_default()
|
||||
.insert(sub, toml_value);
|
||||
sections.entry(group).or_default().insert(sub, toml_value);
|
||||
}
|
||||
None => {
|
||||
sections
|
||||
@@ -823,7 +820,7 @@ fn format_g(v: f64) -> String {
|
||||
let sci = format!("{:.*e}", (PRECISION - 1) as usize, a); // "d.ddddd e±N"
|
||||
let exp = sci.split('e').nth(1).unwrap().parse::<i32>().unwrap();
|
||||
let xr = exp;
|
||||
let body = if xr < -4 || xr >= PRECISION {
|
||||
let body = if !(-4..PRECISION).contains(&xr) {
|
||||
// Scientific notation: mantissa is already rounded; strip trailing zeros.
|
||||
let mant = sci.split('e').next().unwrap();
|
||||
let m = trim_mantissa(mant);
|
||||
@@ -1164,7 +1161,11 @@ mod tests {
|
||||
let content = std::fs::read_to_string(dir.join("config.toml")).unwrap();
|
||||
// Native TOML types: ints/bools/floats unquoted, strings quoted.
|
||||
assert!(content.contains("FlatKey = 1"), "content: {}", content);
|
||||
assert!(content.contains("StrKey = \"hello\""), "content: {}", content);
|
||||
assert!(
|
||||
content.contains("StrKey = \"hello\""),
|
||||
"content: {}",
|
||||
content
|
||||
);
|
||||
assert!(content.contains("DblKey = 2.5"), "content: {}", content);
|
||||
assert!(content.contains("BoolKey = true"), "content: {}", content);
|
||||
// Grouped keys land in [group] tables.
|
||||
@@ -1262,9 +1263,21 @@ UnknownTypedThing=hello
|
||||
assert_eq!(s.get_double(Some("render"), "gain", -1.0), -1.0);
|
||||
// The file holds native TOML types, not stringified values.
|
||||
let content = std::fs::read_to_string(dir.join("config.toml")).unwrap();
|
||||
assert!(content.contains("DefaultSequenceWidth = 640"), "content: {}", content);
|
||||
assert!(content.contains("SplitClipsCopyNodes = false"), "content: {}", content);
|
||||
assert!(content.contains("ProxyPreset = \"medium\""), "content: {}", content);
|
||||
assert!(
|
||||
content.contains("DefaultSequenceWidth = 640"),
|
||||
"content: {}",
|
||||
content
|
||||
);
|
||||
assert!(
|
||||
content.contains("SplitClipsCopyNodes = false"),
|
||||
"content: {}",
|
||||
content
|
||||
);
|
||||
assert!(
|
||||
content.contains("ProxyPreset = \"medium\""),
|
||||
"content: {}",
|
||||
content
|
||||
);
|
||||
assert!(content.contains("gain = 1.5"), "content: {}", content);
|
||||
});
|
||||
}
|
||||
@@ -1293,10 +1306,22 @@ CustomKey=hello
|
||||
assert!(dir.join("config.toml").is_file());
|
||||
assert!(dir.join("config.ini").is_file());
|
||||
let content = std::fs::read_to_string(dir.join("config.toml")).unwrap();
|
||||
assert!(content.contains("DefaultSequenceWidth = 640"), "content: {}", content);
|
||||
assert!(content.contains("UseProxyMedia = false"), "content: {}", content);
|
||||
assert!(
|
||||
content.contains("DefaultSequenceWidth = 640"),
|
||||
"content: {}",
|
||||
content
|
||||
);
|
||||
assert!(
|
||||
content.contains("UseProxyMedia = false"),
|
||||
"content: {}",
|
||||
content
|
||||
);
|
||||
assert!(content.contains("[section]"), "content: {}", content);
|
||||
assert!(content.contains("CustomKey = \"hello\""), "content: {}", content);
|
||||
assert!(
|
||||
content.contains("CustomKey = \"hello\""),
|
||||
"content: {}",
|
||||
content
|
||||
);
|
||||
|
||||
// A second load reads the migrated TOML, not the INI.
|
||||
s.reset_defaults().unwrap();
|
||||
@@ -1615,7 +1640,11 @@ CustomKey=hello
|
||||
// No group defaults exist; every top-level key is a plain value.
|
||||
assert!(table.values().all(|v| !v.is_table()));
|
||||
// Serialized output must not contain INI-style "k=v" lines.
|
||||
assert!(!content.contains("DefaultSequenceWidth=1920"), "content: {}", content);
|
||||
assert!(
|
||||
!content.contains("DefaultSequenceWidth=1920"),
|
||||
"content: {}",
|
||||
content
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1698,7 +1727,7 @@ FlatAfterEmptySection=ok
|
||||
assert_eq!(s.get(Some("g"), "KeyWithEquals").unwrap(), "a=b");
|
||||
// "[]" empties the group, so the key is flat.
|
||||
assert_eq!(s.get(None, "FlatAfterEmptySection").unwrap(), "ok");
|
||||
assert!(matches!(s.get(Some(""), "FlatAfterEmptySection"), Ok(_)));
|
||||
assert!(s.get(Some(""), "FlatAfterEmptySection").is_ok());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -229,9 +229,7 @@ fn parse_xprop_icc_hex(output: &str) -> Option<Vec<u8>> {
|
||||
n_digits += 1;
|
||||
}
|
||||
if n_digits > 0 {
|
||||
if let Ok(byte) =
|
||||
u8::from_str_radix(&rest[digits_start..digits_start + n_digits], 16)
|
||||
{
|
||||
if let Ok(byte) = u8::from_str_radix(&rest[digits_start..digits_start + n_digits], 16) {
|
||||
bytes.push(byte);
|
||||
}
|
||||
}
|
||||
@@ -563,9 +561,8 @@ mod windows {
|
||||
let sub_key = wide_nul(r"Software\Microsoft\Windows\CurrentVersion\VideoSettings");
|
||||
let value_name = wide_nul("EnableAutoColorManagement");
|
||||
let mut key: *mut c_void = std::ptr::null_mut();
|
||||
let status = unsafe {
|
||||
RegOpenKeyExW(HKEY_CURRENT_USER, sub_key.as_ptr(), 0, KEY_READ, &mut key)
|
||||
};
|
||||
let status =
|
||||
unsafe { RegOpenKeyExW(HKEY_CURRENT_USER, sub_key.as_ptr(), 0, KEY_READ, &mut key) };
|
||||
if status != ERROR_SUCCESS {
|
||||
return None;
|
||||
}
|
||||
@@ -628,7 +625,12 @@ mod windows {
|
||||
pub(super) fn display_icc_for(device: &str) -> Option<String> {
|
||||
let name = wide_nul(device);
|
||||
let hdc = unsafe {
|
||||
CreateDCW(name.as_ptr(), name.as_ptr(), std::ptr::null(), std::ptr::null())
|
||||
CreateDCW(
|
||||
name.as_ptr(),
|
||||
name.as_ptr(),
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
)
|
||||
};
|
||||
if hdc.is_null() {
|
||||
return None;
|
||||
@@ -660,7 +662,10 @@ mod windows {
|
||||
sz_device: [0; 32],
|
||||
};
|
||||
let ok = unsafe {
|
||||
GetMonitorInfoW(hmonitor as *const c_void, &mut info as *mut _ as *mut c_void)
|
||||
GetMonitorInfoW(
|
||||
hmonitor as *const c_void,
|
||||
&mut info as *mut _ as *mut c_void,
|
||||
)
|
||||
};
|
||||
if ok == 0 {
|
||||
return None;
|
||||
@@ -864,7 +869,7 @@ mod linux {
|
||||
.lines()
|
||||
.map(str::trim_start)
|
||||
.find(|l| l.starts_with("Filename:"))?;
|
||||
let name = line.splitn(2, ':').nth(1)?.trim();
|
||||
let name = line.split_once(':')?.1.trim();
|
||||
if name.is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -1066,7 +1071,10 @@ Monitors: 1
|
||||
#[test]
|
||||
fn parse_xrandr_monitors_malformed() {
|
||||
assert_eq!(parse_xrandr_monitors(""), Vec::<RandRMonitor>::new());
|
||||
assert_eq!(parse_xrandr_monitors("Monitors: 0\n"), Vec::<RandRMonitor>::new());
|
||||
assert_eq!(
|
||||
parse_xrandr_monitors("Monitors: 0\n"),
|
||||
Vec::<RandRMonitor>::new()
|
||||
);
|
||||
// No `:` separator, a missing geometry, and a geometry with no
|
||||
// outputs are all skipped without panicking.
|
||||
let out = "\
|
||||
|
||||
@@ -51,7 +51,7 @@ fn ends_with_case_insensitive(s: &str, suffix: &str) -> bool {
|
||||
s.as_bytes()[offset..]
|
||||
.iter()
|
||||
.zip(suffix.as_bytes().iter())
|
||||
.all(|(a, b)| a.to_ascii_lowercase() == b.to_ascii_lowercase())
|
||||
.all(|(a, b)| a.eq_ignore_ascii_case(b))
|
||||
}
|
||||
|
||||
/// The filefunctions family (stateless; the handle only exists for C ABI
|
||||
@@ -424,6 +424,33 @@ pub(crate) fn config_location_path() -> Result<PathBuf> {
|
||||
.map(PathBuf::from)
|
||||
}
|
||||
|
||||
/// The default disk cache directory (C++ `DiskManager::
|
||||
/// get_default_disk_cache_path`): `<configuration location>/mediacache`.
|
||||
///
|
||||
/// The `DiskCachePath` config key overrides the location when set (the
|
||||
/// preferences dialog's cache-directory setting); an empty/absent value
|
||||
/// keeps the default.
|
||||
///
|
||||
/// Single-lib unification: this used to live in the oakrender crate's
|
||||
/// `bridge::common` fallback (see `docs/zh/plans/riir/single-lib.md`);
|
||||
/// oaknode and oakrender both call it directly now.
|
||||
pub fn default_disk_cache_path() -> String {
|
||||
// A configured override wins (whitespace-only counts as absent).
|
||||
if let Ok(custom) = crate::configstore::ConfigStore::instance().get(None, "DiskCachePath") {
|
||||
if !custom.trim().is_empty() {
|
||||
return custom;
|
||||
}
|
||||
}
|
||||
Path::new(
|
||||
&FileFunctions::new()
|
||||
.get_configuration_location()
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.join("mediacache")
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -927,30 +954,3 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&b);
|
||||
}
|
||||
}
|
||||
|
||||
/// The default disk cache directory (C++ `DiskManager::
|
||||
/// get_default_disk_cache_path`): `<configuration location>/mediacache`.
|
||||
///
|
||||
/// The `DiskCachePath` config key overrides the location when set (the
|
||||
/// preferences dialog's cache-directory setting); an empty/absent value
|
||||
/// keeps the default.
|
||||
///
|
||||
/// Single-lib unification: this used to live in the oakrender crate's
|
||||
/// `bridge::common` fallback (see `docs/zh/plans/riir/single-lib.md`);
|
||||
/// oaknode and oakrender both call it directly now.
|
||||
pub fn default_disk_cache_path() -> String {
|
||||
// A configured override wins (whitespace-only counts as absent).
|
||||
if let Ok(custom) = crate::configstore::ConfigStore::instance().get(None, "DiskCachePath") {
|
||||
if !custom.trim().is_empty() {
|
||||
return custom;
|
||||
}
|
||||
}
|
||||
Path::new(
|
||||
&FileFunctions::new()
|
||||
.get_configuration_location()
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
.join("mediacache")
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
@@ -482,7 +482,7 @@ mod tests {
|
||||
assert!(cur.get_video_params().unwrap().is_null());
|
||||
|
||||
// Store with a destructor.
|
||||
let p1 = 0x1 as *mut c_void;
|
||||
let p1 = std::ptr::dangling_mut::<c_void>();
|
||||
assert!(cur.set_video_params(p1, Some(count_destroy)).is_ok());
|
||||
assert_eq!(cur.get_video_params().unwrap(), p1);
|
||||
|
||||
|
||||
@@ -271,10 +271,10 @@ impl SubtitleParams {
|
||||
out.push_str("<subtitle");
|
||||
out.push_str(" in=\"");
|
||||
out.push_str(&escape_attribute(&rational_to_string(sub.in_time)));
|
||||
out.push_str("\"");
|
||||
out.push('"');
|
||||
out.push_str(" out=\"");
|
||||
out.push_str(&escape_attribute(&rational_to_string(sub.out_time)));
|
||||
out.push_str("\"");
|
||||
out.push('"');
|
||||
out.push('>');
|
||||
out.push_str(&escape_text(&sub.text));
|
||||
out.push_str("</subtitle>");
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
|
||||
//! Textures and CPU frames.
|
||||
|
||||
use crate::Rational;
|
||||
use crate::PixelFormat;
|
||||
use crate::Rational;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::backend::{BackendKind, GpuContextLike};
|
||||
@@ -385,7 +385,7 @@ mod tests {
|
||||
let pod = f.video_params();
|
||||
assert_eq!(pod.width, 3);
|
||||
assert_eq!(pod.pixel_aspect_num, 2);
|
||||
assert_eq!(f.is_float(), true);
|
||||
assert!(f.is_float());
|
||||
assert_eq!(f.pixel_count(), 6);
|
||||
f.allocate();
|
||||
assert!(!f.data().is_null());
|
||||
|
||||
@@ -703,9 +703,9 @@ impl VideoParams {
|
||||
let divider = squared_divider.sqrt();
|
||||
|
||||
if divider <= DIVIDERS[0] as f64 {
|
||||
return DIVIDERS[0];
|
||||
DIVIDERS[0]
|
||||
} else if divider >= DIVIDERS[7] as f64 {
|
||||
return DIVIDERS[7];
|
||||
DIVIDERS[7]
|
||||
} else {
|
||||
for i in 1..DIVIDERS.len() {
|
||||
let prev = DIVIDERS[i - 1];
|
||||
@@ -716,7 +716,7 @@ impl VideoParams {
|
||||
return if prev_diff < next_diff { prev } else { next };
|
||||
}
|
||||
}
|
||||
return 1; // fallback, unreachable
|
||||
1 // fallback, unreachable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1075,7 +1075,7 @@ fn format_g(value: f64) -> String {
|
||||
e += 1;
|
||||
}
|
||||
|
||||
let body = if PREC > e && e >= -4 {
|
||||
let body = if (-4..PREC).contains(&e) {
|
||||
let decimals = (PREC - 1 - e).max(0) as usize;
|
||||
trim_frac_trailing(format!("{:.*}", decimals, v))
|
||||
} else {
|
||||
|
||||
@@ -537,7 +537,7 @@ impl NodeBehavior for FootageBehavior {
|
||||
if let Some(v) = reader.attribute("ppreset") {
|
||||
if !v.is_empty() {
|
||||
let mut a = [0u8; 32];
|
||||
let n = v.as_bytes().len().min(31);
|
||||
let n = v.len().min(31);
|
||||
a[..n].copy_from_slice(&v.as_bytes()[..n]);
|
||||
custom.preset = a;
|
||||
}
|
||||
@@ -545,7 +545,7 @@ impl NodeBehavior for FootageBehavior {
|
||||
if let Some(v) = reader.attribute("pext") {
|
||||
if !v.is_empty() {
|
||||
let mut a = [0u8; 32];
|
||||
let n = v.as_bytes().len().min(31);
|
||||
let n = v.len().min(31);
|
||||
a[..n].copy_from_slice(&v.as_bytes()[..n]);
|
||||
custom.extension = a;
|
||||
}
|
||||
|
||||
@@ -130,12 +130,7 @@ impl KeyframeTrack {
|
||||
}
|
||||
}
|
||||
|
||||
let (b, a) = match before {
|
||||
Some(pair) => pair,
|
||||
// Unreachable given the front/back guards; C++ logs and falls
|
||||
// through to the standard value.
|
||||
None => return None,
|
||||
};
|
||||
let (b, a) = before?;
|
||||
let before_key = &keys[b];
|
||||
let after_key = &keys[a];
|
||||
|
||||
|
||||
@@ -364,12 +364,11 @@ impl NodeBehavior for BlurFilterNode {
|
||||
let mut can_push_job = true;
|
||||
if radius > 0.0 {
|
||||
// Method-specific considerations.
|
||||
if method == Method::Box as i64 || method == Method::Gaussian as i64 {
|
||||
if !horiz && !vert {
|
||||
if (method == Method::Box as i64 || method == Method::Gaussian as i64)
|
||||
&& !horiz && !vert {
|
||||
// Disable the job if both directions are unchecked.
|
||||
can_push_job = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
can_push_job = false;
|
||||
}
|
||||
@@ -378,11 +377,10 @@ impl NodeBehavior for BlurFilterNode {
|
||||
// all other methods are single-pass (C++ `iterations = 2` only for
|
||||
// the double-pass case).
|
||||
let mut iterations = 1;
|
||||
if method == Method::Box as i64 || method == Method::Gaussian as i64 {
|
||||
if horiz && vert {
|
||||
if (method == Method::Box as i64 || method == Method::Gaussian as i64)
|
||||
&& horiz && vert {
|
||||
iterations = 2;
|
||||
}
|
||||
}
|
||||
|
||||
if can_push_job {
|
||||
// The shader-job box (C++ ShaderJob): the behavior's type id
|
||||
@@ -578,6 +576,17 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(BlurFilterNode))
|
||||
}
|
||||
|
||||
/// Register this node type (C++ `k_blur_filter` in
|
||||
/// `factory.cpp::create_from_factory_index`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.blur",
|
||||
name: "Blur",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -736,14 +745,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Blur");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type (C++ `k_blur_filter` in
|
||||
/// `factory.cpp::create_from_factory_index`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.blur",
|
||||
name: "Blur",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -234,6 +234,16 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(CheckerBoardNode))
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.checkerboard",
|
||||
name: "Checkerboard",
|
||||
categories: &[Category::Generator],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -398,13 +408,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Checkerboard");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.checkerboard",
|
||||
name: "Checkerboard",
|
||||
categories: &[Category::Generator],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -257,6 +257,16 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(ColorBarsNode))
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.colorbars",
|
||||
name: "Color Bars",
|
||||
categories: &[Category::Generator],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -383,13 +393,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Color Bars");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.colorbars",
|
||||
name: "Color Bars",
|
||||
categories: &[Category::Generator],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -198,6 +198,16 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(ColorMatrixNode))
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.colormatrix",
|
||||
name: "Color Matrix",
|
||||
categories: &[Category::Color],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -329,13 +339,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Color Matrix");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.colormatrix",
|
||||
name: "Color Matrix",
|
||||
categories: &[Category::Color],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -518,6 +518,17 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
)
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.cornerpin`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.cornerpin",
|
||||
name: "Corner Pin",
|
||||
categories: &[Category::Distort],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -675,14 +686,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Corner Pin");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.cornerpin`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.cornerpin",
|
||||
name: "Corner Pin",
|
||||
categories: &[Category::Distort],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -410,6 +410,17 @@ fn create_crop_side_input(core: &mut NodeCore, id: &str) {
|
||||
core.add_input(input);
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.crop`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.crop",
|
||||
name: "Crop",
|
||||
categories: &[Category::Distort],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -528,14 +539,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Crop");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.crop`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.crop",
|
||||
name: "Crop",
|
||||
categories: &[Category::Distort],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -196,6 +196,16 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(DilateNode))
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.dilate",
|
||||
name: "Dilate",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -318,13 +328,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Dilate");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.dilate",
|
||||
name: "Dilate",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -244,6 +244,16 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(DirBlurNode))
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.dirblur",
|
||||
name: "Directional Blur",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -394,13 +404,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Directional Blur");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.dirblur",
|
||||
name: "Directional Blur",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -243,6 +243,16 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(DissolveNode))
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.dissolve",
|
||||
name: "Dissolve",
|
||||
categories: &[Category::Math],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -402,13 +412,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Dissolve");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.dissolve",
|
||||
name: "Dissolve",
|
||||
categories: &[Category::Math],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -367,6 +367,17 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(DropShadowFilter))
|
||||
}
|
||||
|
||||
/// Register this node type (C++ `k_drop_shadow_filter` in
|
||||
/// `factory.cpp::create_from_factory_index`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.dropshadow",
|
||||
name: "Drop Shadow",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -485,14 +496,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Drop Shadow");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type (C++ `k_drop_shadow_filter` in
|
||||
/// `factory.cpp::create_from_factory_index`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.dropshadow",
|
||||
name: "Drop Shadow",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -190,6 +190,16 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(EdgeDetectNode))
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.edgedetect",
|
||||
name: "Edge Detect",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -307,13 +317,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Edge Detect");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.edgedetect",
|
||||
name: "Edge Detect",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -196,6 +196,16 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(ErodeNode))
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.erode",
|
||||
name: "Erode",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -318,13 +328,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Erode");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.erode",
|
||||
name: "Erode",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -199,6 +199,17 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(FlipDistortNode))
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.flip`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.flip",
|
||||
name: "Flip",
|
||||
categories: &[Category::Distort],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -315,14 +326,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Flip");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.flip`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.flip",
|
||||
name: "Flip",
|
||||
categories: &[Category::Distort],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -219,6 +219,16 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(KeyMixNode))
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.keymix",
|
||||
name: "KeyMix",
|
||||
categories: &[Category::Math],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -357,13 +367,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "KeyMix");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.keymix",
|
||||
name: "KeyMix",
|
||||
categories: &[Category::Math],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -406,6 +406,17 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(MatrixGenerator))
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.ortho`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.ortho",
|
||||
name: "Orthographic Matrix",
|
||||
categories: &[Category::Generator, Category::Math],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -587,14 +598,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Orthographic Matrix");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.ortho`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.ortho",
|
||||
name: "Orthographic Matrix",
|
||||
categories: &[Category::Generator, Category::Math],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -208,6 +208,17 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(MergeNode))
|
||||
}
|
||||
|
||||
/// Register this node type (C++ `k_merge_node` in
|
||||
/// `factory.cpp::create_from_factory_index`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.merge",
|
||||
name: "Merge",
|
||||
categories: &[Category::Math],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -306,14 +317,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Merge");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type (C++ `k_merge_node` in
|
||||
/// `factory.cpp::create_from_factory_index`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.merge",
|
||||
name: "Merge",
|
||||
categories: &[Category::Math],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -189,6 +189,16 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(MirrorNode))
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.mirror",
|
||||
name: "Mirror",
|
||||
categories: &[Category::Distort],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -327,13 +337,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Mirror");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.mirror",
|
||||
name: "Mirror",
|
||||
categories: &[Category::Distort],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -214,6 +214,17 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(MosaicFilterNode))
|
||||
}
|
||||
|
||||
/// Register this node type (C++ `k_mosaic_filter` in
|
||||
/// `factory.cpp::create_from_factory_index`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.mosaicfilter",
|
||||
name: "Mosaic",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -312,14 +323,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Mosaic");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type (C++ `k_mosaic_filter` in
|
||||
/// `factory.cpp::create_from_factory_index`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.mosaicfilter",
|
||||
name: "Mosaic",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -219,6 +219,17 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(NoiseGeneratorNode))
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.noise`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.noise",
|
||||
name: "Noise",
|
||||
categories: &[Category::Generator],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -310,14 +321,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Noise");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.noise`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.noise",
|
||||
name: "Noise",
|
||||
categories: &[Category::Generator],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -244,6 +244,17 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
)
|
||||
}
|
||||
|
||||
/// Register this node type (C++ `k_opacity_effect` in
|
||||
/// `factory.cpp::create_from_factory_index`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.opacity",
|
||||
name: "Opacity",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -396,14 +407,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Opacity");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type (C++ `k_opacity_effect` in
|
||||
/// `factory.cpp::create_from_factory_index`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.opacity",
|
||||
name: "Opacity",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -196,6 +196,17 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(PanNode))
|
||||
}
|
||||
|
||||
/// Register this node type (C++ `k_audio_panning` in
|
||||
/// `factory.cpp::create_from_factory_index`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.pan",
|
||||
name: "Pan",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -338,14 +349,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Pan");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type (C++ `k_audio_panning` in
|
||||
/// `factory.cpp::create_from_factory_index`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.pan",
|
||||
name: "Pan",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -546,7 +546,7 @@ mod tests {
|
||||
sample_count: 3,
|
||||
// Planar layout: channel 0 plane [1, 2, 3], channel 1 plane
|
||||
// [4, 5, 6].
|
||||
data: vec![1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0]
|
||||
data: [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0]
|
||||
.iter()
|
||||
.flat_map(|f| f.to_le_bytes())
|
||||
.collect(),
|
||||
@@ -580,7 +580,7 @@ mod tests {
|
||||
format: oak_core::SampleFormat::F32,
|
||||
channels: 1,
|
||||
sample_count: 2,
|
||||
data: vec![1.0f32, 2.0]
|
||||
data: [1.0f32, 2.0]
|
||||
.iter()
|
||||
.flat_map(|f| f.to_le_bytes())
|
||||
.collect(),
|
||||
|
||||
@@ -112,7 +112,7 @@ pub fn point_array(core: &NodeCore, inputs: &crate::value::NodeValueRow, time: o
|
||||
.inputs
|
||||
.iter()
|
||||
.find(|i| i.id == POINTS_INPUT)
|
||||
.map(|i| (i.array_size as usize).min(64))
|
||||
.map(|i| i.array_size.min(64))
|
||||
.unwrap_or(5);
|
||||
let mut points: Vec<[f64; 4]> = Vec::new();
|
||||
for i in 0..size {
|
||||
@@ -380,6 +380,17 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(PolygonGenerator))
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.polygon`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.polygon",
|
||||
name: "Polygon",
|
||||
categories: &[Category::Generator],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -507,14 +518,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Polygon");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.polygon`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.polygon",
|
||||
name: "Polygon",
|
||||
categories: &[Category::Generator],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -193,6 +193,16 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(PositionNode))
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.position",
|
||||
name: "Position",
|
||||
categories: &[Category::Distort],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -307,13 +317,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Position");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.position",
|
||||
name: "Position",
|
||||
categories: &[Category::Distort],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -233,6 +233,16 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(PremultiplyNode))
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.premult",
|
||||
name: "Premultiply",
|
||||
categories: &[Category::Math],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -372,13 +382,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Premultiply");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.premult",
|
||||
name: "Premultiply",
|
||||
categories: &[Category::Math],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -233,6 +233,16 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(RampNode))
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.ramp",
|
||||
name: "Ramp",
|
||||
categories: &[Category::Generator],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -394,13 +404,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Ramp");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.ramp",
|
||||
name: "Ramp",
|
||||
categories: &[Category::Generator],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -299,6 +299,17 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(RippleDistortNode { gizmo }))
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.ripple`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.ripple",
|
||||
name: "Ripple",
|
||||
categories: &[Category::Distort],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -404,14 +415,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Ripple");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.ripple`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.ripple",
|
||||
name: "Ripple",
|
||||
categories: &[Category::Distort],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -359,6 +359,17 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(ShapeNode))
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.shape`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.shape",
|
||||
name: "Shape",
|
||||
categories: &[Category::Generator],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -516,14 +527,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Shape");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.shape`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.shape",
|
||||
name: "Shape",
|
||||
categories: &[Category::Generator],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -209,6 +209,16 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(SharpenNode))
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.sharpen",
|
||||
name: "Sharpen",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -327,13 +337,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Sharpen");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.sharpen",
|
||||
name: "Sharpen",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -139,6 +139,17 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(SolidGenerator))
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.solidgenerator`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.solidgenerator",
|
||||
name: "Solid",
|
||||
categories: &[Category::Generator],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -209,14 +220,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Solid");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.solidgenerator`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.solidgenerator",
|
||||
name: "Solid",
|
||||
categories: &[Category::Generator],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -295,6 +295,17 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(StrokeFilterNode))
|
||||
}
|
||||
|
||||
/// Register this node type (C++ `k_stroke_filter` in
|
||||
/// `factory.cpp::create_from_factory_index`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.stroke",
|
||||
name: "Stroke",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -413,14 +424,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Stroke");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type (C++ `k_stroke_filter` in
|
||||
/// `factory.cpp::create_from_factory_index`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.stroke",
|
||||
name: "Stroke",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -274,6 +274,17 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(SwirlDistortNode { gizmo }))
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.swirl`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.swirl",
|
||||
name: "Swirl",
|
||||
categories: &[Category::Distort],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -390,14 +401,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Swirl");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.swirl`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.swirl",
|
||||
name: "Swirl",
|
||||
categories: &[Category::Distort],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ impl TextGeneratorV1 {
|
||||
let text = row
|
||||
.get(TEXT_INPUT)
|
||||
.map(to_text)
|
||||
.unwrap_or_else(|| String::new());
|
||||
.unwrap_or_default();
|
||||
let html = matches!(row.get(HTML_INPUT), Some(NodeValue::Boolean(true)));
|
||||
let mut mode = TextLayoutMode::PlainText;
|
||||
let text = if html {
|
||||
@@ -100,7 +100,7 @@ impl TextGeneratorV1 {
|
||||
TextLayoutRequest {
|
||||
text,
|
||||
mode,
|
||||
font_family: row.get(FONT_INPUT).map(to_text).unwrap_or_else(String::new),
|
||||
font_family: row.get(FONT_INPUT).map(to_text).unwrap_or_default(),
|
||||
font_size_pt: row
|
||||
.get(FONT_SIZE_INPUT)
|
||||
.map(NodeValue::to_double)
|
||||
|
||||
@@ -92,7 +92,7 @@ impl TextGeneratorV2 {
|
||||
let text = row
|
||||
.get(TEXT_INPUT)
|
||||
.map(to_text)
|
||||
.unwrap_or_else(|| String::new());
|
||||
.unwrap_or_default();
|
||||
let html = matches!(row.get(HTML_INPUT), Some(NodeValue::Boolean(true)));
|
||||
let mut mode = TextLayoutMode::PlainText;
|
||||
let text = if html {
|
||||
@@ -111,7 +111,7 @@ impl TextGeneratorV2 {
|
||||
TextLayoutRequest {
|
||||
text,
|
||||
mode,
|
||||
font_family: row.get(FONT_INPUT).map(to_text).unwrap_or_else(String::new),
|
||||
font_family: row.get(FONT_INPUT).map(to_text).unwrap_or_default(),
|
||||
font_size_pt: row
|
||||
.get(FONT_SIZE_INPUT)
|
||||
.map(NodeValue::to_double)
|
||||
|
||||
@@ -760,12 +760,12 @@ impl TextGeneratorV3 {
|
||||
text: row
|
||||
.get(PLAIN_TEXT_INPUT)
|
||||
.map(to_text)
|
||||
.unwrap_or_else(String::new),
|
||||
.unwrap_or_default(),
|
||||
mode: TextLayoutMode::PlainText,
|
||||
font_family: row
|
||||
.get(FONT_FAMILY_INPUT)
|
||||
.map(to_text)
|
||||
.unwrap_or_else(String::new),
|
||||
.unwrap_or_default(),
|
||||
font_size_pt: row
|
||||
.get(FONT_SIZE_INPUT)
|
||||
.map(|v| v.to_double())
|
||||
@@ -776,7 +776,7 @@ impl TextGeneratorV3 {
|
||||
}
|
||||
} else {
|
||||
TextLayoutRequest {
|
||||
text: row.get(TEXT_INPUT).map(to_text).unwrap_or_else(String::new),
|
||||
text: row.get(TEXT_INPUT).map(to_text).unwrap_or_default(),
|
||||
mode: TextLayoutMode::OliveHtml,
|
||||
font_family: String::new(),
|
||||
font_size_pt: 0.0,
|
||||
@@ -2257,7 +2257,7 @@ mod tests {
|
||||
else {
|
||||
panic!("expected a CPU coverage frame");
|
||||
};
|
||||
let stride = frame.linesize_bytes() as usize;
|
||||
let stride = frame.linesize_bytes();
|
||||
let at = y * stride + x * 16;
|
||||
let mut out = [0f32; 4];
|
||||
for (c, v) in out.iter_mut().enumerate() {
|
||||
|
||||
@@ -381,6 +381,17 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(TileDistortNode { gizmo }))
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.tile`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.tile",
|
||||
name: "Tile",
|
||||
categories: &[Category::Distort],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -483,14 +494,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Tile");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.tile`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.tile",
|
||||
name: "Tile",
|
||||
categories: &[Category::Distort],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -246,19 +246,19 @@ impl TransformDistortNode {
|
||||
|
||||
let scale_by_x = sequence_res.0 / texture_res.0;
|
||||
let scale_by_y = sequence_res.1 / texture_res.1;
|
||||
let autoscale_val;
|
||||
|
||||
|
||||
if (autoscale_type == AutoScaleType::Fit) == (sequence_real_ar > footage_real_ar) {
|
||||
let autoscale_val = if (autoscale_type == AutoScaleType::Fit) == (sequence_real_ar > footage_real_ar) {
|
||||
// Scale by height. Either the sequence is wider than
|
||||
// the footage or we're using fill and cutting off the
|
||||
// sides.
|
||||
autoscale_val = scale_by_y;
|
||||
scale_by_y
|
||||
} else {
|
||||
// Scale by width. Either the footage is wider than the
|
||||
// sequence or we're using fill and cutting off the top
|
||||
// and bottom.
|
||||
autoscale_val = scale_by_x;
|
||||
}
|
||||
scale_by_x
|
||||
};
|
||||
|
||||
adjusted_matrix =
|
||||
super::matrix::matrix_scale(adjusted_matrix, autoscale_val, autoscale_val, 1.0);
|
||||
@@ -671,6 +671,17 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
)
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.transform`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.transform",
|
||||
name: "Transform",
|
||||
categories: &[Category::Distort],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -960,14 +971,3 @@ mod tests {
|
||||
assert_eq!(dup.short_name(), "Transform");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.transform`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.transform",
|
||||
name: "Transform",
|
||||
categories: &[Category::Distort],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -478,6 +478,16 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(TransitionFxNode))
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.transitionfx",
|
||||
name: "Transition FX",
|
||||
categories: &[Category::Effect],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -754,13 +764,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Transition FX");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.transitionfx",
|
||||
name: "Transition FX",
|
||||
categories: &[Category::Effect],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -403,6 +403,16 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(TransitionNode))
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.transition",
|
||||
name: "Transition",
|
||||
categories: &[Category::Timeline],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -596,13 +606,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Transition");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.transition",
|
||||
name: "Transition",
|
||||
categories: &[Category::Timeline],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -213,6 +213,17 @@ pub const OPERATION_NAMES: [&str; 9] = [
|
||||
"Hyperbolic Tangent",
|
||||
];
|
||||
|
||||
/// Register this node type (C++ `k_trigonometry_node` in
|
||||
/// `factory.cpp::create_from_factory_index`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.trigonometry",
|
||||
name: "Trigonometry",
|
||||
categories: &[Category::Math],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -363,14 +374,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Trigonometry");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type (C++ `k_trigonometry_node` in
|
||||
/// `factory.cpp::create_from_factory_index`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.trigonometry",
|
||||
name: "Trigonometry",
|
||||
categories: &[Category::Math],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -242,6 +242,16 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(UnpremultiplyNode))
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.unpremult",
|
||||
name: "Unpremultiply",
|
||||
categories: &[Category::Math],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -370,13 +380,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Unpremultiply");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type.
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.unpremult",
|
||||
name: "Unpremultiply",
|
||||
categories: &[Category::Math],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -171,6 +171,17 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(ValueNode))
|
||||
}
|
||||
|
||||
/// Register this node type (C++ `k_value_node` in
|
||||
/// `factory.cpp::create_from_factory_index`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.value",
|
||||
name: "Value",
|
||||
categories: &[Category::Generator],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -262,14 +273,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Value");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type (C++ `k_value_node` in
|
||||
/// `factory.cpp::create_from_factory_index`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.value",
|
||||
name: "Value",
|
||||
categories: &[Category::Generator],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -177,6 +177,17 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(VolumeNode))
|
||||
}
|
||||
|
||||
/// Register this node type (C++ `k_audio_volume` in
|
||||
/// `factory.cpp::create_from_factory_index`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.volume",
|
||||
name: "Volume",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -331,14 +342,3 @@ mod tests {
|
||||
assert_eq!(dup.type_id(), "org.olivevideoeditor.Olive.volume");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type (C++ `k_audio_volume` in
|
||||
/// `factory.cpp::create_from_factory_index`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.volume",
|
||||
name: "Volume",
|
||||
categories: &[Category::Filter],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -241,6 +241,17 @@ pub fn create() -> (NodeCore, Box<dyn NodeBehavior>) {
|
||||
(core, Box::new(WaveDistortNode))
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.wave`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.wave",
|
||||
name: "Wave",
|
||||
categories: &[Category::Distort],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -341,14 +352,3 @@ mod tests {
|
||||
assert_eq!(dup.name(), "Wave");
|
||||
}
|
||||
}
|
||||
|
||||
/// Register this node type (C++ factory entry for
|
||||
/// `org.olivevideoeditor.Olive.wave`).
|
||||
pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
meta.push(NodeMeta {
|
||||
type_id: "org.olivevideoeditor.Olive.wave",
|
||||
name: "Wave",
|
||||
categories: &[Category::Distort],
|
||||
create,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ impl WhiteBalanceNode {
|
||||
// exposure, then let tint move along the green-magenta axis.
|
||||
let tint_gain = (1.0 + tint).clamp(0.0, 2.0);
|
||||
|
||||
[red / green, green / green * tint_gain, blue / green]
|
||||
[red / green, tint_gain, blue / green]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,16 +189,18 @@ impl NodeBehavior for WhiteBalanceNode {
|
||||
params.insert(GAIN_INPUT.to_string(), crate::value::NodeValue::Vec3(gain));
|
||||
table.push(
|
||||
crate::value::ValueType::Texture,
|
||||
crate::value::NodeValue::Texture(crate::handle::make_owned(Job::ShaderJob(ShaderJobPayload {
|
||||
node_id: crate::id::NodeId::INVALID,
|
||||
time,
|
||||
iterations: 1,
|
||||
type_id: self.type_id().to_string(),
|
||||
shader_id: String::new(),
|
||||
effect_input: core.effect_input.clone(),
|
||||
params,
|
||||
iterative_input: String::new(),
|
||||
}))),
|
||||
crate::value::NodeValue::Texture(crate::handle::make_owned(Job::ShaderJob(
|
||||
ShaderJobPayload {
|
||||
node_id: crate::id::NodeId::INVALID,
|
||||
time,
|
||||
iterations: 1,
|
||||
type_id: self.type_id().to_string(),
|
||||
shader_id: String::new(),
|
||||
effect_input: core.effect_input.clone(),
|
||||
params,
|
||||
iterative_input: String::new(),
|
||||
},
|
||||
))),
|
||||
None,
|
||||
);
|
||||
}
|
||||
@@ -270,11 +272,11 @@ pub fn register(meta: &mut Vec<NodeMeta>) {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::value::{NodeValue, NodeValueTable, ValueType};
|
||||
use oak_core::Rational;
|
||||
use super::*;
|
||||
use crate::value::{NodeValue, NodeValueTable, ValueType};
|
||||
use oak_core::Rational;
|
||||
|
||||
#[test]
|
||||
#[test]
|
||||
fn input_names() {
|
||||
let n = WhiteBalanceNode;
|
||||
assert_eq!(n.input_name(TEXTURE_INPUT), "Input");
|
||||
@@ -391,8 +393,7 @@ mod tests {
|
||||
panic!("expected a texture-typed value");
|
||||
};
|
||||
let payload =
|
||||
unsafe { crate::jobs::shader_job(handle) }
|
||||
.expect("payload boxed behind the handle");
|
||||
unsafe { crate::jobs::shader_job(handle) }.expect("payload boxed behind the handle");
|
||||
assert_eq!(payload.type_id, "org.olivevideoeditor.Olive.whitebalance");
|
||||
assert_eq!(payload.shader_id, "");
|
||||
assert_eq!(payload.iterations, 1);
|
||||
|
||||
@@ -252,9 +252,7 @@ pub fn set_value_at_time_command(
|
||||
.and_then(|e| e.core.keyframe_track(&input, element))
|
||||
.and_then(|t| {
|
||||
t.keys()
|
||||
.iter()
|
||||
.filter(|k| k.time <= time)
|
||||
.next_back()
|
||||
.iter().rfind(|k| k.time <= time)
|
||||
.map(|k| k.interpolation)
|
||||
})
|
||||
.unwrap_or(Interpolation::Linear);
|
||||
|
||||
@@ -347,8 +347,8 @@ impl Project {
|
||||
return self.custom_cache_path.clone();
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
if !self.filename.is_empty() {
|
||||
1
|
||||
if !self.filename.is_empty() => {
|
||||
let dir = std::path::Path::new(&self.filename)
|
||||
.parent()
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
@@ -357,7 +357,6 @@ impl Project {
|
||||
return format!("{}/cache", dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// Default location: the shared disk-cache directory (single-lib:
|
||||
|
||||
@@ -690,7 +690,7 @@ impl Clone for NodeValue {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
NodeValue::Texture(h) => {
|
||||
let h2 = h.clone();
|
||||
let h2 = *h;
|
||||
if let Some(f) = h2.addref {
|
||||
// Safety: `h2` is a valid handle; addref only touches
|
||||
// the refcount.
|
||||
|
||||
@@ -163,10 +163,7 @@ fn golden_project_with_footage_loads() {
|
||||
|
||||
// The root folder (from the settings "root" key) holds the bin.
|
||||
let root = p.graph.get(p.root).expect("root folder resolves");
|
||||
assert_eq!(
|
||||
root.behavior.type_id(),
|
||||
"org.olivevideoeditor.Olive.folder"
|
||||
);
|
||||
assert_eq!(root.behavior.type_id(), "org.olivevideoeditor.Olive.folder");
|
||||
let folder = root
|
||||
.behavior
|
||||
.as_any()
|
||||
@@ -507,10 +504,10 @@ fn build_full_project() -> std::sync::Arc<std::sync::Mutex<oak_node::project::Pr
|
||||
}
|
||||
|
||||
/// Borrowed track list of a node.
|
||||
fn list_of<'a>(
|
||||
p: &'a oak_node::project::Project,
|
||||
fn list_of(
|
||||
p: &oak_node::project::Project,
|
||||
id: oak_node::id::NodeId,
|
||||
) -> &'a oak_node::track::TrackListBehavior {
|
||||
) -> &oak_node::track::TrackListBehavior {
|
||||
p.graph
|
||||
.get(id)
|
||||
.unwrap()
|
||||
@@ -521,7 +518,10 @@ fn list_of<'a>(
|
||||
}
|
||||
|
||||
/// Field-by-field comparison of the round-tripped full project.
|
||||
fn assert_full_roundtrip_fields(orig: &oak_node::project::Project, loaded: &oak_node::project::Project) {
|
||||
fn assert_full_roundtrip_fields(
|
||||
orig: &oak_node::project::Project,
|
||||
loaded: &oak_node::project::Project,
|
||||
) {
|
||||
use oak_node::block::ClipBlockBehavior;
|
||||
use oak_node::folder::FolderBehavior;
|
||||
use oak_node::footage::FootageBehavior;
|
||||
@@ -533,7 +533,11 @@ fn assert_full_roundtrip_fields(orig: &oak_node::project::Project, loaded: &oak_
|
||||
assert_eq!(loaded.settings, orig.settings, "settings");
|
||||
|
||||
// The graph keeps its node count, types and edge count.
|
||||
assert_eq!(loaded.graph.node_count(), orig.graph.node_count(), "node count");
|
||||
assert_eq!(
|
||||
loaded.graph.node_count(),
|
||||
orig.graph.node_count(),
|
||||
"node count"
|
||||
);
|
||||
let o_types: Vec<&str> = orig
|
||||
.graph
|
||||
.node_ids()
|
||||
@@ -607,7 +611,10 @@ fn assert_full_roundtrip_fields(orig: &oak_node::project::Project, loaded: &oak_
|
||||
l_f.proxy_video_stream_index, o_f.proxy_video_stream_index,
|
||||
"proxy stream"
|
||||
);
|
||||
assert_eq!(l_f.proxy_preset_version, o_f.proxy_preset_version, "proxy preset");
|
||||
assert_eq!(
|
||||
l_f.proxy_preset_version, o_f.proxy_preset_version,
|
||||
"proxy preset"
|
||||
);
|
||||
assert_eq!(l_f.streams.len(), o_f.streams.len(), "stream count");
|
||||
for (ls, os) in l_f.streams.iter().zip(&o_f.streams) {
|
||||
assert_eq!(ls.index, os.index, "stream index");
|
||||
@@ -644,7 +651,11 @@ fn assert_full_roundtrip_fields(orig: &oak_node::project::Project, loaded: &oak_
|
||||
orig.graph.get(o_seq).unwrap().core.override_color,
|
||||
"sequence color"
|
||||
);
|
||||
assert_eq!(l_s.track_lists.len(), o_s.track_lists.len(), "track list count");
|
||||
assert_eq!(
|
||||
l_s.track_lists.len(),
|
||||
o_s.track_lists.len(),
|
||||
"track list count"
|
||||
);
|
||||
let (o_vlist, l_vlist) = (o_s.track_lists[0], l_s.track_lists[0]);
|
||||
let (o_alist, l_alist) = (o_s.track_lists[1], l_s.track_lists[1]);
|
||||
|
||||
@@ -653,14 +664,22 @@ fn assert_full_roundtrip_fields(orig: &oak_node::project::Project, loaded: &oak_
|
||||
assert_eq!(l_vl.kind, o_vl.kind, "video list kind");
|
||||
assert_eq!(l_vl.array_base, o_vl.array_base, "video list base");
|
||||
assert_eq!(l_vl.sequence, Some(l_seq), "video list sequence backref");
|
||||
assert_eq!(l_vl.tracks.len(), o_vl.tracks.len(), "video list track count");
|
||||
assert_eq!(
|
||||
l_vl.tracks.len(),
|
||||
o_vl.tracks.len(),
|
||||
"video list track count"
|
||||
);
|
||||
let (o_vtrack, l_vtrack) = (o_vl.tracks[0], l_vl.tracks[0]);
|
||||
let o_al = list_of(orig, o_alist);
|
||||
let l_al = list_of(loaded, l_alist);
|
||||
assert_eq!(l_al.kind, o_al.kind, "audio list kind");
|
||||
assert_eq!(l_al.array_base, o_al.array_base, "audio list base");
|
||||
assert_eq!(l_al.sequence, Some(l_seq), "audio list sequence backref");
|
||||
assert_eq!(l_al.tracks.len(), o_al.tracks.len(), "audio list track count");
|
||||
assert_eq!(
|
||||
l_al.tracks.len(),
|
||||
o_al.tracks.len(),
|
||||
"audio list track count"
|
||||
);
|
||||
let (o_atrack, l_atrack) = (o_al.tracks[0], l_al.tracks[0]);
|
||||
|
||||
// Video track: kind/blocks/muted/locked/height/index/backref.
|
||||
@@ -686,7 +705,11 @@ fn assert_full_roundtrip_fields(orig: &oak_node::project::Project, loaded: &oak_
|
||||
assert_eq!(l_t.height, o_t.height, "video track height");
|
||||
assert_eq!(l_t.index, o_t.index, "video track index");
|
||||
assert_eq!(l_t.track_list, Some(l_vlist), "video track list backref");
|
||||
assert_eq!(l_t.blocks.len(), o_t.blocks.len(), "video track block count");
|
||||
assert_eq!(
|
||||
l_t.blocks.len(),
|
||||
o_t.blocks.len(),
|
||||
"video track block count"
|
||||
);
|
||||
let o_clip1 = o_t.blocks[0];
|
||||
let o_gap1 = o_t.blocks[1];
|
||||
let o_clip2 = o_t.blocks[2];
|
||||
@@ -816,7 +839,11 @@ fn assert_full_roundtrip_fields(orig: &oak_node::project::Project, loaded: &oak_
|
||||
assert_eq!(l_at.kind, TrackType::Audio, "audio track kind");
|
||||
assert_eq!(l_at.locked, o_at.locked, "audio track locked");
|
||||
assert_eq!(l_at.track_list, Some(l_alist), "audio track list backref");
|
||||
assert_eq!(l_at.blocks.len(), o_at.blocks.len(), "audio track block count");
|
||||
assert_eq!(
|
||||
l_at.blocks.len(),
|
||||
o_at.blocks.len(),
|
||||
"audio track block count"
|
||||
);
|
||||
let o_c3 = orig
|
||||
.graph
|
||||
.get(o_at.blocks[0])
|
||||
@@ -956,7 +983,11 @@ fn multicam_node_round_trip_preserves_current_in() {
|
||||
// serializes combo indices as Int — `string_to_value` maps
|
||||
// `ValueType::Combo` to `NodeValue::Int`).
|
||||
assert_eq!(
|
||||
l.graph.get(loaded_mc).unwrap().core.standard_value(CURRENT_INPUT, -1),
|
||||
l.graph
|
||||
.get(loaded_mc)
|
||||
.unwrap()
|
||||
.core
|
||||
.standard_value(CURRENT_INPUT, -1),
|
||||
NodeValue::Int(2),
|
||||
"current_in survives the round-trip"
|
||||
);
|
||||
@@ -1004,8 +1035,11 @@ fn dynamic_plugin_node_round_trips_across_load() {
|
||||
// from its OFX params).
|
||||
let probe_core = || {
|
||||
let mut core = NodeCore::new();
|
||||
core.inputs
|
||||
.push(Input::new(INPUT_ID, ValueType::Float, NodeValue::Float(0.0)));
|
||||
core.inputs.push(Input::new(
|
||||
INPUT_ID,
|
||||
ValueType::Float,
|
||||
NodeValue::Float(0.0),
|
||||
));
|
||||
core
|
||||
};
|
||||
{
|
||||
@@ -1049,7 +1083,11 @@ fn dynamic_plugin_node_round_trips_across_load() {
|
||||
.find(|id| l.graph.get(*id).map(|e| e.behavior.type_id()) == Some(TYPE_ID))
|
||||
.expect("loaded project has the dynamic node");
|
||||
assert_eq!(
|
||||
l.graph.get(probe).unwrap().core.standard_value(INPUT_ID, -1),
|
||||
l.graph
|
||||
.get(probe)
|
||||
.unwrap()
|
||||
.core
|
||||
.standard_value(INPUT_ID, -1),
|
||||
NodeValue::Float(2.5),
|
||||
"the standard value survives the rebuild"
|
||||
);
|
||||
@@ -1077,7 +1115,10 @@ fn multicam_clip_round_trip_preserves_wiring() {
|
||||
let seq_id = p.graph.add_node(core, behavior);
|
||||
let (core, behavior) = TrackListBehavior::create();
|
||||
let list_id = p.graph.add_node(core, behavior);
|
||||
let (core, behavior) = (NodeCore::new(), Box::new(TrackBehavior::new(TrackType::Video)));
|
||||
let (core, behavior) = (
|
||||
NodeCore::new(),
|
||||
Box::new(TrackBehavior::new(TrackType::Video)),
|
||||
);
|
||||
let track_id = p.graph.add_node(core, behavior);
|
||||
{
|
||||
let seq = p.graph.get_mut(seq_id).unwrap();
|
||||
@@ -1132,18 +1173,13 @@ fn multicam_clip_round_trip_preserves_wiring() {
|
||||
// The multicam node routed between the sequence and the clip.
|
||||
let (core, behavior) = create();
|
||||
let mc_id = p.graph.add_node(core, behavior);
|
||||
p.graph
|
||||
.connect(mc_id, clip_id, "tex_in", -1)
|
||||
.unwrap();
|
||||
p.graph.connect(mc_id, clip_id, "tex_in", -1).unwrap();
|
||||
p.graph.connect(seq_id, mc_id, SEQUENCE_INPUT, -1).unwrap();
|
||||
let mc = p.graph.get_mut(mc_id).unwrap();
|
||||
mc.core
|
||||
.set_standard_value(CURRENT_INPUT, -1, NodeValue::Combo(2));
|
||||
mc.core.set_standard_value(
|
||||
SEQUENCE_TYPE_INPUT,
|
||||
-1,
|
||||
NodeValue::Combo(0),
|
||||
);
|
||||
mc.core
|
||||
.set_standard_value(SEQUENCE_TYPE_INPUT, -1, NodeValue::Combo(0));
|
||||
(seq_id, clip_id, mc_id)
|
||||
};
|
||||
let _ = (seq_id, clip_id, mc_id);
|
||||
@@ -1165,12 +1201,20 @@ fn multicam_clip_round_trip_preserves_wiring() {
|
||||
})
|
||||
.expect("loaded project has a MultiCamNode");
|
||||
assert_eq!(
|
||||
l.graph.get(loaded_mc).unwrap().core.standard_value(CURRENT_INPUT, -1),
|
||||
l.graph
|
||||
.get(loaded_mc)
|
||||
.unwrap()
|
||||
.core
|
||||
.standard_value(CURRENT_INPUT, -1),
|
||||
NodeValue::Int(2),
|
||||
"current_in survives"
|
||||
);
|
||||
assert_eq!(
|
||||
l.graph.get(loaded_mc).unwrap().core.standard_value(SEQUENCE_TYPE_INPUT, -1),
|
||||
l.graph
|
||||
.get(loaded_mc)
|
||||
.unwrap()
|
||||
.core
|
||||
.standard_value(SEQUENCE_TYPE_INPUT, -1),
|
||||
NodeValue::Int(0),
|
||||
"sequence_type_in survives"
|
||||
);
|
||||
@@ -1332,5 +1376,8 @@ fn legacy_project_without_endpoints_migrates_on_load() {
|
||||
let r = reloaded.lock().unwrap();
|
||||
assert_eq!(r.graph.endpoints(), Some((li, lo)));
|
||||
assert_eq!(r.graph.node_count(), legacy_count + 2);
|
||||
assert_eq!(r.graph.connected_output(lo, GRAPH_OUTPUT_INPUT, -1), Some(li));
|
||||
assert_eq!(
|
||||
r.graph.connected_output(lo, GRAPH_OUTPUT_INPUT, -1),
|
||||
Some(li)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ fn texture_value_drop_releases() {
|
||||
assert_eq!(refs(&h), 2);
|
||||
|
||||
{
|
||||
let v = NodeValue::Texture(h.clone());
|
||||
let v = NodeValue::Texture(h);
|
||||
assert_eq!(refs(&h), 2, "no extra reference taken on clone");
|
||||
drop(v); // must release the payload's reference
|
||||
assert_eq!(refs(&h), 1);
|
||||
@@ -97,7 +97,7 @@ fn texture_value_clone_addrefs() {
|
||||
assert_eq!(refs(&h), 1);
|
||||
|
||||
{
|
||||
let a = NodeValue::Texture(h.clone());
|
||||
let a = NodeValue::Texture(h);
|
||||
assert_eq!(refs(&h), 1, "construction takes the caller's reference");
|
||||
{
|
||||
let b = a.clone();
|
||||
|
||||
@@ -1455,7 +1455,7 @@ mod tests {
|
||||
assert!(FcpxmlError::UnsupportedVersion("x".into())
|
||||
.to_string()
|
||||
.contains("version error"));
|
||||
let io = FcpxmlError::Io(std::io::Error::new(std::io::ErrorKind::Other, "io"));
|
||||
let io = FcpxmlError::Io(std::io::Error::other("io"));
|
||||
assert!(io.to_string().contains("file error"));
|
||||
assert!(io.source().is_some());
|
||||
assert!(FcpxmlError::Xml("x".into()).source().is_none());
|
||||
@@ -1577,6 +1577,6 @@ mod tests {
|
||||
#[test]
|
||||
fn file_io_errors() {
|
||||
assert!(from_fcpxml_file("/nonexistent/oakotio-test.fcpxml").is_err());
|
||||
assert!(to_fcpxml_file(&[], &std::env::temp_dir()).is_err());
|
||||
assert!(to_fcpxml_file(&[], std::env::temp_dir()).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,9 @@
|
||||
//! version) and lenient handling of unknown elements.
|
||||
|
||||
use oak_core::Rational;
|
||||
use oak_otio::{Clip, Composable, ExternalReference, MediaReference, RationalTime, Timeline, Track};
|
||||
use oak_otio::{
|
||||
Clip, Composable, ExternalReference, MediaReference, RationalTime, Timeline, Track,
|
||||
};
|
||||
|
||||
/// The NTSC video rate 30000/1001 (~29.97 fps) as an exact rational.
|
||||
const NTSC_RATE: f64 = 30000.0 / 1001.0;
|
||||
@@ -189,7 +191,7 @@ fn parse_synthetic_document() {
|
||||
#[test]
|
||||
fn model_round_trips_through_fcpxml() {
|
||||
let original = parse_synthetic();
|
||||
let xml = oak_otio::to_fcpxml_string(&[original.clone()]).expect("export fcpxml");
|
||||
let xml = oak_otio::to_fcpxml_string(std::slice::from_ref(&original)).expect("export fcpxml");
|
||||
let reparsed = oak_otio::from_fcpxml_string(&xml).expect("reparse exported fcpxml");
|
||||
assert_eq!(reparsed.len(), 1);
|
||||
assert_eq!(reparsed[0], original, "model survives fcpxml round trip");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user