From c822fcdff31c8d4b33ad112ac3b4c489a5de46e6 Mon Sep 17 00:00:00 2001 From: Nigel Ihza Firdausy <106657905+Ni-zav@users.noreply.github.com> Date: Sat, 10 Oct 2026 15:57:09 +0700 Subject: [PATCH] fix(render-queue): honor height-only locked resize edits (#541) --- crates/engine/src/commands/render_queue.rs | 15 +++++++++++++ crates/engine/src/rq_tests.rs | 25 ++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/crates/engine/src/commands/render_queue.rs b/crates/engine/src/commands/render_queue.rs index ab9a785a..67a3d851 100644 --- a/crates/engine/src/commands/render_queue.rs +++ b/crates/engine/src/commands/render_queue.rs @@ -583,6 +583,21 @@ fn set_output_module(s: &mut Session, p: &Value) -> Result { let m = module_p(s, i, p, "renderQueue.setOutputModule")?; let mut om = module_ref(&s.project.render_queue[i], m).clone(); let changed = apply_output(&mut om, &s.project.render_templates, s.state.region_of_interest, p, "renderQueue.setOutputModule")?; + + // When aspect is locked, an explicit height-only edit must drive the size. + // frame_size() derives height from width; calculate the matching width from the + // cropped render bounds before storing the updated module. + let width_given = p.get("resizeWidth").is_some() || p.get("resize").and_then(Value::as_object).is_some_and(|r| r.contains_key("width") || r.contains_key("preset")); + let height_given = p.get("resizeHeight").is_some() || p.get("resize").and_then(Value::as_object).is_some_and(|r| r.contains_key("height")); + if om.resize.enabled && om.resize.lock_aspect && height_given && !width_given { + let item = &s.project.render_queue[i]; + let comp = s.project.comp(item.comp).ok_or(EngineError::NoComp)?; + let (w, h) = item.settings.output_size(comp); + let mut crop_only = om.clone(); + crop_only.resize.enabled = false; + let (cw, ch) = crop_only.frame_size(w, h, item.settings.resolution.clamp(0.01, 4.0)); + om.resize.width = ((om.resize.height as f64 * cw as f64 / ch as f64).round() as u32).clamp(1, 30_000); + } let post = match str_p(p, "postRenderAction") { Some(a) => Some(post_render_parse(a).ok_or_else(|| bad("renderQueue.setOutputModule", "postRenderAction: none|import|importAndReplace|setProxy"))?), None => None, diff --git a/crates/engine/src/rq_tests.rs b/crates/engine/src/rq_tests.rs index 60a73acf..a8d11dfa 100644 --- a/crates/engine/src/rq_tests.rs +++ b/crates/engine/src/rq_tests.rs @@ -165,3 +165,28 @@ fn without_exporter() { assert_eq!(f["formats"].as_array().unwrap().len(), 12); assert_eq!(f["formats"][0]["available"], false); } + +#[test] +fn locked_resize_uses_height_for_height_only_output_module_updates() { + for change in [ + json!({"resizeHeight": 32}), + json!({"resize": {"height": 32, "lockAspect": true}}), + ] { + let mut s = Session::default(); + s.execute("comp.new", json!({"name":"Square","width":64,"height":64,"frameRate":10,"duration":1})).unwrap(); + s.execute("renderQueue.add", json!({})).unwrap(); + s.execute("renderQueue.setOutputModule", json!({"resize": {"width": 64, "lockAspect": true}})).unwrap(); + s.execute("renderQueue.setOutputModule", change).unwrap(); + let item = &s.project.render_queue[0]; + let comp = s.project.comp(item.comp).unwrap(); + assert_eq!(item.output.output_size(comp, &item.settings), (32, 32)); + assert_eq!(item.output.resize.height, 32); + } + let mut s = Session::default(); + s.execute("comp.new", json!({"name":"Square","width":64,"height":64,"duration":1})).unwrap(); + s.execute("renderQueue.add", json!({})).unwrap(); + s.execute("renderQueue.setOutputModule", json!({"resize": {"width": 32, "height": 54, "lockAspect": true}})).unwrap(); + let item = &s.project.render_queue[0]; + let comp = s.project.comp(item.comp).unwrap(); + assert_eq!(item.output.output_size(comp, &item.settings), (32, 32), "explicit width retains precedence"); +}