mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
Composition Settings: Resolution menu with the frame size and memory at that resolution (fixes #2167)
This commit is contained in:
@@ -262,6 +262,7 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
("Frame Aspect Ratio: {}", "フレームアスペクト比:{}"),
|
||||
("Frame Blending: {}", "フレームブレンド:{}"),
|
||||
("Frame Rate:", "フレームレート:"),
|
||||
("Resolution:", "解像度:"),
|
||||
("Frame Render Time {}ms", "フレームのレンダリング時間 {}ms"),
|
||||
("Frame Target:", "フレームターゲット:"),
|
||||
("Freeze", "フリーズ"),
|
||||
@@ -997,6 +998,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
|
||||
("Frame Aspect Ratio: {}", "帧长宽比:{}"),
|
||||
("Frame Blending: {}", "帧混合:{}"),
|
||||
("Frame Rate:", "帧速率:"),
|
||||
("Resolution:", "分辨率:"),
|
||||
("Frame Render Time {}ms", "帧渲染时间 {} 毫秒"),
|
||||
("Frame Target:", "帧目标:"),
|
||||
("Freeze", "冻结"),
|
||||
@@ -1737,6 +1739,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
|
||||
("Frame Aspect Ratio: {}", "影格長寬比:{}"),
|
||||
("Frame Blending: {}", "影格混合:{}"),
|
||||
("Frame Rate:", "影格速率:"),
|
||||
("Resolution:", "解析度:"),
|
||||
("Frame Render Time {}ms", "影格算圖時間 {} 毫秒"),
|
||||
("Frame Target:", "幀目標:"),
|
||||
("Freeze", "凍結"),
|
||||
@@ -2480,6 +2483,7 @@ const UKRAINIAN: &[(&str, &str)] = &[
|
||||
("Frame Aspect Ratio: {}", "Пропорції кадру: {}"),
|
||||
("Frame Blending: {}", "Змішування кадрів: {}"),
|
||||
("Frame Rate:", "Частота кадрів:"),
|
||||
("Resolution:", "Роздільність:"),
|
||||
("Frame Render Time {}ms", "Час рендерингу кадру {} мс"),
|
||||
("Frame Target:", "Цільовий фрейм:"),
|
||||
("Freeze", "Заморозити"),
|
||||
@@ -3040,6 +3044,7 @@ const PORTUGUESE: &[(&str, &str)] = &[
|
||||
("Frame Aspect Ratio: {}", "Proporção do quadro: {}"),
|
||||
("Frame Blending: {}", "Mesclagem de quadros: {}"),
|
||||
("Frame Rate:", "Taxa de quadros:"),
|
||||
("Resolution:", "Resolução:"),
|
||||
("Frame Render Time {}ms", "Tempo de renderização do quadro {}ms"),
|
||||
("Frame Target:", "Destino do quadro:"),
|
||||
("Grab bag", "Lista"),
|
||||
@@ -3498,6 +3503,7 @@ const CZECH: &[(&str, &str)] = &[
|
||||
("Frame Aspect Ratio: {}", "Poměr stran snímku: {}"),
|
||||
("Frame Blending: {}", "Prolnutí snímků: {}"),
|
||||
("Frame Rate:", "Snímková frekvence:"),
|
||||
("Resolution:", "Rozlišení:"),
|
||||
("Frame Render Time {}ms", "Čas vykreslení snímku {}ms"),
|
||||
("Frame Target:", "Cílový rámec:"),
|
||||
("Freeze", "Zmrazit"),
|
||||
@@ -4061,6 +4067,7 @@ const SPANISH: &[(&str, &str)] = &[
|
||||
("Frame Aspect Ratio: {}", "Relación de aspecto del fotograma: {}"),
|
||||
("Frame Blending: {}", "Fusión de fotogramas: {}"),
|
||||
("Frame Rate:", "Velocidad de fotogramas:"),
|
||||
("Resolution:", "Resolución:"),
|
||||
("Frame Render Time {}ms", "Tiempo de procesamiento del fotograma {}ms"),
|
||||
("Frame Target:", "Marco de destino:"),
|
||||
("Freeze", "Congelar"),
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
use crate::i18n::{tr, tr_args};
|
||||
use crate::icons::Icon;
|
||||
use effectcraft_engine::prefs::CompPreset;
|
||||
use effectcraft_engine::project::Resolution;
|
||||
use effectcraft_engine::time::{FrameRate, format_timecode_frames, parse_timecode};
|
||||
use egui::{Color32, vec2};
|
||||
use serde_json::json;
|
||||
@@ -94,6 +95,9 @@ pub struct CompDraft {
|
||||
pub preserve_frame_rate: bool,
|
||||
pub preserve_resolution: bool,
|
||||
pub advanced_3d: bool,
|
||||
/// The comp's Resolution (the Composition panel's popup): saved with the project but no undo
|
||||
/// step, so OK sets it apart from `comp.new` / `comp.settings`.
|
||||
pub resolution: Resolution,
|
||||
pub tab: Tab,
|
||||
/// Timecode text being edited (start, duration).
|
||||
pub start_tc: Option<String>,
|
||||
@@ -122,6 +126,7 @@ impl Default for CompDraft {
|
||||
preserve_frame_rate: false,
|
||||
preserve_resolution: false,
|
||||
advanced_3d: false,
|
||||
resolution: Resolution::Auto,
|
||||
tab: Tab::Basic,
|
||||
start_tc: None,
|
||||
dur_tc: None,
|
||||
@@ -376,9 +381,30 @@ pub fn show(
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label("");
|
||||
let mb = d.width as f64 * d.height as f64 * 4.0 / (1024.0 * 1024.0);
|
||||
ui.label(egui::RichText::new(tr_args("{} x {}, {}MB per 8bpc frame", &[&d.width, &d.height, &format!("{mb:.1}")])).color(Color32::GRAY));
|
||||
// As in After Effects, the Resolution menu (the Composition panel's popup) with
|
||||
// the frame it renders beside it.
|
||||
ui.label(tr("Resolution:"));
|
||||
ui.horizontal(|ui| {
|
||||
let r = egui::ComboBox::from_id_salt("comp-resolution").width(90.0).selected_text(tr(d.resolution.label())).show_ui(ui, |ui| {
|
||||
for res in Resolution::ALL {
|
||||
ui.selectable_value(&mut d.resolution, res, tr(res.label()));
|
||||
}
|
||||
let custom = matches!(d.resolution, Resolution::Custom(_));
|
||||
if ui.selectable_label(custom, tr("Custom")).clicked() && !custom {
|
||||
d.resolution = Resolution::Custom(5);
|
||||
}
|
||||
});
|
||||
auto.add("dialog.comp.resolution", r.response.rect, "Resolution");
|
||||
if let Resolution::Custom(n) = d.resolution {
|
||||
let mut n = n;
|
||||
let r = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut n).range(2..=40)));
|
||||
auto.add("dialog.comp.resolutionFactor", r.rect, "Custom Resolution");
|
||||
d.resolution = Resolution::Custom(n);
|
||||
}
|
||||
let (w, h) = frame_at(d.width, d.height, d.resolution);
|
||||
let mb = w as f64 * h as f64 * 4.0 / (1024.0 * 1024.0);
|
||||
ui.label(egui::RichText::new(tr_args("{} x {}, {}MB per 8bpc frame", &[&w, &h, &format!("{mb:.1}")])).color(Color32::GRAY));
|
||||
});
|
||||
ui.end_row();
|
||||
ui.label(tr("Start Timecode:"));
|
||||
let r = ui.horizontal(|ui| timecode_field(ui, &mut d.start_tc, &mut d.start, rate, i64::MIN / 4)).inner;
|
||||
@@ -486,10 +512,26 @@ pub fn params(d: &CompDraft) -> serde_json::Value {
|
||||
})
|
||||
}
|
||||
|
||||
/// The frame size the comp renders at `res`: every n-th pixel, a partial one counted (Auto
|
||||
/// follows the magnification, so the dialog shows the full frame for it).
|
||||
pub fn frame_at(w: u32, h: u32, res: Resolution) -> (u32, u32) {
|
||||
let n = u32::from(res.factor().unwrap_or(1).max(1));
|
||||
(w.div_ceil(n), h.div_ceil(n))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn frame_size_follows_the_resolution() {
|
||||
assert_eq!(frame_at(1920, 1081, Resolution::Half), (960, 541), "an odd side keeps its last pixel");
|
||||
assert_eq!(frame_at(1920, 1080, Resolution::Third), (640, 360));
|
||||
assert_eq!(frame_at(1920, 1080, Resolution::Custom(7)), (275, 155));
|
||||
assert_eq!(frame_at(1920, 1080, Resolution::Full), (1920, 1080));
|
||||
assert_eq!(frame_at(1920, 1080, Resolution::Auto), (1920, 1080));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aspect_labels_match_ae() {
|
||||
assert_eq!(aspect_label(1920.0, 1080.0), "16:9 (1.78)");
|
||||
|
||||
@@ -102,6 +102,7 @@ pub fn open_comp_settings_for(app: &mut EffectcraftApp, cid: ItemId) -> Result<(
|
||||
preserve_frame_rate: c.preserve_frame_rate,
|
||||
preserve_resolution: c.preserve_resolution,
|
||||
advanced_3d: c.renderer == effectcraft_engine::project::Renderer::Advanced3D,
|
||||
resolution: c.resolution,
|
||||
..Default::default()
|
||||
};
|
||||
app.dialog_state.editing_existing = true;
|
||||
@@ -408,6 +409,11 @@ fn comp_settings(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens, exis
|
||||
m.insert("comp".into(), json!(cid.0));
|
||||
}
|
||||
let r = if existing { app.session.execute("comp.settings", params) } else { app.session.execute("comp.new", params) };
|
||||
let cid = if existing { app.dialog_state.editing_comp.map(|c| c.0) } else { r.as_ref().ok().and_then(|v| v["comp"].as_u64()) };
|
||||
let r = match cid {
|
||||
Some(cid) if r.is_ok() => set_resolution(app, cid, app.dialog_state.comp.resolution).and(r),
|
||||
_ => r,
|
||||
};
|
||||
if !existing && r.is_ok() {
|
||||
app.dialog_state.last_new_comp = Some(app.dialog_state.comp.clone());
|
||||
}
|
||||
@@ -420,6 +426,15 @@ fn comp_settings(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens, exis
|
||||
}
|
||||
}
|
||||
|
||||
/// The Resolution chosen in Composition Settings, set as the Composition panel's popup sets it:
|
||||
/// saved with the project but no undo step, so it stays out of `comp.new` / `comp.settings`.
|
||||
fn set_resolution(app: &mut EffectcraftApp, cid: u64, r: effectcraft_engine::project::Resolution) -> effectcraft_engine::Result<Value> {
|
||||
match r.factor() {
|
||||
None => app.session.execute("view.res.auto", json!({"comp": cid})),
|
||||
Some(n) => app.session.execute("view.res.custom", json!({"comp": cid, "factor": n})),
|
||||
}
|
||||
}
|
||||
|
||||
/// One frame of the open dialog with `key` pressed.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn press_key(app: &mut EffectcraftApp, ctx: &egui::Context, key: egui::Key, modifiers: egui::Modifiers) {
|
||||
@@ -827,6 +842,59 @@ mod tests {
|
||||
assert_eq!(selected(&ctx), Some(0..app.dialog_state.solid_name.chars().count()));
|
||||
}
|
||||
|
||||
/// One frame of Composition Settings (or New Composition) with Enter pressed: OK.
|
||||
fn press_ok(app: &mut EffectcraftApp, ctx: &egui::Context, existing: bool) {
|
||||
let t = app.tokens;
|
||||
let mut out = ctx.run_ui(Default::default(), |ui| {
|
||||
app.auto.begin_frame();
|
||||
ui.input_mut(|i| {
|
||||
i.events.push(egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() })
|
||||
});
|
||||
comp_settings(app, ui.ctx(), &t, existing);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
assert!(app.dialog.is_none(), "OK closed the dialog");
|
||||
}
|
||||
|
||||
/// The Resolution menu sets the comp's Resolution on OK. As from the Composition panel's popup
|
||||
/// it is no undo step: the settings are one step, and undoing them keeps the Resolution.
|
||||
#[test]
|
||||
fn composition_settings_resolution_applies_on_ok_outside_the_undo_history() {
|
||||
use effectcraft_engine::project::Resolution;
|
||||
let (mut app, ctx) = app();
|
||||
let cid = app.session.active_comp_id().unwrap();
|
||||
app.session.execute("view.res.third", json!({})).unwrap();
|
||||
open_comp_settings(&mut app).unwrap();
|
||||
assert_eq!(app.dialog_state.comp.resolution, Resolution::Third, "the dialog opens on the comp's Resolution");
|
||||
frame(&mut app, &ctx);
|
||||
assert!(app.auto.find("dialog.comp.resolution").is_some());
|
||||
let steps = app.session.history.undo.len();
|
||||
(app.dialog_state.comp.resolution, app.dialog_state.comp.width) = (Resolution::Half, 700);
|
||||
app.dialog_state.comp.lock_aspect = false;
|
||||
press_ok(&mut app, &ctx, true);
|
||||
let c = app.session.project.comp(cid).unwrap();
|
||||
assert_eq!((c.width, c.resolution), (700, Resolution::Half));
|
||||
assert_eq!(app.session.history.undo.len(), steps + 1, "Composition Settings is one undo step");
|
||||
app.session.execute("edit.undo", json!({})).unwrap();
|
||||
let c = app.session.project.comp(cid).unwrap();
|
||||
assert_eq!((c.width, c.resolution), (640, Resolution::Half), "undo keeps the Resolution");
|
||||
// Only the Resolution changed: still set, and no extra step.
|
||||
open_comp_settings(&mut app).unwrap();
|
||||
app.dialog_state.comp.resolution = Resolution::Custom(6);
|
||||
let steps = app.session.history.undo.len();
|
||||
press_ok(&mut app, &ctx, true);
|
||||
assert_eq!(app.session.project.comp(cid).unwrap().resolution, Resolution::Custom(6));
|
||||
assert_eq!(app.session.history.undo.len(), steps, "no undo step");
|
||||
// New Composition makes its comp at the menu's Resolution; the comp the viewer showed keeps its own.
|
||||
open_new_comp(&mut app);
|
||||
app.dialog_state.comp.resolution = Resolution::Quarter;
|
||||
press_ok(&mut app, &ctx, false);
|
||||
let new = app.session.active_comp_id().unwrap();
|
||||
assert_ne!(new, cid);
|
||||
assert_eq!(app.session.project.comp(new).unwrap().resolution, Resolution::Quarter);
|
||||
assert_eq!(app.session.project.comp(cid).unwrap().resolution, Resolution::Custom(6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn composition_settings_register_every_control_and_new_comp_remembers() {
|
||||
let (mut app, ctx) = app();
|
||||
|
||||
@@ -688,8 +688,10 @@ Fixes from measured reports:
|
||||
step (undo and redo keep it) and keeps the comp's cached frames; projects saved before open
|
||||
with Auto. View ▸ Resolution and the panel's popup run `view.res.full|half|third|quarter|auto`
|
||||
and `view.res.custom {factor}`, which now work headless too; `comp.info` reports `resolution`
|
||||
and `resolutionFactor` (#417). Not yet: the magnification is not saved per comp, and the
|
||||
Composition Settings dialog has no Resolution popup.
|
||||
and `resolutionFactor` (#417). Composition Settings ▸ Basic has the same Resolution menu, with
|
||||
the frame size and memory per 8bpc frame at that resolution beside it; OK sets it as the popup
|
||||
does, outside the undo step of the other settings (#2167). Not yet: the magnification is not
|
||||
saved per comp.
|
||||
|
||||
### Update: plug-ins and extensions as in After Effects; ease presets become a ScriptUI panel
|
||||
|
||||
|
||||
Reference in New Issue
Block a user