Fix signed angle scrubbing across zero and monitor wraps
This commit is contained in:
parent
adbe4dff4e
commit
1c2bf2f157
|
|
@ -51,7 +51,10 @@ pub fn angle_field(ui: &mut egui::Ui, at: Pos2, id: egui::Id, v: f64, decimals:
|
|||
let (dr, new_deg, _) = widgets::hot_number_at(ui, pos2(sign.max.x - 2.0, at.y), id, deg.abs(), 0.5, (-1e9, 1e9), decimals, "°", t);
|
||||
let out = match (new_rev, new_deg) {
|
||||
(Some(r), _) => Some(r as f64 * 360.0 + deg),
|
||||
// The degrees field shows the remainder's size; its sign stays.
|
||||
// Scrubbing changes the full signed angle. Reapplying the displayed
|
||||
// remainder's sign would reverse the next delta after crossing zero.
|
||||
(None, Some(d)) if ui.ctx().is_being_dragged(id) => Some(v + (d - deg.abs())),
|
||||
// Typing replaces the remainder's size and keeps its displayed sign.
|
||||
(None, Some(d)) => Some(rev as f64 * 360.0 + if deg < 0.0 || (deg == 0.0 && v < 0.0) { -d } else { d }),
|
||||
(None, None) => None,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -187,6 +187,8 @@ mod tests {
|
|||
warps: Vec<Pos2>,
|
||||
standard: bool,
|
||||
ordinary: bool,
|
||||
angle: bool,
|
||||
angle_rects: Option<(Rect, Rect)>,
|
||||
modifiers: Modifiers,
|
||||
}
|
||||
|
||||
|
|
@ -201,6 +203,8 @@ mod tests {
|
|||
warps: Vec::new(),
|
||||
standard: false,
|
||||
ordinary: false,
|
||||
angle: false,
|
||||
angle_rects: None,
|
||||
modifiers: Modifiers::NONE,
|
||||
}
|
||||
}
|
||||
|
|
@ -224,6 +228,19 @@ mod tests {
|
|||
);
|
||||
} else if self.ordinary {
|
||||
ui.interact(Rect::from_min_size(pos2(100.0, 100.0), vec2(60.0, 20.0)), Id::new("ordinary"), egui::Sense::drag());
|
||||
} else if self.angle {
|
||||
let (rr, dr, value) = crate::panels::fx_widgets::angle_field(
|
||||
ui,
|
||||
pos2(100.0, 100.0),
|
||||
Id::new("scrub"),
|
||||
self.value,
|
||||
1,
|
||||
&crate::theme::Tokens::for_kind(crate::theme::ThemeKind::Dark),
|
||||
);
|
||||
self.angle_rects = Some((rr, dr));
|
||||
if let Some(v) = value {
|
||||
self.value = v;
|
||||
}
|
||||
} else {
|
||||
let (_, value, _) = crate::widgets::hot_number_at(
|
||||
ui,
|
||||
|
|
@ -262,6 +279,42 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn angle_degrees_continue_across_zero_and_repeated_monitor_wraps() {
|
||||
for initial in [0.0, 359.5, -359.5, 720.0, -720.0] {
|
||||
for direction in [-1.0, 1.0] {
|
||||
let mut s = Scrub::new();
|
||||
s.angle = true;
|
||||
s.value = initial;
|
||||
s.frame(vec![], true);
|
||||
let p = s.angle_rects.unwrap().1.center();
|
||||
s.frame(
|
||||
vec![Event::PointerMoved(p), Event::PointerButton { pos: p, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE }],
|
||||
true,
|
||||
);
|
||||
let mut last = p;
|
||||
let mut expected = initial;
|
||||
// Feed small real movements, including zero/whole-turn crossings, then
|
||||
// the synthetic OS warp. Every real pixel contributes exactly once.
|
||||
for step in 0..800 {
|
||||
let direction = if step < 400 { direction } else { -direction };
|
||||
let next = pos2((last.x + direction * 13.0).clamp(1.0, 799.0), last.y);
|
||||
expected += f64::from(next.x - last.x) * 0.5;
|
||||
let count = s.warps.len();
|
||||
s.move_to(next);
|
||||
assert!((s.value - expected).abs() < 1e-6, "initial={initial}, direction={direction}, actual={}, expected={expected}", s.value);
|
||||
last = next;
|
||||
if s.warps.len() > count {
|
||||
last = *s.warps.last().unwrap();
|
||||
s.move_to(last);
|
||||
assert!((s.value - expected).abs() < 1e-6, "the teleport must not reset the angle");
|
||||
}
|
||||
}
|
||||
assert!(s.warps.len() >= 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numeric_drag_wraps_repeatedly_without_counting_the_teleport() {
|
||||
let mut s = Scrub::new();
|
||||
|
|
@ -385,13 +438,13 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn real_properties_and_textbox_drags_keep_each_gesture_in_one_undo_step() {
|
||||
fn real_properties_angles_and_textbox_drags_keep_each_gesture_in_one_undo_step() {
|
||||
use crate::{EffectcraftApp, dock::PanelKind};
|
||||
use effectcraft_engine::project::LayerId;
|
||||
use eframe::App;
|
||||
use serde_json::json;
|
||||
|
||||
for textbox in [false, true] {
|
||||
for (textbox, angle) in [(false, false), (false, true), (true, false)] {
|
||||
let mut session = effectcraft_host::session();
|
||||
// This patch also works without the optional TextBox plugin.
|
||||
if textbox && effectcraft_engine::effects::plugin::plugin("org.effectcraft.text-box").is_none() {
|
||||
|
|
@ -402,13 +455,25 @@ mod tests {
|
|||
if textbox {
|
||||
session.execute("effect.apply", json!({"layer": lid, "effect": "TextBox"})).unwrap();
|
||||
}
|
||||
let path = if textbox { "effects/#1/paddingX" } else { "transform/position" };
|
||||
let path = if textbox {
|
||||
"effects/#1/paddingX"
|
||||
} else if angle {
|
||||
"transform/rotation"
|
||||
} else {
|
||||
"transform/position"
|
||||
};
|
||||
let prop = session.active_comp().unwrap().layer(LayerId(lid)).unwrap().props.prop(path).unwrap();
|
||||
let uid = prop.uid;
|
||||
let original = prop.value.clone();
|
||||
let undo = session.history.undo.len();
|
||||
let panel = if textbox { PanelKind::EffectControls } else { PanelKind::Properties };
|
||||
let id = if textbox { format!("effectControls.prop.{uid}.value") } else { format!("properties.prop.{uid}.value.0") };
|
||||
let id = if textbox {
|
||||
format!("effectControls.prop.{uid}.value")
|
||||
} else if angle {
|
||||
format!("properties.prop.{uid}.value")
|
||||
} else {
|
||||
format!("properties.prop.{uid}.value.0")
|
||||
};
|
||||
let mut app = EffectcraftApp::new(session);
|
||||
app.show_panel(panel);
|
||||
app.toggle_maximize(panel);
|
||||
|
|
@ -435,21 +500,27 @@ mod tests {
|
|||
step(&mut app, vec![]);
|
||||
}
|
||||
for gesture in 1..=2 {
|
||||
let direction = if gesture == 1 { -1.0 } else { 1.0 };
|
||||
let r = app.auto.find(&id).unwrap().rect;
|
||||
let p = pos2(r[0] + r[2] / 2.0, r[1] + r[3] / 2.0);
|
||||
step(
|
||||
&mut app,
|
||||
vec![Event::PointerMoved(p), Event::PointerButton { pos: p, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE }],
|
||||
);
|
||||
step(&mut app, vec![Event::PointerMoved(p + vec2(10.0, 0.0))]);
|
||||
step(&mut app, vec![Event::PointerMoved(p + vec2(direction * 10.0, 0.0))]);
|
||||
let mut last = p;
|
||||
for _ in 0..3 {
|
||||
let target = step(&mut app, vec![Event::PointerMoved(pos2(1599.0, p.y))]).expect("real property scrubs wrap");
|
||||
let edge = if direction < 0.0 { 1.0 } else { 1599.0 };
|
||||
let target = step(&mut app, vec![Event::PointerMoved(pos2(edge, p.y))]).expect("real property scrubs wrap");
|
||||
let before = app.session.active_comp().unwrap().layer(LayerId(lid)).unwrap().props.find(uid).unwrap().value.clone();
|
||||
step(&mut app, vec![Event::PointerMoved(target)]);
|
||||
assert_eq!(app.session.active_comp().unwrap().layer(LayerId(lid)).unwrap().props.find(uid).unwrap().value, before);
|
||||
last = target + vec2(10.0, 0.0);
|
||||
last = target + vec2(direction * 10.0, 0.0);
|
||||
step(&mut app, vec![Event::PointerMoved(last)]);
|
||||
if angle {
|
||||
let value = app.session.active_comp().unwrap().layer(LayerId(lid)).unwrap().props.find(uid).unwrap().value.as_f64();
|
||||
assert!((value - before.as_f64() - f64::from(direction) * 5.0).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
step(&mut app, vec![Event::PointerButton { pos: last, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE }]);
|
||||
assert_eq!(app.session.history.undo.len(), undo + gesture, "each drag is one undo step: {panel:?}");
|
||||
|
|
|
|||
Loading…
Reference in New Issue