M4.9: Preserve frame rate when nested or in render queue, Preserve resolution when nested

Composition Settings ▸ Advanced has both options (they were missing from the model, the
renderer and the dialog):
- Preserve frame rate: nested in another comp, the comp shows only its own frames
  (`EvalCtx::nested_time` floors the precomp layer's source time to the nested comp's frame
  rate; used for the nested frame, its proxy, aux channels, collapsed transformations and the
  viewer's internal wireframes), and the Render Queue renders it at its own rate whatever the
  Frame Rate override (`RenderSettings::rate`).
- Preserve resolution: nested in a comp rendered at Half / Third / Quarter, it renders at full
  size and the buffer's scale places it.
- `comp.settings {preserveFrameRate, preserveResolution}`, `comp.info` reports them, scripts
  read and set `CompItem.preserveNestedFrameRate` / `preserveNestedResolution`, Pre-compose
  copies them, and the dialog registers `dialog.comp.preserveFrameRate` /
  `dialog.comp.preserveResolution`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic
2026-10-05 01:33:14 +07:00
co-authored by Claude Opus 5.5
parent 4e98b8c4bd
commit 0fe6db00e8
14 changed files with 176 additions and 11 deletions
+8 -2
View File
@@ -90,6 +90,12 @@ fn apply_settings(c: &mut Comp, p: &Value) {
if let Some(n) = p.get("adaptiveSampleLimit").and_then(Value::as_u64) {
c.motion_blur_adaptive_limit = (n as u32).clamp(16, 256);
}
if let Some(b) = p.get("preserveFrameRate").and_then(Value::as_bool) {
c.preserve_frame_rate = b;
}
if let Some(b) = p.get("preserveResolution").and_then(Value::as_bool) {
c.preserve_resolution = b;
}
}
fn new_comp(s: &mut Session, p: &Value) -> Result<Value> {
@@ -252,7 +258,7 @@ pub fn specs() -> Vec<CommandSpec> {
"New Composition...",
["Composition"],
Some("Cmd+N"),
"{name?, width?, height?, frameRate?, duration? (s), startTime? (s) | startTimecode?, background? [r,g,b]|#hex, pixelAspect?, shutterAngle?, shutterPhase?, motionBlurSamples?, adaptiveSampleLimit? (16–256), renderer? classic3D|advanced3D, anchor?, open?}",
"{name?, width?, height?, frameRate?, duration? (s), startTime? (s) | startTimecode?, background? [r,g,b]|#hex, pixelAspect?, shutterAngle?, shutterPhase?, motionBlurSamples?, adaptiveSampleLimit? (16–256), preserveFrameRate?: bool, preserveResolution?: bool, renderer? classic3D|advanced3D, anchor?, open?}",
always,
new_comp
),
@@ -261,7 +267,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Composition Settings...",
["Composition"],
Some("Cmd+K"),
"{comp?, name?, width?, height?, anchor? 0-8 (resize anchor, 4 = center), frameRate?, duration?, startTime? (s) | startTimecode?, background?, shutterAngle?, shutterPhase?, motionBlurSamples?, adaptiveSampleLimit? (16–256), pixelAspect?, renderer? classic3D|advanced3D}",
"{comp?, name?, width?, height?, anchor? 0-8 (resize anchor, 4 = center), frameRate?, duration?, startTime? (s) | startTimecode?, background?, shutterAngle?, shutterPhase?, motionBlurSamples?, adaptiveSampleLimit? (16–256), preserveFrameRate?: bool (nested or in the render queue it shows only its own frames), preserveResolution?: bool (nested, it renders at full size), pixelAspect?, renderer? classic3D|advanced3D}",
has_comp,
settings
),
+1
View File
@@ -79,6 +79,7 @@ fn comp_info(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({
"id": cid.0, "name": name, "width": c.width, "height": c.height, "frameRate": c.frame_rate.as_f64(), "duration": c.duration.seconds(),
"workArea": [c.work_area.0.seconds(), c.work_area.1.seconds()], "background": c.background,
"preserveFrameRate": c.preserve_frame_rate, "preserveResolution": c.preserve_resolution,
"layers": c.layers.iter().enumerate().map(|(i, l)| layer_json(l, i + 1)).collect::<Vec<_>>(),
}))
}
+21
View File
@@ -811,3 +811,24 @@ fn project_files_keep_their_format_warn_about_newer_versions_and_never_save_unre
let e = s.execute("file.open", json!({"path": junk})).unwrap_err().to_string();
assert!(e.contains("is not a project EffectCraft can open"), "{e}");
}
#[test]
fn comp_settings_preserve_frame_rate_and_resolution() {
let mut s = comp();
let info = s.execute("comp.info", json!({})).unwrap();
assert_eq!((info["preserveFrameRate"].clone(), info["preserveResolution"].clone()), (json!(false), json!(false)));
s.execute_checked("comp.settings", json!({"preserveFrameRate": true, "preserveResolution": true})).unwrap();
let info = s.execute("comp.info", json!({})).unwrap();
assert_eq!((info["preserveFrameRate"].clone(), info["preserveResolution"].clone()), (json!(true), json!(true)));
// Pre-compose keeps them; one undo step clears them.
let a = solid(&mut s, "#ff0000");
let r = s.execute("layer.precompose", json!({"layers": [a], "name": "Inner"})).unwrap();
let inner = s.project.comp(effectcraft_project::ItemId(r["comp"].as_u64().unwrap())).unwrap();
assert!(inner.preserve_frame_rate && inner.preserve_resolution);
// Undo Pre-compose and the solid; then one more step undoes the settings.
s.execute("edit.undo", json!({})).unwrap();
s.execute("edit.undo", json!({})).unwrap();
assert!(s.active_comp().unwrap().preserve_frame_rate);
s.execute("edit.undo", json!({})).unwrap();
assert!(!s.active_comp().unwrap().preserve_frame_rate && !s.active_comp().unwrap().preserve_resolution);
}