Merge pull request #1923 from marcoarnulfo/fix/move-angle-distance

Move dialog: Distance and Angle, linked to Horizontal and Vertical
This commit is contained in:
BFlat
2026-10-16 02:15:59 +07:00
committed by GitHub
14 changed files with 90 additions and 2 deletions
+3 -2
View File
@@ -48,7 +48,7 @@ pub(super) fn length(ui: &mut egui::Ui, d: &mut Dialog, key: &str, unit: Unit, w
/// The fields of the form dialogs ([`grid`]) that are distances, by dialog kind.
fn lengths(kind: &str) -> &'static [&'static str] {
match kind {
"move" => &["dx", "dy"],
"move" => &["dx", "dy", "distance"],
"rectangle" | "ellipse" | "rectangularGrid" | "polarGrid" | "artboardOptions" => &["width", "height"],
"roundedRectangle" => &["width", "height", "radius"],
"polygon" => &["radius"],
@@ -64,7 +64,7 @@ fn lengths(kind: &str) -> &'static [&'static str] {
/// number: fields that ↑/↓ step by one (Shift: ten) and that do math, as the distances do.
fn suffix(kind: &str, key: &str) -> Option<&'static str> {
match (kind, key) {
("rotate" | "shear" | "reflect", "angle") => Some("°"),
("rotate" | "shear" | "reflect" | "move", "angle") => Some("°"),
("scale", "sx" | "sy") => Some("%"),
_ => None,
}
@@ -74,6 +74,7 @@ fn suffix(kind: &str, key: &str) -> Option<&'static str> {
/// name).
fn order(kind: &str) -> &'static [&'static str] {
match kind {
"move" => &["dx", "dy", "distance", "angle"],
"offsetPath" => &["offset", "joins", "miterLimit"],
"rectangularGrid" => &["width", "height", "rows", "rowSkew", "columns", "columnSkew"],
"polarGrid" => &["width", "height", "concentric", "concentricSkew", "radial", "radialSkew"],
+75
View File
@@ -22,7 +22,13 @@ fn title(kind: &str) -> &'static str {
}
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
if d.kind == "move" {
link_distance_and_angle(d);
}
form::grid(ui, d, app.session.general_unit());
if d.kind == "move" {
link_distance_and_angle(d);
}
ui.add_space(6.0);
if d.kind == "scale" {
form::check(ui, d, "uniform", tl!("Uniform"));
@@ -38,6 +44,34 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
false
}
/// Move's Distance and Angle (fields `distance`, `angle`) and Horizontal and Vertical (`dx`, `dy`)
/// are the same offset: the pair edited since the last call (`__polar` holds the values then)
/// sets the other. The angle is counter-clockwise on the page and y points down, so 90° moves up.
fn link_distance_and_angle(d: &mut Dialog) {
let [dx, dy, distance, angle] = ["dx", "dy", "distance", "angle"].map(|k| d.f64(k, 0.0));
let last: Option<Vec<f64>> = d.fields.get("__polar").and_then(|v| serde_json::from_value(v.clone()).ok());
let polar_edited = last.as_deref().is_some_and(|l| l.get(2..) != Some(&[distance, angle][..]));
let [dx, dy, distance, angle] = if polar_edited {
let a = angle.to_radians();
// An angle past 180° reads as its negative one (270° is -90°), as the documented dialog has it.
let turn = angle.rem_euclid(360.0);
let angle = if turn > 180.0 { turn - 360.0 } else { turn };
[distance * a.cos(), -distance * a.sin(), distance, angle].map(clean)
} else {
[dx, dy, dx.hypot(dy), (-dy).atan2(dx).to_degrees()].map(clean)
};
for (k, v) in [("dx", dx), ("dy", dy), ("distance", distance), ("angle", angle)] {
d.fields.insert(k.into(), json!(v));
}
d.fields.insert("__polar".into(), json!([dx, dy, distance, angle]));
}
/// `v` without the float noise of the trigonometry (cos 90° is not quite 0). Adding 0 turns -0
/// into 0.
fn clean(v: f64) -> f64 {
(v * 1e6).round() / 1e6 + 0.0
}
/// Transform Patterns: the dialog's field, else General › Transform Pattern Tiles.
fn transform_patterns(app: &VectorcraftApp, d: &Dialog) -> bool {
d.fields.get("patterns").and_then(Value::as_bool).unwrap_or(app.session.prefs.transform_pattern_tiles)
@@ -261,6 +295,47 @@ mod tests {
}
}
/// #1915: Move's Distance and Angle follow Horizontal and Vertical, and they follow them back.
/// The angle is counter-clockwise on the page and Vertical is down = +, so 90° moves up.
#[test]
fn move_takes_a_distance_and_an_angle() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 300})).unwrap();
app.run("shape.rectangle", json!({"x": 100, "y": 100, "width": 60, "height": 20})).unwrap();
let ctx = egui::Context::default();
crate::theme::install_fonts(&ctx);
let frame = |app: &mut VectorcraftApp| {
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| crate::dialogs::show(app, ui.ctx()));
out.textures_delta.clear();
};
let set = |app: &mut VectorcraftApp, values: [(&str, f64); 2]| {
app.ui.dialog.as_mut().unwrap().fields.extend(values.map(|(k, v)| (k.to_string(), json!(v))));
};
let fields = |app: &VectorcraftApp| {
let d = app.ui.dialog.as_ref().unwrap();
["dx", "dy", "distance", "angle"].map(|k| d.f64(k, f64::NAN))
};
crate::menus::invoke(&mut app, "object.move", json!({}));
frame(&mut app);
assert_eq!(fields(&app), [0.0, 0.0, 0.0, 0.0]);
set(&mut app, [("distance", 10.0), ("angle", 90.0)]);
frame(&mut app);
assert_eq!(fields(&app), [0.0, -10.0, 10.0, 90.0]);
set(&mut app, [("dx", 3.0), ("dy", 4.0)]);
frame(&mut app);
assert_eq!(fields(&app), [3.0, 4.0, 5.0, -53.130102]);
set(&mut app, [("distance", 10.0), ("angle", 270.0)]);
frame(&mut app);
assert_eq!(fields(&app), [0.0, 10.0, 10.0, -90.0], "270° is -90°");
set(&mut app, [("distance", 10.0), ("angle", 90.0)]);
frame(&mut app);
crate::dialogs::confirm(&mut app).unwrap();
let (cmd, p) = app.session.journal.last().unwrap();
assert_eq!((cmd.as_str(), &p["dx"], &p["dy"]), ("object.move", &json!(0.0), &json!(-10.0)));
let b = app.session.doc().unwrap().doc.layers[0].children().unwrap()[0].geometric_bounds().unwrap();
assert_eq!((b.x0, b.y0), (100.0, 90.0));
}
/// Transform Patterns starts from General › Transform Pattern Tiles (#394) and goes with the
/// command.
#[test]
+1
View File
@@ -4399,3 +4399,4 @@ theme Appearance Erscheinungsbild
@msg Unknown unit Unbekannte Einheit
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. Schließt die kreisförmigen Enden offener Profile. Bei einer Teildrehung bleiben die Schnittseiten offen.
Distance Abstand
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4811,3 +4811,4 @@ mark-style Japanese Japonesas
@msg Unknown unit Unidad desconocida
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. Cierra los extremos circulares de los perfiles abiertos. Los giros parciales mantienen abiertos sus lados de corte.
Distance Distancia
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4836,3 +4836,4 @@ imageTrace Outline Körvonal
@msg Unknown unit Ismeretlen mértékegység
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. Nyitott profilok kör alakú végeinek lezárása. Részleges körforgatás esetén a vágott oldalak nyitva maradnak.
Distance Távolság
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4444,3 +4444,4 @@ lighting Light Luce
@msg Unknown unit Unità sconosciuta
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. Chiude le estremità circolari dei profili aperti. Le rotazioni parziali mantengono aperti i lati di taglio.
Distance Distanza
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4439,3 +4439,4 @@ lighting Light ライト
@msg Unknown unit 単位が不明です
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. 開いたプロファイルの円形の端を閉じます。部分的な回転では切断面は開いたままです。
Distance 距離
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4408,3 +4408,4 @@ lighting Light 광원
@msg Unknown unit 알 수 없는 단위
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. 열린 프로파일의 원형 끝을 닫습니다. 부분 회전에서는 절단 측면이 열린 상태로 유지됩니다.
Distance 거리
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4711,3 +4711,4 @@ mark-style Japanese Japońskie
@msg Unknown unit Nieznana jednostka
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. Zamknij okrągłe końce otwartych profili. Przy częściowym obrocie boki przekroju pozostają otwarte.
Distance Odległość
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4440,3 +4440,4 @@ lighting Light Свет
@msg Unknown unit Неизвестная единица
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. Закрывает круглые торцы открытых профилей. При неполном вращении боковые срезы остаются открытыми.
Distance Расстояние
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -4424,3 +4424,4 @@ lighting Light Світло
@msg Unknown unit Невідома одиниця
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. Закриває круглі торці відкритих профілів. За неповного обертання бічні зрізи залишаються відкритими.
Distance Відстань
Can't render this file because it contains an unexpected character in line 713 and column 40.
+1
View File
@@ -4423,3 +4423,4 @@ theme Appearance Giao diện
@msg Unknown unit Đơn vị không xác định
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. Đóng các đầu tròn của biên dạng mở. Khi xoay một phần, các mặt cắt bên vẫn mở.
Distance Khoảng cách
Can't render this file because it contains an unexpected character in line 3696 and column 10.
+1
View File
@@ -3999,3 +3999,4 @@ lighting Light 光源
@msg Choose a precision from 0 to 6 请选择 0 到 6 之间的精度
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. 封闭开放轮廓的圆形端部。部分旋转的切割侧面保持开放。
Distance 距离
Can't render this file because it contains an unexpected character in line 2 and column 73.
+1
View File
@@ -3785,3 +3785,4 @@ lighting Light 光源
Y: Y:
# Revolve cap description
Close the circular ends of open profiles. Partial sweeps keep their cut sides open. 封閉開放輪廓的圓形端部。部分旋轉的切割側面保持開放。
Distance 距離
Can't render this file because it contains an unexpected character in line 6 and column 47.