|
|
|
@@ -2,13 +2,17 @@
|
|
|
|
|
//! aligning to a key object) and Align To (selection / key object / artboard).
|
|
|
|
|
|
|
|
|
|
use egui::Ui;
|
|
|
|
|
use serde::Serialize;
|
|
|
|
|
use serde_json::{Value, json};
|
|
|
|
|
|
|
|
|
|
use super::{pstate, selection_len, set_pstate};
|
|
|
|
|
use crate::VectorcraftApp;
|
|
|
|
|
use crate::widgets::{self, menu_item};
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
|
|
|
/// What Align aligns to. The Align To choice kept with the UI state ([`crate::UiState::align_to`])
|
|
|
|
|
/// is Selection or Artboard: Align to Key Object stores no choice.
|
|
|
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize)]
|
|
|
|
|
#[serde(rename_all = "lowercase")]
|
|
|
|
|
pub enum AlignTo {
|
|
|
|
|
#[default]
|
|
|
|
|
Selection,
|
|
|
|
@@ -26,14 +30,34 @@ impl AlignTo {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Serde reader for the saved Align To choice ([`crate::UiState::align_to`]): `"artboard"` is
|
|
|
|
|
/// Artboard, and any other value, `"key"` included, is Selection.
|
|
|
|
|
pub fn deserialize_align_to<'de, D: serde::Deserializer<'de>>(d: D) -> Result<AlignTo, D::Error> {
|
|
|
|
|
let v = <Value as serde::Deserialize>::deserialize(d)?;
|
|
|
|
|
Ok(if v == "artboard" { AlignTo::Artboard } else { AlignTo::Selection })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// What Align aligns to with `n_selected` objects selected: the artboard for a single object, else
|
|
|
|
|
/// `to`.
|
|
|
|
|
fn aligns_to(to: AlignTo, n_selected: usize) -> AlignTo {
|
|
|
|
|
if n_selected == 1 { AlignTo::Artboard } else { to }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// `object.align` params: a single object aligns to the artboard (Illustrator does the same).
|
|
|
|
|
pub fn align_params(base: Value, to: AlignTo, n_selected: usize) -> Value {
|
|
|
|
|
let mut p = base;
|
|
|
|
|
let to = if n_selected == 1 { AlignTo::Artboard } else { to };
|
|
|
|
|
p["to"] = json!(to.param());
|
|
|
|
|
p["to"] = json!(aligns_to(to, n_selected).param());
|
|
|
|
|
p
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// `object.align` params `base` with the `to` that the align buttons of the Align panel, the Control
|
|
|
|
|
/// bar and the Properties panel send: the artboard for a single selected object, else the key object
|
|
|
|
|
/// while there is one, else the Align To choice. Object → Align sends it too while every selected
|
|
|
|
|
/// object is selected whole ([`crate::menus::invoke`]).
|
|
|
|
|
pub fn with_align_to(app: &VectorcraftApp, base: Value) -> Value {
|
|
|
|
|
align_params(base, align_to(app), selection_len(app))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub const ALIGN: [(&str, &str, &str, &str); 6] = [
|
|
|
|
|
("dc-al-left", "Horizontal Align Left", "horizontal", "left"),
|
|
|
|
|
("dc-al-hcenter", "Horizontal Align Center", "horizontal", "center"),
|
|
|
|
@@ -56,8 +80,6 @@ pub const DISTRIBUTE: [(&str, &str, &str, &str); 6] = [
|
|
|
|
|
/// They align to what the Align panel's Align To says (the key object while there is one), as
|
|
|
|
|
/// its own buttons do (#789).
|
|
|
|
|
pub fn align_buttons(app: &mut VectorcraftApp, ui: &mut Ui, size: f32) {
|
|
|
|
|
let n = selection_len(app);
|
|
|
|
|
let to = align_to(app, ui.ctx());
|
|
|
|
|
for (i, (icon, tip, axis, v)) in [
|
|
|
|
|
("align-start-vertical", "Horizontal Align Left", "horizontal", "left"),
|
|
|
|
|
("align-center-vertical", "Horizontal Align Center", "horizontal", "center"),
|
|
|
|
@@ -70,10 +92,10 @@ pub fn align_buttons(app: &mut VectorcraftApp, ui: &mut Ui, size: f32) {
|
|
|
|
|
.enumerate()
|
|
|
|
|
{
|
|
|
|
|
if i == 3 && widgets::icon_button(ui, "dc-al-center", tl!("Horizontal & Vertical Align Center"), false, size).clicked() {
|
|
|
|
|
app.run("object.align", align_params(json!({"horizontal": "center", "vertical": "center"}), to, n)).ok();
|
|
|
|
|
app.run("object.align", with_align_to(app, json!({"horizontal": "center", "vertical": "center"}))).ok();
|
|
|
|
|
}
|
|
|
|
|
if widgets::icon_button(ui, icon, tl!(tip), false, size).clicked() {
|
|
|
|
|
app.run("object.align", align_params(json!({axis: v}), to, n)).ok();
|
|
|
|
|
app.run("object.align", with_align_to(app, json!({axis: v}))).ok();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -91,13 +113,13 @@ fn topmost(app: &VectorcraftApp) -> Option<vectorcraft_doc::NodeId> {
|
|
|
|
|
/// What Align aligns to: the key object while there is one (a click on an object of the
|
|
|
|
|
/// selection makes it the key, and Align to Key Object makes the topmost one the key), else the
|
|
|
|
|
/// stored Align To choice, Selection or Artboard.
|
|
|
|
|
fn align_to(app: &VectorcraftApp, ctx: &egui::Context) -> AlignTo {
|
|
|
|
|
if has_key(app) { AlignTo::Key } else { pstate(ctx, "align-to") }
|
|
|
|
|
fn align_to(app: &VectorcraftApp) -> AlignTo {
|
|
|
|
|
if has_key(app) { AlignTo::Key } else { app.ui.align_to }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|
|
|
|
let n = selection_len(app);
|
|
|
|
|
let to = align_to(app, ui.ctx());
|
|
|
|
|
let to = align_to(app);
|
|
|
|
|
widgets::subheader(ui, tl!("Align Objects:"));
|
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
ui.spacing_mut().item_spacing.x = 5.0;
|
|
|
|
@@ -105,12 +127,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|
|
|
|
if i == 3 {
|
|
|
|
|
ui.add_space(6.0);
|
|
|
|
|
if widgets::icon_button_enabled(ui, "dc-al-center", tl!("Horizontal & Vertical Align Center"), false, n >= 1, 32.0).clicked() {
|
|
|
|
|
app.run("object.align", align_params(json!({"horizontal": "center", "vertical": "center"}), to, n)).ok();
|
|
|
|
|
app.run("object.align", with_align_to(app, json!({"horizontal": "center", "vertical": "center"}))).ok();
|
|
|
|
|
}
|
|
|
|
|
ui.add_space(6.0);
|
|
|
|
|
}
|
|
|
|
|
if widgets::icon_button_enabled(ui, icon, tl!(tip), false, n >= 1, 32.0).clicked() {
|
|
|
|
|
app.run("object.align", align_params(json!({*axis: v}), to, n)).ok();
|
|
|
|
|
app.run("object.align", with_align_to(app, json!({*axis: v}))).ok();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
@@ -155,6 +177,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|
|
|
|
ui.separator();
|
|
|
|
|
ui.vertical(|ui| {
|
|
|
|
|
widgets::subheader(ui, tl!("Align To:"));
|
|
|
|
|
// With one object selected the buttons align it to the artboard, and Align To shows Artboard.
|
|
|
|
|
let shown = aligns_to(to, n);
|
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
for (v, icon, tip) in [
|
|
|
|
|
(AlignTo::Selection, "dc-alignto-selection", tl!("Align to Selection")),
|
|
|
|
@@ -163,7 +187,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|
|
|
|
] {
|
|
|
|
|
// A key object is one of two or more selected objects.
|
|
|
|
|
let on = v != AlignTo::Key || n >= 2;
|
|
|
|
|
if widgets::icon_button_enabled(ui, icon, tl!(tip), to == v, on, 28.0).clicked() {
|
|
|
|
|
if widgets::icon_button_enabled(ui, icon, tl!(tip), shown == v, on, 28.0).clicked() {
|
|
|
|
|
if v == AlignTo::Key {
|
|
|
|
|
// The topmost selected object becomes the key object, unless
|
|
|
|
|
// there is one already. The Align To choice stays as it was, and
|
|
|
|
@@ -174,7 +198,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|
|
|
|
app.run("select.key", json!({"id": id.0})).ok();
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
set_pstate(ui.ctx(), "align-to", v);
|
|
|
|
|
app.ui.align_to = v;
|
|
|
|
|
// Aligning to the selection or the artboard lets go of the key object.
|
|
|
|
|
if has_key(app) {
|
|
|
|
|
app.run("select.key", json!({})).ok();
|
|
|
|
@@ -197,7 +221,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|
|
|
|
super::transform::set_pref(app, "usePreviewBounds", !pb);
|
|
|
|
|
}
|
|
|
|
|
// Cancel Key Object clears the key, and Align To shows its stored choice again.
|
|
|
|
|
if menu_item(ui, tl!("Cancel Key Object"), align_to(app, ui.ctx()) == AlignTo::Key, false) {
|
|
|
|
|
if menu_item(ui, tl!("Cancel Key Object"), align_to(app) == AlignTo::Key, false) {
|
|
|
|
|
app.run("select.key", json!({})).ok();
|
|
|
|
|
}
|
|
|
|
|
menu_item(ui, tl!("Align to Glyph Bounds"), false, false);
|
|
|
|
@@ -252,7 +276,7 @@ mod tests {
|
|
|
|
|
frame(&mut app, vec![press(false)]);
|
|
|
|
|
assert_eq!(bounds(&app).x1, 200.0, "the artboard's right edge");
|
|
|
|
|
// The Align panel's Horizontal Align Left.
|
|
|
|
|
let p = align_params(json!({"horizontal": "left", "bounds": "geometric"}), align_to(&app, &ctx), selection_len(&app));
|
|
|
|
|
let p = with_align_to(&app, json!({"horizontal": "left", "bounds": "geometric"}));
|
|
|
|
|
app.run("object.align", p).unwrap();
|
|
|
|
|
assert_eq!(bounds(&app).x0, 0.0, "the artboard's left edge");
|
|
|
|
|
}
|
|
|
|
@@ -274,12 +298,11 @@ mod tests {
|
|
|
|
|
let a = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 10, "height": 10})).unwrap()["id"].clone();
|
|
|
|
|
let b = app.run("shape.rectangle", json!({"x": 80, "y": 50, "width": 30, "height": 30})).unwrap()["id"].clone();
|
|
|
|
|
app.run("select.set", json!({"ids": [a, b]})).unwrap();
|
|
|
|
|
let ctx = egui::Context::default();
|
|
|
|
|
assert_eq!(align_to(&app, &ctx), AlignTo::Selection);
|
|
|
|
|
assert_eq!(align_to(&app), AlignTo::Selection);
|
|
|
|
|
app.run("select.key", json!({"id": b})).unwrap();
|
|
|
|
|
assert_eq!(align_to(&app, &ctx), AlignTo::Key);
|
|
|
|
|
assert_eq!(align_to(&app), AlignTo::Key);
|
|
|
|
|
app.run("select.key", json!({})).unwrap();
|
|
|
|
|
assert_eq!(align_to(&app, &ctx), AlignTo::Selection, "no key: the choice again");
|
|
|
|
|
assert_eq!(align_to(&app), AlignTo::Selection, "no key: the choice again");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Two rectangles, the back one at x 10 and the front one at x 80, selected front first →
|
|
|
|
@@ -326,7 +349,7 @@ mod tests {
|
|
|
|
|
app.run("select.set", json!({"ids": [back, front]})).unwrap();
|
|
|
|
|
click(&ctx, &mut app, key.center(), show);
|
|
|
|
|
assert_eq!(key_object(&app), front.as_u64(), "the front rectangle, selected last");
|
|
|
|
|
assert_eq!(align_to(&app, &ctx), AlignTo::Key);
|
|
|
|
|
assert_eq!(align_to(&app), AlignTo::Key);
|
|
|
|
|
click(&ctx, &mut app, left.center(), show);
|
|
|
|
|
assert_eq!((left_edge(&app, &back), left_edge(&app, &front)), (80.0, 80.0), "to the key's left edge: {}", app.ui.status);
|
|
|
|
|
app.run("select.key", json!({"id": back})).unwrap();
|
|
|
|
@@ -344,20 +367,40 @@ mod tests {
|
|
|
|
|
let ([_, key, artboard], _) = panel_buttons(&mut app, &ctx);
|
|
|
|
|
click(&ctx, &mut app, artboard.center(), show);
|
|
|
|
|
click(&ctx, &mut app, key.center(), show);
|
|
|
|
|
assert_eq!(align_to(&app, &ctx), AlignTo::Key);
|
|
|
|
|
assert_eq!(align_to(&app), AlignTo::Key);
|
|
|
|
|
let cancel = text_rect(&frame_events(&ctx, &mut app, vec![], menu), " Cancel Key Object");
|
|
|
|
|
click(&ctx, &mut app, cancel.center(), menu);
|
|
|
|
|
assert!(!has_key(&app));
|
|
|
|
|
assert_eq!(align_to(&app, &ctx), AlignTo::Artboard, "Cancel Key Object");
|
|
|
|
|
assert_eq!(align_to(&app), AlignTo::Artboard, "Cancel Key Object");
|
|
|
|
|
// The Selection tool's click on the key object runs `select.key {}`.
|
|
|
|
|
click(&ctx, &mut app, key.center(), show);
|
|
|
|
|
assert!(has_key(&app));
|
|
|
|
|
app.run("select.key", json!({})).unwrap();
|
|
|
|
|
assert_eq!(align_to(&app, &ctx), AlignTo::Artboard, "a click on the key");
|
|
|
|
|
assert_eq!(align_to(&app), AlignTo::Artboard, "a click on the key");
|
|
|
|
|
click(&ctx, &mut app, key.center(), show);
|
|
|
|
|
assert!(has_key(&app));
|
|
|
|
|
app.run("select.set", json!({"ids": [back, front]})).unwrap();
|
|
|
|
|
assert_eq!(align_to(&app, &ctx), AlignTo::Artboard, "another selection");
|
|
|
|
|
assert_eq!(align_to(&app), AlignTo::Artboard, "another selection");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The Align To choice is kept with the UI state, which the app saves with its preferences and
|
|
|
|
|
/// `ui.inspect` reports. A saved value other than "artboard" reads as Selection, and the rest of
|
|
|
|
|
/// the state still loads.
|
|
|
|
|
#[test]
|
|
|
|
|
fn the_align_to_choice_is_kept_with_the_ui_state() {
|
|
|
|
|
let (mut app, _, _) = back_and_front();
|
|
|
|
|
let ctx = egui::Context::default();
|
|
|
|
|
let ([_, _, artboard], _) = panel_buttons(&mut app, &ctx);
|
|
|
|
|
click(&ctx, &mut app, artboard.center(), show);
|
|
|
|
|
assert_eq!(crate::control::inspect(&app, &ctx)["ui"]["align_to"], "artboard");
|
|
|
|
|
let back: crate::UiState = serde_json::from_value(serde_json::to_value(&app.ui).unwrap()).unwrap();
|
|
|
|
|
assert_eq!(back.align_to, AlignTo::Artboard);
|
|
|
|
|
for v in [json!("key"), json!("Artboard"), json!(3), Value::Null] {
|
|
|
|
|
let ui: crate::UiState = serde_json::from_value(json!({"align_to": v, "place_link": false})).unwrap();
|
|
|
|
|
assert_eq!((ui.align_to, ui.place_link), (AlignTo::Selection, false), "{v}");
|
|
|
|
|
}
|
|
|
|
|
let old: crate::UiState = serde_json::from_value(json!({})).unwrap();
|
|
|
|
|
assert_eq!(old.align_to, AlignTo::Selection, "preferences saved without it");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// With one object selected, Align to Key Object is disabled: a click sets no key and keeps the
|
|
|
|
@@ -372,11 +415,36 @@ mod tests {
|
|
|
|
|
assert!(!clickable, "disabled");
|
|
|
|
|
click(&ctx, &mut app, key.center(), show);
|
|
|
|
|
assert_eq!(app.session.active().unwrap().selection.key, None);
|
|
|
|
|
assert_eq!(align_to(&app, &ctx), AlignTo::Selection);
|
|
|
|
|
assert_eq!(align_to(&app), AlignTo::Selection);
|
|
|
|
|
click(&ctx, &mut app, left.center(), show);
|
|
|
|
|
assert_eq!(left_edge(&app, &front), 0.0, "the artboard's left edge");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// One frame of the Align panel → the rects of the Align To buttons drawn as chosen.
|
|
|
|
|
fn chosen_align_to(app: &mut VectorcraftApp, ctx: &egui::Context) -> Vec<egui::Rect> {
|
|
|
|
|
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| show(app, ui));
|
|
|
|
|
out.textures_delta.clear();
|
|
|
|
|
let fill = crate::theme::Tokens::get(ctx).tool_active;
|
|
|
|
|
let chosen = |s: &egui::Shape| match s {
|
|
|
|
|
egui::Shape::Rect(r) if r.fill == fill && r.rect.size() == egui::vec2(28.0, 28.0) => Some(r.rect),
|
|
|
|
|
_ => None,
|
|
|
|
|
};
|
|
|
|
|
out.shapes.iter().filter_map(|c| chosen(&c.shape)).collect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// With one object selected the align buttons align it to the artboard, and Align To shows
|
|
|
|
|
/// Artboard; with two, it shows the stored choice.
|
|
|
|
|
#[test]
|
|
|
|
|
fn align_to_shows_artboard_while_one_object_is_selected() {
|
|
|
|
|
let (mut app, _, front) = back_and_front();
|
|
|
|
|
let ctx = egui::Context::default();
|
|
|
|
|
let ([selection, _, artboard], _) = panel_buttons(&mut app, &ctx);
|
|
|
|
|
assert_eq!(chosen_align_to(&mut app, &ctx), [selection], "two objects");
|
|
|
|
|
app.run("select.set", json!({"ids": [front]})).unwrap();
|
|
|
|
|
assert_eq!(chosen_align_to(&mut app, &ctx), [artboard], "one object");
|
|
|
|
|
assert_eq!(align_to(&app), AlignTo::Selection, "the stored choice");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// #789: the Control bar's and the Properties panel's align buttons follow Align To too.
|
|
|
|
|
#[test]
|
|
|
|
|
fn the_other_align_buttons_align_to_the_align_to_choice() {
|
|
|
|
@@ -386,7 +454,7 @@ mod tests {
|
|
|
|
|
let b = app.run("shape.rectangle", json!({"x": 80, "y": 50, "width": 30, "height": 30})).unwrap()["id"].clone();
|
|
|
|
|
app.run("select.set", json!({"ids": [a, b]})).unwrap();
|
|
|
|
|
let ctx = egui::Context::default();
|
|
|
|
|
set_pstate(&ctx, "align-to", AlignTo::Artboard);
|
|
|
|
|
app.ui.align_to = AlignTo::Artboard;
|
|
|
|
|
// One frame finds the first button (Horizontal Align Right is third); the next clicks it.
|
|
|
|
|
let at = std::cell::Cell::new(egui::Pos2::ZERO);
|
|
|
|
|
let frame = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
|
|
|
|
@@ -407,4 +475,55 @@ mod tests {
|
|
|
|
|
let x1 = |id: &serde_json::Value| st.doc.bounds_of(&[vectorcraft_doc::NodeId(id.as_u64().unwrap())], false).unwrap().x1;
|
|
|
|
|
assert_eq!((x1(&a), x1(&b)), (200.0, 200.0), "both to the artboard's right edge");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Runs the Object → Align item `label` as a click on it does.
|
|
|
|
|
fn object_align(app: &mut VectorcraftApp, label: &str) {
|
|
|
|
|
let entry = crate::menus::menu_entries(app).into_iter().find(|e| e.path == ["Object", "Align"] && e.label == label).expect("menu item");
|
|
|
|
|
let (id, p) = crate::menus::click_target(&entry.label, entry.command.as_deref().unwrap(), &entry.params);
|
|
|
|
|
crate::menus::invoke(app, &id, p);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Object → Align aligns as the align buttons do: it aligns one object to the active artboard
|
|
|
|
|
/// whatever Align To says, and with Align to Artboard chosen it aligns two objects to the active
|
|
|
|
|
/// artboard too.
|
|
|
|
|
#[test]
|
|
|
|
|
fn object_align_follows_align_to() {
|
|
|
|
|
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
|
|
|
|
app.run("file.new", json!({"width": 200, "height": 200, "artboards": 2})).unwrap();
|
|
|
|
|
let boards: Vec<_> = app.session.active().unwrap().doc.artboards.iter().map(|a| a.rect).collect();
|
|
|
|
|
let a = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 10, "height": 10})).unwrap()["id"].clone();
|
|
|
|
|
let b = app.run("shape.rectangle", json!({"x": 80, "y": 50, "width": 30, "height": 30})).unwrap()["id"].clone();
|
|
|
|
|
let bounds = |app: &VectorcraftApp, id: &Value| {
|
|
|
|
|
app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id.as_u64().unwrap())).unwrap().geometric_bounds().unwrap()
|
|
|
|
|
};
|
|
|
|
|
app.run("select.set", json!({"ids": [b]})).unwrap();
|
|
|
|
|
object_align(&mut app, "Horizontal Align Left");
|
|
|
|
|
assert_eq!(bounds(&app, &b).x0, 0.0, "one object, Align to Selection: the artboard's left edge");
|
|
|
|
|
app.ui.align_to = AlignTo::Artboard;
|
|
|
|
|
app.view_mut().unwrap().artboard = 1;
|
|
|
|
|
app.run("select.set", json!({"ids": [a, b]})).unwrap();
|
|
|
|
|
object_align(&mut app, "Horizontal Align Right");
|
|
|
|
|
assert_eq!((bounds(&app, &a).x1, bounds(&app, &b).x1), (boards[1].x1, boards[1].x1), "the active artboard's right edge");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// With anchor points selected, Object → Align sends its params unchanged: two anchor points of
|
|
|
|
|
/// one path align to their own bounds with Align to Selection and with Align to Artboard chosen.
|
|
|
|
|
#[test]
|
|
|
|
|
fn object_align_aligns_anchor_points_to_each_other() {
|
|
|
|
|
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
|
|
|
|
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
|
|
|
|
|
let id = app.run("path.create", json!({"d": "M100 40 L200 60 L150 160 Z"})).unwrap()["id"].as_u64().unwrap();
|
|
|
|
|
let anchors = |app: &VectorcraftApp| -> Vec<(f64, f64)> {
|
|
|
|
|
let node = app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap();
|
|
|
|
|
node.path_data().unwrap().anchors().map(|(_, _, a)| (a.p.x, a.p.y)).collect()
|
|
|
|
|
};
|
|
|
|
|
for to in [AlignTo::Selection, AlignTo::Artboard] {
|
|
|
|
|
app.ui.align_to = to;
|
|
|
|
|
app.run("select.anchors", json!({"id": id, "anchors": [[0, 0], [0, 1]], "mode": "set"})).unwrap();
|
|
|
|
|
object_align(&mut app, "Horizontal Align Left");
|
|
|
|
|
assert_eq!(anchors(&app), [(100.0, 40.0), (100.0, 60.0), (150.0, 160.0)], "{to:?}: {}", app.ui.status);
|
|
|
|
|
app.run("edit.undo", json!({})).unwrap();
|
|
|
|
|
assert_eq!(anchors(&app)[1], (200.0, 60.0), "undone");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|