#614: Head, Head and Tail and Continuous video thumbnails in the Timeline
Problem: a video clip only ever showed its first frame. Show Video
Thumbnails was a plain on/off switch, so a wide clip at high zoom stayed a
flat colour after its head.
Fix: the wrench menu keeps Show Video Thumbnails and adds Head (the
default, as before), Head and Tail and Continuous under it. Continuous
tiles aspect-correct frames across the clip on a grid anchored at its head,
only where the panel shows them (at most 256 per clip), each frame the
source time under the tile's left edge (speed, reverse, frame holds,
multicam angles), requested at thumbnail priority through the existing
frame cache. A clip too narrow for the tiles falls back to its head. The
mode is UI state (`thumbnail_mode`, serde default Head, so saved settings
keep working) and `ui.set {timeline: {thumbnails}}` sets it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
98ba9acbce
commit
c85d0ac973
|
|
@ -27,7 +27,7 @@ use serde_json::{Value, json};
|
|||
|
||||
use crate::FilmcraftApp;
|
||||
use crate::dock::PanelKind;
|
||||
use crate::state::{Mode, PlaybackRes, Tool};
|
||||
use crate::state::{Mode, PlaybackRes, ThumbnailMode, Tool};
|
||||
|
||||
/// Prefix marking errors that may resolve after another frame.
|
||||
pub const RETRY: &str = "\u{1}";
|
||||
|
|
@ -189,6 +189,18 @@ pub fn handle(app: &mut FilmcraftApp, ctx: &egui::Context, req: &ControlRequest)
|
|||
if let Some(v) = tl.get("audioTrackHeight").and_then(Value::as_f64) {
|
||||
app.ui.timeline.audio_track_h = v as f32;
|
||||
}
|
||||
// Show Video Thumbnails: "off" or a mode ("head", "headAndTail", "continuous")
|
||||
if let Some(m) = tl.get("thumbnails").and_then(Value::as_str) {
|
||||
if m.eq_ignore_ascii_case("off") {
|
||||
app.ui.timeline.show_thumbnails = false;
|
||||
} else {
|
||||
let Some(mode) = ThumbnailMode::from_name(m) else {
|
||||
return err(format!("unknown thumbnail mode `{m}` (off, head, headAndTail, continuous)"));
|
||||
};
|
||||
app.ui.timeline.show_thumbnails = true;
|
||||
app.ui.timeline.thumbnail_mode = mode;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(q) = s("effectsSearch") {
|
||||
app.ui.effects_search = q.to_string();
|
||||
|
|
|
|||
|
|
@ -1215,6 +1215,9 @@
|
|||
Caption track options Opciones de pista de subtítulos
|
||||
Timeline Display Settings Ajustes de visualización de la línea de tiempo
|
||||
Show Video Thumbnails Mostrar miniaturas de video
|
||||
Head Cabeza
|
||||
Head and Tail Cabeza y cola
|
||||
Continuous Continuas
|
||||
Show Audio Waveform Mostrar forma de onda de audio
|
||||
Expand All Tracks Expandir todas las pistas
|
||||
Minimize All Tracks Minimizar todas las pistas
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 3 and column 95.
|
|
|
@ -1215,6 +1215,9 @@
|
|||
Caption track options キャプショントラックのオプション
|
||||
Timeline Display Settings タイムライン表示設定
|
||||
Show Video Thumbnails ビデオのサムネイルを表示
|
||||
Head 先頭
|
||||
Head and Tail 先頭と末尾
|
||||
Continuous 連続
|
||||
Show Audio Waveform オーディオ波形を表示
|
||||
Expand All Tracks すべてのトラックを展開
|
||||
Minimize All Tracks すべてのトラックを最小化
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 11 and column 59.
|
|
|
@ -1208,6 +1208,9 @@
|
|||
Caption track options 字幕轨道选项
|
||||
Timeline Display Settings 时间轴显示设置
|
||||
Show Video Thumbnails 显示视频缩略图
|
||||
Head 首帧
|
||||
Head and Tail 首尾帧
|
||||
Continuous 连续
|
||||
Show Audio Waveform 显示音频波形
|
||||
Expand All Tracks 展开所有轨道
|
||||
Minimize All Tracks 最小化所有轨道
|
||||
|
|
|
|||
|
Can't render this file because it contains an unexpected character in line 3 and column 95.
|
|
|
@ -19,7 +19,7 @@ use serde_json::{Value, json};
|
|||
|
||||
use crate::FilmcraftApp;
|
||||
use crate::icons::{self, Icon};
|
||||
use crate::state::{TimelineView, Tool};
|
||||
use crate::state::{ThumbnailMode, TimelineView, Tool};
|
||||
use crate::theme::Tokens;
|
||||
|
||||
use super::timeline_hit::{EdgeKind, Grab, edge_geometry, grab_at};
|
||||
|
|
@ -604,6 +604,36 @@ fn in_range(app: &FilmcraftApp, it: &TrackItem) -> bool {
|
|||
it.start < b && it.end() > a
|
||||
}
|
||||
|
||||
/// Most thumbnails one clip draws: a long clip far zoomed in only shows the visible ones anyway.
|
||||
const MAX_THUMBNAIL_TILES: usize = 256;
|
||||
|
||||
/// The video thumbnails of a clip whose picture area spans `x0..x1` (points), for frames `tile_w`
|
||||
/// wide: the left edge of each tile and the offset into the clip (`dur` long, frames of `frame`)
|
||||
/// whose picture it shows. Only tiles that meet `visible` (the panel's x range) are listed.
|
||||
/// Continuous tiles sit on a grid anchored at the clip's head, so scrolling only reveals new tiles
|
||||
/// and the others keep their frames. A clip too narrow for more than one tile shows its head.
|
||||
pub fn thumbnail_tiles(mode: ThumbnailMode, x0: f32, x1: f32, tile_w: f32, visible: (f32, f32), dur: Tick, frame: Tick) -> Vec<(f32, Tick)> {
|
||||
let width = x1 - x0;
|
||||
if !(x0.is_finite() && width.is_finite() && width > 0.0 && tile_w.is_finite() && tile_w >= 1.0) {
|
||||
return Vec::new();
|
||||
}
|
||||
let last = (dur - frame).max(Tick::ZERO);
|
||||
let offset_at = |x: f32| Tick(((x - x0) as f64 / width as f64 * dur.0 as f64).round() as i64).clamp(Tick::ZERO, last);
|
||||
let shown = |x: f32| x < visible.1 && x + tile_w > visible.0;
|
||||
let head = (x0, Tick::ZERO);
|
||||
let tiles = match mode {
|
||||
ThumbnailMode::Head => vec![head],
|
||||
ThumbnailMode::HeadAndTail if width >= 2.0 * tile_w => vec![head, (x1 - tile_w, last)],
|
||||
ThumbnailMode::HeadAndTail => vec![head],
|
||||
ThumbnailMode::Continuous => {
|
||||
let first = ((visible.0 - x0).max(0.0) / tile_w).floor() as usize;
|
||||
let end = ((visible.1.min(x1) - x0) / tile_w).ceil().max(1.0) as usize;
|
||||
(first..end).take(MAX_THUMBNAIL_TILES).map(|k| x0 + k as f32 * tile_w).map(|x| (x, offset_at(x))).collect()
|
||||
}
|
||||
};
|
||||
tiles.into_iter().filter(|(x, _)| shown(*x)).collect()
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw_clip(
|
||||
app: &mut FilmcraftApp,
|
||||
|
|
@ -632,7 +662,8 @@ fn draw_clip(
|
|||
p.line_segment([pos2(body.min.x, body.min.y + 0.5), pos2(body.max.x, body.min.y + 0.5)], Stroke::new(1.0, lighten(fill, 0.15)));
|
||||
let name_h = 16.0;
|
||||
let w = body.width();
|
||||
// head thumbnail (video): left-aligned, aspect-correct, below the name band
|
||||
// video thumbnails, aspect-correct, below the name band: the head frame, head and tail, or
|
||||
// frames side by side across the clip (Show Video Thumbnails in the wrench menu)
|
||||
if kind == TrackKind::Video && app.ui.timeline.show_thumbnails && body.height() > 28.0 && w > 20.0 && it.enabled {
|
||||
let th = Rect::from_min_max(pos2(body.min.x + 1.0, body.min.y + name_h), pos2(body.max.x - 1.0, body.max.y - 1.0));
|
||||
let aspect = app.session.project.item(it.item).and_then(|pi| match &pi.kind {
|
||||
|
|
@ -640,24 +671,28 @@ fn draw_clip(
|
|||
filmcraft_project::ItemKind::Sequence(s) => Some(s.settings.width as f32 / s.settings.height as f32),
|
||||
_ => None,
|
||||
});
|
||||
if let Some(aspect) = aspect {
|
||||
let tw = (th.height() * aspect).min(th.width());
|
||||
let mt = rate.snap(it.source_in);
|
||||
// a multi-camera clip shows its angle's clip
|
||||
let (thumb_item, mt) = app
|
||||
.session
|
||||
.project
|
||||
.sequence(it.item)
|
||||
.and_then(|q| {
|
||||
let ti = q.angle_video_track_index(it.multicam_angle(q)?)?;
|
||||
let inner = q.video_tracks[ti].item_at(mt)?;
|
||||
Some((inner.item, inner.source_time_at(mt)))
|
||||
})
|
||||
.unwrap_or((it.item, mt));
|
||||
if let Some((tex, _)) = app.thumbnail(ctx, thumb_item, mt, 160) {
|
||||
let r = Rect::from_min_size(th.min, vec2(tw, th.height()));
|
||||
let cp = p.with_clip_rect(th.intersect(p.clip_rect()));
|
||||
cp.image(tex, r, Rect::from_min_max(pos2(0.0, 0.0), pos2((tw / (th.height() * aspect)).min(1.0), 1.0)), Color32::WHITE);
|
||||
if let Some(aspect) = aspect.filter(|a| a.is_finite() && *a > 0.0) {
|
||||
let tw = th.height() * aspect;
|
||||
let cp = p.with_clip_rect(th.intersect(p.clip_rect()));
|
||||
let visible = (cp.clip_rect().min.x, cp.clip_rect().max.x);
|
||||
let mode = app.ui.timeline.thumbnail_mode;
|
||||
for (x, offset) in thumbnail_tiles(mode, th.min.x, th.max.x, tw, visible, it.duration, rate.frame_duration()) {
|
||||
let mt = rate.snap(it.source_time_at(it.start + offset));
|
||||
// a multi-camera clip shows its angle's clip
|
||||
let (thumb_item, mt) = app
|
||||
.session
|
||||
.project
|
||||
.sequence(it.item)
|
||||
.and_then(|q| {
|
||||
let ti = q.angle_video_track_index(it.multicam_angle(q)?)?;
|
||||
let inner = q.video_tracks.get(ti)?.item_at(mt)?;
|
||||
Some((inner.item, inner.source_time_at(mt)))
|
||||
})
|
||||
.unwrap_or((it.item, mt));
|
||||
if let Some((tex, _)) = app.thumbnail(ctx, thumb_item, mt, 160) {
|
||||
let r = Rect::from_min_size(pos2(x, th.min.y), vec2(tw, th.height()));
|
||||
cp.image(tex, r, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1179,7 +1214,20 @@ fn draw_top(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect, seq: &Sequenc
|
|||
}
|
||||
if key == "settings" {
|
||||
egui::Popup::menu(&resp).show(|ui| {
|
||||
ui.checkbox(&mut app.ui.timeline.show_thumbnails, tl!("Show Video Thumbnails"));
|
||||
let c = ui.checkbox(&mut app.ui.timeline.show_thumbnails, tl!("Show Video Thumbnails"));
|
||||
app.auto.add("timeline.settings.showThumbnails", c.rect, "Show Video Thumbnails");
|
||||
let on = app.ui.timeline.show_thumbnails;
|
||||
ui.indent("timeline.settings.thumbnails", |ui| {
|
||||
for (mode, label) in
|
||||
[(ThumbnailMode::Head, tl!("Head")), (ThumbnailMode::HeadAndTail, tl!("Head and Tail")), (ThumbnailMode::Continuous, tl!("Continuous"))]
|
||||
{
|
||||
let r = ui.add_enabled(on, egui::RadioButton::new(app.ui.timeline.thumbnail_mode == mode, label));
|
||||
app.auto.add(&format!("timeline.settings.thumbnails.{}", mode.name()), r.rect, label);
|
||||
if r.clicked() {
|
||||
app.ui.timeline.thumbnail_mode = mode;
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.checkbox(&mut app.ui.timeline.show_waveforms, tl!("Show Audio Waveform"));
|
||||
ui.separator();
|
||||
let mut te = app.session.state.show_through_edits;
|
||||
|
|
@ -2524,3 +2572,47 @@ mod chain_starts_tests {
|
|||
assert_eq!(chain_starts(Tick(5), &[Tick(-3), Tick(4)]), vec![Tick(5), Tick(5)]);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod thumbnail_tiles_tests {
|
||||
use super::*;
|
||||
|
||||
const S: Tick = Tick(TICKS_PER_SECOND);
|
||||
const FRAME: Tick = Tick(TICKS_PER_SECOND / 25);
|
||||
|
||||
#[test]
|
||||
fn continuous_thumbnails_tile_the_visible_part_of_the_clip() {
|
||||
// a 10 s clip 1000 pt wide, 80 pt tiles, the panel showing x 250..650
|
||||
let tiles = thumbnail_tiles(ThumbnailMode::Continuous, 0.0, 1000.0, 80.0, (250.0, 650.0), Tick(10 * S.0), FRAME);
|
||||
let xs: Vec<f32> = tiles.iter().map(|t| t.0).collect();
|
||||
assert_eq!(xs, [240.0, 320.0, 400.0, 480.0, 560.0, 640.0]);
|
||||
// each tile shows the clip time under its left edge (100 pt per second)
|
||||
assert_eq!(tiles[0].1, Tick(24 * S.0 / 10));
|
||||
assert_eq!(tiles[5].1, Tick(64 * S.0 / 10));
|
||||
// scrolling 30 pt moves the tiles with the clip: the ones still shown keep their frames
|
||||
let scrolled = thumbnail_tiles(ThumbnailMode::Continuous, -30.0, 970.0, 80.0, (250.0, 650.0), Tick(10 * S.0), FRAME);
|
||||
assert_eq!(scrolled.first(), Some(&(210.0, tiles[0].1)));
|
||||
assert!(scrolled.iter().all(|(x, t)| tiles.iter().any(|(x0, t0)| *x0 - 30.0 == *x && t0 == t)));
|
||||
// the last tile never shows a time past the clip's last frame
|
||||
let end = thumbnail_tiles(ThumbnailMode::Continuous, 0.0, 1000.0, 80.0, (0.0, 2000.0), Tick(10 * S.0), FRAME);
|
||||
assert_eq!(end.len(), 13);
|
||||
assert!(end.iter().all(|(_, t)| *t <= Tick(10 * S.0) - FRAME));
|
||||
// a clip far zoomed in asks for a bounded number of frames
|
||||
let huge = thumbnail_tiles(ThumbnailMode::Continuous, -1.0e9, 1.0e9, 1.0, (-2.0e9, 2.0e9), Tick(10 * S.0), FRAME);
|
||||
assert_eq!(huge.len(), MAX_THUMBNAIL_TILES);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn head_and_tail_thumbnails_need_room_for_two_frames() {
|
||||
let dur = Tick(4 * S.0);
|
||||
assert_eq!(thumbnail_tiles(ThumbnailMode::Head, 100.0, 500.0, 80.0, (0.0, 800.0), dur, FRAME), [(100.0, Tick::ZERO)]);
|
||||
assert_eq!(thumbnail_tiles(ThumbnailMode::HeadAndTail, 100.0, 500.0, 80.0, (0.0, 800.0), dur, FRAME), [(100.0, Tick::ZERO), (420.0, dur - FRAME)]);
|
||||
// narrower than two tiles: the head only, as in Continuous narrower than one tile
|
||||
assert_eq!(thumbnail_tiles(ThumbnailMode::HeadAndTail, 100.0, 250.0, 80.0, (0.0, 800.0), dur, FRAME), [(100.0, Tick::ZERO)]);
|
||||
assert_eq!(thumbnail_tiles(ThumbnailMode::Continuous, 100.0, 150.0, 80.0, (0.0, 800.0), dur, FRAME), [(100.0, Tick::ZERO)]);
|
||||
// off screen or degenerate: nothing
|
||||
assert!(thumbnail_tiles(ThumbnailMode::Head, 900.0, 1300.0, 80.0, (0.0, 800.0), dur, FRAME).is_empty());
|
||||
assert!(thumbnail_tiles(ThumbnailMode::Continuous, 0.0, 400.0, f32::NAN, (0.0, 800.0), dur, FRAME).is_empty());
|
||||
assert!(thumbnail_tiles(ThumbnailMode::Continuous, 0.0, 0.0, 80.0, (0.0, 800.0), dur, FRAME).is_empty());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,6 +190,9 @@ pub struct TimelineView {
|
|||
pub audio_track_h: f32,
|
||||
pub header_w: f32,
|
||||
pub show_thumbnails: bool,
|
||||
/// Which frames a video clip shows while `show_thumbnails` is on (the wrench menu).
|
||||
#[serde(default)]
|
||||
pub thumbnail_mode: ThumbnailMode,
|
||||
pub show_waveforms: bool,
|
||||
/// Follow playhead during playback (page scroll).
|
||||
pub follow: bool,
|
||||
|
|
@ -225,6 +228,7 @@ impl Default for TimelineView {
|
|||
audio_track_h: 56.0,
|
||||
header_w: 204.0,
|
||||
show_thumbnails: true,
|
||||
thumbnail_mode: ThumbnailMode::Head,
|
||||
show_waveforms: true,
|
||||
follow: true,
|
||||
fit_pending: true,
|
||||
|
|
@ -234,6 +238,35 @@ impl Default for TimelineView {
|
|||
}
|
||||
}
|
||||
|
||||
/// Video thumbnails on timeline clips (Timeline wrench menu ▸ Show Video Thumbnails).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum ThumbnailMode {
|
||||
/// The clip's first frame at its head.
|
||||
#[default]
|
||||
Head,
|
||||
/// The first frame at the head and the last one at the tail.
|
||||
HeadAndTail,
|
||||
/// Frames side by side across the whole clip.
|
||||
Continuous,
|
||||
}
|
||||
|
||||
impl ThumbnailMode {
|
||||
pub const ALL: [ThumbnailMode; 3] = [ThumbnailMode::Head, ThumbnailMode::HeadAndTail, ThumbnailMode::Continuous];
|
||||
|
||||
/// The name used by automation ids and `ui.set` (`timeline.thumbnails`).
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
ThumbnailMode::Head => "head",
|
||||
ThumbnailMode::HeadAndTail => "headAndTail",
|
||||
ThumbnailMode::Continuous => "continuous",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_name(name: &str) -> Option<ThumbnailMode> {
|
||||
Self::ALL.into_iter().find(|m| m.name().eq_ignore_ascii_case(name))
|
||||
}
|
||||
}
|
||||
|
||||
/// Monitor display mode (View ▸ Display Mode / the monitor's wrench menu). Multi-Camera is the
|
||||
/// separate `MonitorView::multicam` switch.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -263,3 +263,25 @@ fn the_scroll_bar_thumb_follows_the_pointer_and_reaches_both_ends() {
|
|||
let (scroll, pps, ..) = d.view();
|
||||
assert!(pps < 50.0 && (scroll - at).abs() < 1e-6, "{pps} {scroll}");
|
||||
}
|
||||
|
||||
/// Show Video Thumbnails ▸ Continuous asks for frames across each video clip, not only its head;
|
||||
/// `ui.set` switches the mode (and Off) and refuses an unknown one.
|
||||
#[test]
|
||||
fn continuous_video_thumbnails_ask_for_frames_across_each_clip() {
|
||||
let mut d = Driver::demo();
|
||||
d.ok("ui.set", json!({"timeline": {"thumbnails": "head", "pps": 50.0, "scroll": 0.0}}));
|
||||
d.view();
|
||||
d.frames(10);
|
||||
let head = d.app().frames.stats().request_misses;
|
||||
d.ok("ui.set", json!({"timeline": {"thumbnails": "continuous"}}));
|
||||
d.frames(3);
|
||||
let continuous = d.app().frames.stats().request_misses;
|
||||
assert!(continuous >= head + 5, "continuous thumbnails queued {} more frames", continuous - head);
|
||||
let tl = d.ok("ui.inspect", none())["ui"]["timeline"].clone();
|
||||
assert_eq!((tl["show_thumbnails"].clone(), tl["thumbnail_mode"].clone()), (json!(true), json!("Continuous")));
|
||||
d.ok("ui.set", json!({"timeline": {"thumbnails": "off"}}));
|
||||
assert!(!d.app().ui.timeline.show_thumbnails);
|
||||
let v = d.call("ui.set", json!({"timeline": {"thumbnails": "filmstrip"}}));
|
||||
assert_eq!(v["ok"], json!(false), "{v}");
|
||||
assert!(!d.app().ui.timeline.show_thumbnails);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ Methods (handlers in `crates/ui-egui/src/control.rs`):
|
|||
| `engine.commands` / `ui.menu.list` | – | command registry / menu tree |
|
||||
| `ui.inspect` | – | UI state (tool, workspace, dock, timeline view, playback, fps…) |
|
||||
| `ui.elements` | `{prefix?}` | every on-screen interactive element: id, label, rect |
|
||||
| `ui.set` | `{tool, workspace, mode, theme, focused, playbackRes, timeline:{pps,scroll,fit}, program:{…}, source:{…}, clipDialog:{param: value}, menuDialog:{param: value}, panels:{scopes, timecode, reference, events, progress}, export:{…}}` | `program` / `source` merge fields into the monitor view state (`res`, `paused_res`, `high_quality`, `display`, `zoom`, `pan`, `show_rulers`, `show_guides`, `lock_guides`, `snap`, `guides`, `compare_ref`, …; see [monitors.md](monitors.md)); `clipDialog` sets fields of the open Edit / Clip / File dialog (Paste Attributes, Make Subclip, Frame Hold Options, …); `menuDialog` those of the M3.11 dialogs (Find, Create Search Bin, Project Settings, Scene Edit Detection, Simplify Sequence, Automate to Sequence…; see `panels::menu_dialogs`); `panels` merges into the panel settings (`ui.inspect` → `ui.panels`; see `panels::panel_state`): Lumetri Scopes `{shown: ["vectorscopeYuv"|"vectorscopeHls"|"histogram"|"parade"|"waveform"], waveformType, paradeType, colorSpace, brightness, scale, clamp, targets}`, Timecode `{rows: [{source, mode, display}], showName}`, Reference Monitor `{ganged, time, display: "composite"|"scopes", scopes}`, Events `{level, selected}`, Progress `{showFinished}` `export` deep-merges into Export mode state (`preset` — applied first —, `settings` (ExportSettings JSON), `fileName`, `location`, `range`, `customStart`, `customEnd`, `openSections`, `manager` (Preset Manager: `query`, `favoritesOnly`, `selected`, `saveName`), `quickOpen`, `quickPath`, `quickPreset`); `program` / `source` merge fields into the monitor view state (`res`, `paused_res`, `high_quality`, `display`, `zoom`, `pan`, `show_rulers`, `show_guides`, `lock_guides`, `snap`, `guides`, `compare_ref`, …; see [monitors.md](monitors.md)); `clipDialog` sets fields of the open Edit / Clip / File dialog (Paste Attributes, Make Subclip, Frame Hold Options, …); `menuDialog` those of the M3.11 dialogs (Find, Create Search Bin, Project Settings, Scene Edit Detection, Simplify Sequence, Automate to Sequence…; see `panels::menu_dialogs`); `projectPanel` merges into the Project panel's view state (`bin` shown in place, `tabs` (bins in a tab or window: `{bin, floating, view, iconSize, nav}`), `activeTab`, `selectedBin`, `rename: {item|bin, text}`, `dialog` (`{"metadataDisplay": {columns, filter}}`, `{"savePresetAs": {name}}`, `{"managePresets": {selected, name}}`, `{"freeformOptions": {options}}`, `{"saveArrangement": {name, bin}}`)); `mediaBrowser` into the Media Browser's (`expanded`, `pathEdit`, `editColumns`, `treeWidth`). View settings, columns and presets are engine commands (`project.view.set`, `project.columns.set`, `project.viewPreset.*`, `mediaBrowser.settings`) |
|
||||
| `ui.set` | `{tool, workspace, mode, theme, focused, playbackRes, timeline:{pps,scroll,fit,thumbnails}, program:{…}, source:{…}, clipDialog:{param: value}, menuDialog:{param: value}, panels:{scopes, timecode, reference, events, progress}, export:{…}}` | `program` / `source` merge fields into the monitor view state (`res`, `paused_res`, `high_quality`, `display`, `zoom`, `pan`, `show_rulers`, `show_guides`, `lock_guides`, `snap`, `guides`, `compare_ref`, …; see [monitors.md](monitors.md)); `timeline.thumbnails` is `off`, `head`, `headAndTail` or `continuous` (the wrench menu's Show Video Thumbnails); `clipDialog` sets fields of the open Edit / Clip / File dialog (Paste Attributes, Make Subclip, Frame Hold Options, …); `menuDialog` those of the M3.11 dialogs (Find, Create Search Bin, Project Settings, Scene Edit Detection, Simplify Sequence, Automate to Sequence…; see `panels::menu_dialogs`); `panels` merges into the panel settings (`ui.inspect` → `ui.panels`; see `panels::panel_state`): Lumetri Scopes `{shown: ["vectorscopeYuv"|"vectorscopeHls"|"histogram"|"parade"|"waveform"], waveformType, paradeType, colorSpace, brightness, scale, clamp, targets}`, Timecode `{rows: [{source, mode, display}], showName}`, Reference Monitor `{ganged, time, display: "composite"|"scopes", scopes}`, Events `{level, selected}`, Progress `{showFinished}` `export` deep-merges into Export mode state (`preset` — applied first —, `settings` (ExportSettings JSON), `fileName`, `location`, `range`, `customStart`, `customEnd`, `openSections`, `manager` (Preset Manager: `query`, `favoritesOnly`, `selected`, `saveName`), `quickOpen`, `quickPath`, `quickPreset`); `program` / `source` merge fields into the monitor view state (`res`, `paused_res`, `high_quality`, `display`, `zoom`, `pan`, `show_rulers`, `show_guides`, `lock_guides`, `snap`, `guides`, `compare_ref`, …; see [monitors.md](monitors.md)); `clipDialog` sets fields of the open Edit / Clip / File dialog (Paste Attributes, Make Subclip, Frame Hold Options, …); `menuDialog` those of the M3.11 dialogs (Find, Create Search Bin, Project Settings, Scene Edit Detection, Simplify Sequence, Automate to Sequence…; see `panels::menu_dialogs`); `projectPanel` merges into the Project panel's view state (`bin` shown in place, `tabs` (bins in a tab or window: `{bin, floating, view, iconSize, nav}`), `activeTab`, `selectedBin`, `rename: {item|bin, text}`, `dialog` (`{"metadataDisplay": {columns, filter}}`, `{"savePresetAs": {name}}`, `{"managePresets": {selected, name}}`, `{"freeformOptions": {options}}`, `{"saveArrangement": {name, bin}}`)); `mediaBrowser` into the Media Browser's (`expanded`, `pathEdit`, `editColumns`, `treeWidth`). View settings, columns and presets are engine commands (`project.view.set`, `project.columns.set`, `project.viewPreset.*`, `mediaBrowser.settings`) |
|
||||
| `ui.panel.show` / `ui.panel.close` | `{panel}` | |
|
||||
| `ui.click` / `ui.move` | `{id}` or `{x,y}`, `button`, `count`, `modifiers` | synthetic pointer input |
|
||||
| `ui.drag` | `{from, to, steps, modifiers}` | press–move–release |
|
||||
|
|
|
|||
Loading…
Reference in New Issue