Files
vectorcraft/crates/engine/tests/it/ruler_origin.rs
T
aiongg 8cc69ed45c CI: one test binary per crate, nextest, line tables only (#3855)
* CI: line tables only for workspace crates, no debug info for dependencies

* CI: one integration-test binary per crate

Each crate's tests/*.rs files become modules of tests/it/main.rs. Files that change
process-wide state (Color Settings, the font database, installed plug-ins, the working
directory), the fuzz sweeps and the test targets the Affinity corpus workflow names stay
their own binaries.

* Docs: name the integration tests' new paths

* CI: xtask test with nextest and doctests, one wasm check, clippy --keep-going

cargo xtask ci runs the tests with cargo nextest (cargo test when it is not installed),
builds them with --lib --bins --tests, runs the doctests as their own step, and checks
the wasm crates in one cargo check --keep-going call.
2026-10-23 21:36:10 -04:00

215 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! #1212: ruler coordinates are saved metadata, independent of the artwork's coordinates.
use serde_json::{Value, json};
use vectorcraft_doc::RulerMode;
use vectorcraft_engine::Session;
use vectorcraft_format::{SaveOptions, load, save, save_with};
fn session() -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
s.execute("shape.rectangle", &json!({"x": 100, "y": 50, "width": 80, "height": 40})).unwrap();
s.execute("guide.add", &json!({"vertical": true, "pos": 125})).unwrap();
s
}
fn origin(s: &mut Session) -> Value {
s.execute("document.rulerOrigin", &json!({})).unwrap()
}
#[test]
fn changing_origin_preserves_artwork_and_selection_and_is_one_undo_step() {
let mut s = session();
let before = s.doc().unwrap().doc.clone();
let selection = s.doc().unwrap().selection.clone();
let undo = s.doc().unwrap().history.undo.len();
assert_eq!(origin(&mut s), json!({"x": 0.0, "y": 0.0}));
let p = json!({"x": 125.5, "y": -30.25});
s.execute("document.setRulerOrigin", &p).unwrap();
assert_eq!(origin(&mut s), p);
assert_eq!(s.doc().unwrap().selection, selection);
let mut changed = s.execute("document.json", &json!({})).unwrap();
changed.as_object_mut().unwrap().remove("artboard_ruler_origins");
assert_eq!(changed, serde_json::to_value(&*before).unwrap(), "only the origin changed");
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
let revision = s.doc().unwrap().revision;
s.execute("document.setRulerOrigin", &p).unwrap();
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "same origin is a no-op");
assert_eq!(s.doc().unwrap().revision, revision);
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(&*s.doc().unwrap().doc, &*before);
s.execute("edit.redo", &json!({})).unwrap();
assert_eq!(origin(&mut s), p);
}
#[test]
fn reset_uses_the_requested_artboards_top_left_and_ignores_noops() {
let mut s = session();
// The one document zero point of global rulers (artboard rulers keep one per artboard).
s.execute("view.globalRulers", &json!({"global": true})).unwrap();
s.execute("artboard.new", &json!({"x": 700, "y": -250, "width": 200, "height": 150})).unwrap();
s.execute("document.setRulerOrigin", &json!({"x": 25, "y": 50})).unwrap();
let boards = s.doc().unwrap().doc.artboards.clone();
s.execute("document.resetRulerOrigin", &json!({"artboard": 1})).unwrap();
assert_eq!(origin(&mut s), json!({"x": 700.0, "y": -250.0}));
assert_eq!(s.doc().unwrap().doc.artboards, boards);
let undo = s.doc().unwrap().history.undo.len();
s.execute("document.resetRulerOrigin", &json!({"artboard": 1})).unwrap();
assert_eq!(s.doc().unwrap().history.undo.len(), undo);
for p in [json!({"artboard": 9}), json!({"artboard": -1}), json!({"artboard": "first"})] {
assert!(s.execute("document.resetRulerOrigin", &p).is_err());
assert_eq!(s.doc().unwrap().history.undo.len(), undo);
}
s.execute("document.resetRulerOrigin", &json!({})).unwrap();
assert_eq!(origin(&mut s), json!({"x": 0.0, "y": 0.0}));
}
#[test]
fn native_versions_and_compression_keep_the_origin_and_old_files_default_to_zero() {
let mut s = session();
let legacy = String::from_utf8(save(&s.doc().unwrap().doc, false)).unwrap().replacen("\"format\":\"vectorcraft\"", "\"format\":\"drawcraft\"", 1);
assert!(serde_json::to_value(load(legacy.as_bytes()).unwrap()).unwrap().get("ruler_origin").is_none());
let p = json!({"x": -12.5, "y": 27.75});
s.execute("document.setRulerOrigin", &p).unwrap();
let doc = &s.doc().unwrap().doc;
for (version, compress) in [(1, false), (2, false), (3, false), (4, false), (4, true)] {
let bytes = save_with(doc, &SaveOptions { version, compress, ..Default::default() }).unwrap();
let back = load(&bytes).unwrap();
assert_eq!(back.ruler_origin_for(0), doc.ruler_origin_for(0), "v{version}, compression {compress}");
assert_eq!((back.rulers, &back.artboard_ruler_origins), (doc.rulers, &doc.artboard_ruler_origins), "v{version}, compression {compress}");
assert_eq!(back.layers, doc.layers);
assert_eq!(back.guides, doc.guides);
}
}
#[test]
fn origin_rejects_invalid_coordinates_without_changing_absolute_command_coordinates() {
let mut s = session();
s.execute("document.setRulerOrigin", &json!({"x": 200, "y": 150})).unwrap();
let before = s.doc().unwrap().doc.clone();
let undo = s.doc().unwrap().history.undo.len();
for p in
[json!({"x": 4_000_001, "y": 0}), json!({"x": 0, "y": -4_000_001}), json!({"x": "10", "y": 0}), json!({"x": 0}), json!({"x": null, "y": 0})]
{
assert!(s.execute("document.setRulerOrigin", &p).is_err(), "{p}");
assert_eq!(&*s.doc().unwrap().doc, &*before);
assert_eq!(s.doc().unwrap().history.undo.len(), undo);
}
s.execute("object.setBounds", &json!({"x": 80, "y": 40, "reference": 0})).unwrap();
let bounds = s.doc().unwrap().doc.bounds_of(&s.doc().unwrap().selection.objects, false).unwrap();
assert_eq!((bounds.x0, bounds.y0), (80.0, 40.0), "engine coordinates stay absolute");
assert_eq!(origin(&mut s), json!({"x": 200.0, "y": 150.0}));
}
// View › Rulers › Change to Global / Artboard Rulers. Behaviour from Adobe's public help:
// https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/about-rulers.html
// (global rulers count from the first artboard's top-left corner and stay there; artboard rulers,
// the default, from the active artboard's, and each artboard can have its own origin) and
// https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/use-rulers.html
// (View > Rulers > Change to Artboard Rulers / Change to Global Rulers; double-click the corner
// where the rulers meet to restore the default origin).
fn two_boards() -> Session {
let mut s = session();
s.execute("artboard.new", &json!({"x": 700, "y": -250, "width": 200, "height": 150})).unwrap();
s
}
fn origin_of(s: &mut Session, artboard: usize) -> Value {
s.execute("document.rulerOrigin", &json!({"artboard": artboard})).unwrap()
}
#[test]
fn new_documents_use_artboard_rulers_that_count_from_the_active_artboard() {
let mut s = two_boards();
assert_eq!(s.doc().unwrap().doc.rulers, RulerMode::Artboard);
assert_eq!(origin_of(&mut s, 0), json!({"x": 0.0, "y": 0.0}));
assert_eq!(origin_of(&mut s, 1), json!({"x": 700.0, "y": -250.0}), "zero at the second artboard's top-left corner");
// Moving an artboard moves its zero point with it.
s.execute("artboard.setProps", &json!({"index": 1, "x": 650, "y": -200})).unwrap();
assert_eq!(origin_of(&mut s, 1), json!({"x": 650.0, "y": -200.0}));
assert!(s.execute("document.rulerOrigin", &json!({"artboard": 2})).is_err());
}
#[test]
fn global_rulers_count_from_one_zero_point_whichever_artboard_is_active() {
let mut s = two_boards();
let undo = s.doc().unwrap().history.undo.len();
assert_eq!(s.execute("view.globalRulers", &json!({})).unwrap(), json!({"global": true}));
assert_eq!(s.doc().unwrap().doc.rulers, RulerMode::Global);
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
for i in [0, 1] {
assert_eq!(origin_of(&mut s, i), json!({"x": 0.0, "y": 0.0}), "artboard {i}");
}
// Setting it explicitly to what it is is a no-op; a bad value changes nothing.
s.execute("view.globalRulers", &json!({"global": true})).unwrap();
assert!(s.execute("view.globalRulers", &json!({"global": "yes"})).is_err());
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().doc.rulers, RulerMode::Artboard);
assert_eq!(origin_of(&mut s, 1), json!({"x": 700.0, "y": -250.0}));
s.execute("edit.redo", &json!({})).unwrap();
assert_eq!(s.execute("view.globalRulers", &json!({})).unwrap(), json!({"global": false}), "toggles back");
}
#[test]
fn each_artboard_keeps_its_own_origin_apart_from_the_global_one() {
let mut s = two_boards();
s.execute("document.setRulerOrigin", &json!({"x": 720, "y": -230, "artboard": 1})).unwrap();
assert_eq!(origin_of(&mut s, 1), json!({"x": 720.0, "y": -230.0}));
assert_eq!(origin_of(&mut s, 0), json!({"x": 0.0, "y": 0.0}), "the other artboard keeps its corner");
// The origin is kept relative to its artboard: it follows a move.
s.execute("artboard.setProps", &json!({"index": 1, "x": 600, "y": -250})).unwrap();
assert_eq!(origin_of(&mut s, 1), json!({"x": 620.0, "y": -230.0}));
// Global rulers have their own zero point, untouched by the artboard's.
s.execute("view.globalRulers", &json!({"global": true})).unwrap();
s.execute("document.setRulerOrigin", &json!({"x": 10, "y": 20})).unwrap();
assert_eq!(origin_of(&mut s, 1), json!({"x": 10.0, "y": 20.0}));
s.execute("view.globalRulers", &json!({"global": false})).unwrap();
assert_eq!(origin_of(&mut s, 1), json!({"x": 620.0, "y": -230.0}));
assert_eq!(origin_of(&mut s, 0), json!({"x": 0.0, "y": 0.0}));
// Double-clicking the corner restores that artboard's top-left corner, in one undo step.
let undo = s.doc().unwrap().history.undo.len();
s.execute("document.resetRulerOrigin", &json!({"artboard": 1})).unwrap();
assert_eq!(origin_of(&mut s, 1), json!({"x": 600.0, "y": -250.0}));
assert!(s.doc().unwrap().doc.artboard_ruler_origins.is_empty());
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
s.execute("document.resetRulerOrigin", &json!({"artboard": 1})).unwrap();
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "already there: no-op");
assert!(s.execute("document.setRulerOrigin", &json!({"x": 1, "y": 1, "artboard": 5})).is_err());
}
#[test]
fn the_ruler_mode_is_saved_and_files_from_before_it_measure_globally() {
let mut s = two_boards();
s.execute("document.setRulerOrigin", &json!({"x": 750, "y": -200, "artboard": 1})).unwrap();
let doc = s.doc().unwrap().doc.clone();
let back = load(&save(&doc, false)).unwrap();
assert_eq!((back.rulers, back.ruler_origin_for(1)), (RulerMode::Artboard, doc.ruler_origin_for(1)));
s.execute("view.globalRulers", &json!({"global": true})).unwrap();
let back = load(&save(&s.doc().unwrap().doc, false)).unwrap();
assert_eq!(back.rulers, RulerMode::Global);
// A file written before the choice existed has no `rulers` key: its rulers measured globally.
let mut old = serde_json::to_value(&*doc).unwrap();
old.as_object_mut().unwrap().remove("rulers");
let old: vectorcraft_doc::Document = serde_json::from_value(old).unwrap();
assert_eq!(old.rulers, RulerMode::Global);
assert_eq!(old.ruler_origin_for(1), old.ruler_origin_or_zero());
}
#[test]
fn stored_origins_out_of_range_fall_back_to_the_artboard_corner() {
let mut s = two_boards();
let mut doc = s.doc().unwrap().doc.clone();
let id = doc.artboards[1].id;
for bad in [vectorcraft_geom::Vec2::new(f64::NAN, 0.0), vectorcraft_geom::Vec2::new(0.0, 9.0e6)] {
std::sync::Arc::make_mut(&mut doc).artboard_ruler_origins.insert(id, bad);
assert_eq!(doc.ruler_origin_for(1), vectorcraft_geom::Point::new(700.0, -250.0));
}
// An index past the artboards uses the first one; no artboard at all, the global zero point.
assert_eq!(doc.ruler_origin_for(9), vectorcraft_geom::Point::ZERO);
std::sync::Arc::make_mut(&mut doc).artboards.clear();
assert_eq!(doc.ruler_origin_for(0), doc.ruler_origin_or_zero());
let _ = origin_of(&mut s, 0);
}