Merge pull request #1029 from vbasky/fix/956-scale-unlink

This commit is contained in:
Brandon Thomas committed 2026-10-13 04:27:24 -04:00
commit c069a72928
2 files changed
+52 -11

No files matched your search

+14 -11
View File
@@ -277,16 +277,18 @@ fn transform_row(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter,
let c = value.components();
let n = if pr.shown_dims > 0 && !(layer.is_3d() && c.len() == 3) { pr.shown_dims as usize } else { c.len() };
let mut x = vx;
let link_id = egui::Id::new(("props-scale-link", lid));
let linked = is_scale && ui.data(|d| d.get_temp::<bool>(link_id)).unwrap_or(true);
if is_scale {
// Scale: the chain link (Constrain Proportions, on by default), sharing the
// Timeline's per-property state so both panels agree (#956).
let linkable = pr.match_id == "scale";
let linked = linkable && !app.ui.timeline.unlinked.contains(&uid);
if linkable {
// Constrain Proportions toggle, left of the values.
let lr = Rect::from_center_size(pos2(x - 12.0, cy), vec2(12.0, 12.0));
let resp = ui.interact(lr, link_id.with("btn"), Sense::click());
let resp = ui.interact(lr, egui::Id::new(("props-link", uid)), Sense::click()).on_hover_text(tr("Constrain Proportions"));
icons::paint(p, lr, Icon::Link, if linked { t.accent } else { t.text_faint });
app.auto.add(&format!("{base}.constrain"), lr, "Constrain Proportions");
if resp.clicked() {
ui.data_mut(|d| d.insert_temp(link_id, !linked));
if resp.clicked() && !app.ui.timeline.unlinked.remove(&uid) {
app.ui.timeline.unlinked.insert(uid);
}
}
for d in 0..n.min(c.len()) {
@@ -295,13 +297,14 @@ fn transform_row(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter,
app.auto.add(&format!("{base}.value.{d}"), vr, &pr.name);
if let Some(nv) = nv {
let mut nc = c.clone();
if linked && c[d].abs() > 1e-9 {
let k = nv / c[d];
for v in nc.iter_mut().take(n) {
*v *= k;
// Alt edits one value of a linked pair, as in the Timeline.
let shown = if linked && !ui.input(|i| i.modifiers.alt && !widgets::proportional_scrub(i.modifiers)) { n.min(c.len()) } else { 1 };
let from = if shown == 1 { d } else { 0 };
for (e, v) in widgets::constrained(c.get(from..from + shown).unwrap_or_default(), d - from, nv).into_iter().enumerate() {
if let Some(slot) = nc.get_mut(from + e) {
*slot = v;
}
}
nc[d] = nv;
set(actions, json!(nc));
}
x = vr.max.x + 10.0;
+38
View File
@@ -466,6 +466,44 @@ fn essential_graphics_scale_keeps_constrain_proportions() {
assert!(unlinked[0] > linked[0] && unlinked[1] == linked[1], "unlinked, X moves alone: {unlinked:?}");
}
/// #956: the Properties panel's Scale chain link shares the Timeline's Constrain Proportions
/// state: unlinking in Properties unlinks the Timeline row too, and X/Y scale together only
/// while linked.
#[test]
fn properties_scale_link_shares_the_timeline_state() {
let mut h = harness();
let layer = h.state().session.active_comp().unwrap().layers[0].id.0;
h.state_mut().session.execute("layer.select", json!({"layers": [layer]})).unwrap();
h.state_mut().session.execute("prop.set", json!({"layer": layer, "path": "transform/scale", "value": [100.0, 50.0]})).unwrap();
open(&mut h, "properties");
let uid = {
let comp = h.state().session.active_comp().unwrap();
let l = comp.layers.iter().find(|l| l.id.0 == layer).unwrap();
l.props.prop("transform/scale").unwrap().uid
};
let link = format!("properties.prop.{uid}.constrain");
assert!(h.state().auto.find(&link).is_some(), "the chain link is drawn");
let scale = |h: &Harness<'_, EffectcraftApp>| {
let comp = h.state().session.active_comp().unwrap();
let l = comp.layers.iter().find(|l| l.id.0 == layer).unwrap();
l.props.prop("transform/scale").unwrap().value.components()
};
// Linked: dragging X scales Y with it.
drag_x(&mut h, &format!("properties.prop.{uid}.value.0"), 40.0);
let linked = scale(&h);
assert!(linked[0] > 100.0, "{linked:?}");
assert!((linked[1] / linked[0] - 0.5).abs() < 1e-6, "Y follows X in proportion: {linked:?}");
// Unlinking in Properties writes the shared state, so the Timeline row unlinks too.
click(&mut h, &link);
assert!(h.state().ui.timeline.unlinked.contains(&uid), "unlinking writes the shared state");
drag_x(&mut h, &format!("properties.prop.{uid}.value.0"), 40.0);
let unlinked = scale(&h);
assert!(unlinked[0] > linked[0] && unlinked[1] == linked[1], "unlinked, X moves alone: {unlinked:?}");
// Re-linking clears it again.
click(&mut h, &link);
assert!(!h.state().ui.timeline.unlinked.contains(&uid), "re-linking clears the shared state");
}
/// #757: Effects & Presets lists the built-in Transitions presets under * Animation Presets;
/// double-clicking one applies it to the selected layer (effect plus keyframes).
#[test]