Merge #2223: Composition Settings: Resolution menu with the frame size and memory at that resolution (@marcoarnulfo)

This commit is contained in:
aiongg
2026-10-23 23:33:23 +00:00
6 changed files with 132 additions and 10 deletions
+1 -3
View File
@@ -26,9 +26,7 @@ pub(crate) fn render<'g>(e: &mut Enc<'g>, r: &Renderer, comp_id: ItemId, t: Tick
return None; return None;
} }
let ctx = r.eval_ctx(comp_id, t)?; let ctx = r.eval_ctx(comp_id, t)?;
let s = r.opts.scale; let (w, h) = effectcraft_render::frame_size(ctx.comp.width, ctx.comp.height, r.opts.scale);
let w = ((ctx.comp.width as f64 * s).round() as u32).max(1);
let h = ((ctx.comp.height as f64 * s).round() as u32).max(1);
if r.depth() > 16 || !e.g.fits(w, h) { if r.depth() > 16 || !e.g.fits(w, h) {
return None; return None;
} }
+7 -2
View File
@@ -981,8 +981,7 @@ impl<'a> Renderer<'a> {
pub fn comp_frame_cpu(&self, comp_id: ItemId, t: Tick) -> Image { pub fn comp_frame_cpu(&self, comp_id: ItemId, t: Tick) -> Image {
let Some(comp) = self.project.comp(comp_id) else { return Image::new(1, 1) }; let Some(comp) = self.project.comp(comp_id) else { return Image::new(1, 1) };
let s = self.opts.scale; let s = self.opts.scale;
let w = ((comp.width as f64 * s).round() as u32).max(1); let (w, h) = frame_size(comp.width, comp.height, s);
let h = ((comp.height as f64 * s).round() as u32).max(1);
// Region of interest on a comp with 3D layers: render the frame and crop it. A region // Region of interest on a comp with 3D layers: render the frame and crop it. A region
// reaching past the comp frame (the Extended Viewer) renders Classic 3D natively — the // reaching past the comp frame (the Extended Viewer) renders Classic 3D natively — the
// planes project through the offset camera — and Advanced 3D as the frame, padded. // planes project through the offset camera — and Advanced 3D as the frame, padded.
@@ -2389,6 +2388,12 @@ pub fn extended_region(w: u32, h: u32, visible: [f64; 4], margin: f64) -> Option
Some([x0, y0, x1 - x0, y1 - y0]) Some([x0, y0, x1 - x0, y1 - y0])
} }
/// The size of a `width` x `height` comp frame rendered at `scale` (both compositors, and the
/// frame size Composition Settings shows for a Resolution).
pub fn frame_size(width: u32, height: u32, scale: f64) -> (u32, u32) {
(((width as f64 * scale).round() as u32).max(1), ((height as f64 * scale).round() as u32).max(1))
}
pub fn render_frame(project: &Project, comp: ItemId, t: Tick, scale: f64) -> Image { pub fn render_frame(project: &Project, comp: ItemId, t: Tick, scale: f64) -> Image {
Renderer::new(project, &NoFootage, RenderOpts { scale, ..Default::default() }).comp_frame(comp, t) Renderer::new(project, &NoFootage, RenderOpts { scale, ..Default::default() }).comp_frame(comp, t)
} }
+7
View File
@@ -298,6 +298,7 @@ const JAPANESE: &[(&str, &str)] = &[
("Frame Aspect Ratio: {}", "フレームアスペクト比:{}"), ("Frame Aspect Ratio: {}", "フレームアスペクト比:{}"),
("Frame Blending: {}", "フレームブレンド:{}"), ("Frame Blending: {}", "フレームブレンド:{}"),
("Frame Rate:", "フレームレート:"), ("Frame Rate:", "フレームレート:"),
("Resolution:", "解像度:"),
("Frame Render Time {}ms", "フレームのレンダリング時間 {}ms"), ("Frame Render Time {}ms", "フレームのレンダリング時間 {}ms"),
("Frame Target:", "フレームターゲット:"), ("Frame Target:", "フレームターゲット:"),
("Freeze", "フリーズ"), ("Freeze", "フリーズ"),
@@ -1033,6 +1034,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
("Frame Aspect Ratio: {}", "帧长宽比:{}"), ("Frame Aspect Ratio: {}", "帧长宽比:{}"),
("Frame Blending: {}", "帧混合:{}"), ("Frame Blending: {}", "帧混合:{}"),
("Frame Rate:", "帧速率:"), ("Frame Rate:", "帧速率:"),
("Resolution:", "分辨率:"),
("Frame Render Time {}ms", "帧渲染时间 {} 毫秒"), ("Frame Render Time {}ms", "帧渲染时间 {} 毫秒"),
("Frame Target:", "帧目标:"), ("Frame Target:", "帧目标:"),
("Freeze", "冻结"), ("Freeze", "冻结"),
@@ -1773,6 +1775,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
("Frame Aspect Ratio: {}", "影格長寬比:{}"), ("Frame Aspect Ratio: {}", "影格長寬比:{}"),
("Frame Blending: {}", "影格混合:{}"), ("Frame Blending: {}", "影格混合:{}"),
("Frame Rate:", "影格速率:"), ("Frame Rate:", "影格速率:"),
("Resolution:", "解析度:"),
("Frame Render Time {}ms", "影格算圖時間 {} 毫秒"), ("Frame Render Time {}ms", "影格算圖時間 {} 毫秒"),
("Frame Target:", "幀目標:"), ("Frame Target:", "幀目標:"),
("Freeze", "凍結"), ("Freeze", "凍結"),
@@ -2467,6 +2470,7 @@ const UKRAINIAN: &[(&str, &str)] = &[
("Frame Aspect Ratio: {}", "Пропорції кадру: {}"), ("Frame Aspect Ratio: {}", "Пропорції кадру: {}"),
("Frame Blending: {}", "Змішування кадрів: {}"), ("Frame Blending: {}", "Змішування кадрів: {}"),
("Frame Rate:", "Частота кадрів:"), ("Frame Rate:", "Частота кадрів:"),
("Resolution:", "Роздільність:"),
("Frame Render Time {}ms", "Час рендерингу кадру {} мс"), ("Frame Render Time {}ms", "Час рендерингу кадру {} мс"),
("Frame Target:", "Цільовий фрейм:"), ("Frame Target:", "Цільовий фрейм:"),
("Freeze", "Заморозити"), ("Freeze", "Заморозити"),
@@ -3027,6 +3031,7 @@ const PORTUGUESE: &[(&str, &str)] = &[
("Frame Aspect Ratio: {}", "Proporção do quadro: {}"), ("Frame Aspect Ratio: {}", "Proporção do quadro: {}"),
("Frame Blending: {}", "Mesclagem de quadros: {}"), ("Frame Blending: {}", "Mesclagem de quadros: {}"),
("Frame Rate:", "Taxa de quadros:"), ("Frame Rate:", "Taxa de quadros:"),
("Resolution:", "Resolução:"),
("Frame Render Time {}ms", "Tempo de renderização do quadro {}ms"), ("Frame Render Time {}ms", "Tempo de renderização do quadro {}ms"),
("Frame Target:", "Destino do quadro:"), ("Frame Target:", "Destino do quadro:"),
("Grab bag", "Lista"), ("Grab bag", "Lista"),
@@ -3485,6 +3490,7 @@ const CZECH: &[(&str, &str)] = &[
("Frame Aspect Ratio: {}", "Poměr stran snímku: {}"), ("Frame Aspect Ratio: {}", "Poměr stran snímku: {}"),
("Frame Blending: {}", "Prolnutí snímků: {}"), ("Frame Blending: {}", "Prolnutí snímků: {}"),
("Frame Rate:", "Snímková frekvence:"), ("Frame Rate:", "Snímková frekvence:"),
("Resolution:", "Rozlišení:"),
("Frame Render Time {}ms", "Čas vykreslení snímku {}ms"), ("Frame Render Time {}ms", "Čas vykreslení snímku {}ms"),
("Frame Target:", "Cílový rámec:"), ("Frame Target:", "Cílový rámec:"),
("Freeze", "Zmrazit"), ("Freeze", "Zmrazit"),
@@ -4048,6 +4054,7 @@ const SPANISH: &[(&str, &str)] = &[
("Frame Aspect Ratio: {}", "Relación de aspecto del fotograma: {}"), ("Frame Aspect Ratio: {}", "Relación de aspecto del fotograma: {}"),
("Frame Blending: {}", "Fusión de fotogramas: {}"), ("Frame Blending: {}", "Fusión de fotogramas: {}"),
("Frame Rate:", "Velocidad de fotogramas:"), ("Frame Rate:", "Velocidad de fotogramas:"),
("Resolution:", "Resolución:"),
("Frame Render Time {}ms", "Tiempo de procesamiento del fotograma {}ms"), ("Frame Render Time {}ms", "Tiempo de procesamiento del fotograma {}ms"),
("Frame Target:", "Marco de destino:"), ("Frame Target:", "Marco de destino:"),
("Freeze", "Congelar"), ("Freeze", "Congelar"),
+45 -3
View File
@@ -5,6 +5,7 @@
use crate::i18n::{tr, tr_args}; use crate::i18n::{tr, tr_args};
use crate::icons::Icon; use crate::icons::Icon;
use effectcraft_engine::prefs::CompPreset; use effectcraft_engine::prefs::CompPreset;
use effectcraft_engine::project::Resolution;
use effectcraft_engine::time::{FrameRate, format_timecode_frames, parse_timecode}; use effectcraft_engine::time::{FrameRate, format_timecode_frames, parse_timecode};
use egui::{Color32, vec2}; use egui::{Color32, vec2};
use serde_json::json; use serde_json::json;
@@ -94,6 +95,9 @@ pub struct CompDraft {
pub preserve_frame_rate: bool, pub preserve_frame_rate: bool,
pub preserve_resolution: bool, pub preserve_resolution: bool,
pub advanced_3d: 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, pub tab: Tab,
/// Timecode text being edited (start, duration). /// Timecode text being edited (start, duration).
pub start_tc: Option<String>, pub start_tc: Option<String>,
@@ -122,6 +126,7 @@ impl Default for CompDraft {
preserve_frame_rate: false, preserve_frame_rate: false,
preserve_resolution: false, preserve_resolution: false,
advanced_3d: false, advanced_3d: false,
resolution: Resolution::Auto,
tab: Tab::Basic, tab: Tab::Basic,
start_tc: None, start_tc: None,
dur_tc: None, dur_tc: None,
@@ -376,9 +381,30 @@ pub fn show(
} }
}); });
ui.end_row(); ui.end_row();
ui.label(""); // As in After Effects, the Resolution menu (the Composition panel's popup) with
let mb = d.width as f64 * d.height as f64 * 4.0 / (1024.0 * 1024.0); // the frame it renders beside it.
ui.label(egui::RichText::new(tr_args("{} x {}, {}MB per 8bpc frame", &[&d.width, &d.height, &format!("{mb:.1}")])).color(Color32::GRAY)); 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.end_row();
ui.label(tr("Start Timecode:")); 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; 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` (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) {
effectcraft_render::frame_size(w, h, 1.0 / f64::from(res.factor().unwrap_or(1).max(1)))
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn frame_size_follows_the_resolution() {
// The renderer's size, which rounds: 1081 / 3 = 360.33 and 1920 / 7 = 274.3.
assert_eq!(frame_at(1920, 1081, Resolution::Third), (640, 360));
assert_eq!(frame_at(1920, 1080, Resolution::Custom(7)), (274, 154));
assert_eq!(frame_at(1920, 1081, Resolution::Half), (960, 541));
assert_eq!(frame_at(1920, 1080, Resolution::Full), (1920, 1080));
assert_eq!(frame_at(1920, 1080, Resolution::Auto), (1920, 1080));
}
#[test] #[test]
fn aspect_labels_match_ae() { fn aspect_labels_match_ae() {
assert_eq!(aspect_label(1920.0, 1080.0), "16:9 (1.78)"); assert_eq!(aspect_label(1920.0, 1080.0), "16:9 (1.78)");
+68
View File
@@ -103,6 +103,7 @@ pub fn open_comp_settings_for(app: &mut EffectcraftApp, cid: ItemId) -> Result<(
preserve_frame_rate: c.preserve_frame_rate, preserve_frame_rate: c.preserve_frame_rate,
preserve_resolution: c.preserve_resolution, preserve_resolution: c.preserve_resolution,
advanced_3d: c.renderer == effectcraft_engine::project::Renderer::Advanced3D, advanced_3d: c.renderer == effectcraft_engine::project::Renderer::Advanced3D,
resolution: c.resolution,
..Default::default() ..Default::default()
}; };
app.dialog_state.editing_existing = true; app.dialog_state.editing_existing = true;
@@ -410,6 +411,11 @@ fn comp_settings(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens, exis
m.insert("comp".into(), json!(cid.0)); 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 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() { if !existing && r.is_ok() {
app.dialog_state.last_new_comp = Some(app.dialog_state.comp.clone()); app.dialog_state.last_new_comp = Some(app.dialog_state.comp.clone());
} }
@@ -422,6 +428,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. /// One frame of the open dialog with `key` pressed.
#[cfg(test)] #[cfg(test)]
pub(crate) fn press_key(app: &mut EffectcraftApp, ctx: &egui::Context, key: egui::Key, modifiers: egui::Modifiers) { pub(crate) fn press_key(app: &mut EffectcraftApp, ctx: &egui::Context, key: egui::Key, modifiers: egui::Modifiers) {
@@ -829,6 +844,59 @@ mod tests {
assert_eq!(selected(&ctx), Some(0..app.dialog_state.solid_name.chars().count())); 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] #[test]
fn composition_settings_register_every_control_and_new_comp_remembers() { fn composition_settings_register_every_control_and_new_comp_remembers() {
let (mut app, ctx) = app(); let (mut app, ctx) = app();
+4 -2
View File
@@ -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 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` 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 `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 and `resolutionFactor` (#417). Composition Settings ▸ Basic has the same Resolution menu, with
Composition Settings dialog has no Resolution popup. 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 ### Update: plug-ins and extensions as in After Effects; ease presets become a ScriptUI panel