vectorcraft/crates/engine/src/tests_units.rs
bflatastic 3b99df3b9a M14.5: Preferences > Units drive every length the UI shows and reads
The General unit is the active document's units: Preferences > Units >
General sets them (one undo step) and is the units new documents start
in (and the unit shown with no document open); Document Setup changes
only the document. The Transform panel, Properties, the Control bar, the
Info panel, rulers, the status bar, Align spacing, Tabs, paragraph
indents, Pattern Options, dialog distances (Move, shape sizes, path
dialogs, Artboard Options, effect distances, Transform Each, parameter
dialogs), New Document and the canvas measurement labels (smart-guide
sizes, moves, the Measure tool, the Live Corners radius) all follow it,
live. Units > Type drives font size, leading and baseline shift; Units >
Stroke also reaches the Appearance panel and the Magic Wand weight
tolerance. Typed units ("5 mm", "12 pt") work in any field; all ten
units are choices; length preferences show in their unit and accept a
unit; offset/simplify/grid/type-size params read typed units. Typed text
starting with a multi-byte character no longer panics the parser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 07:43:41 +07:00

134 lines
6.4 KiB
Rust

//! Preferences ▸ Units: the General unit is the open document's units (Preferences and Document
//! Setup set it, new documents start in the preference), Stroke and Type are preferences, and the
//! canvas measurement labels follow them.
use serde_json::json;
use vectorcraft_doc::Unit;
use vectorcraft_tools::{Mods, Overlay, PointerEvent, PointerKind};
use super::*;
use crate::cmd::prefscmds::{PREF_SPECS, PrefKind, UNITS};
use crate::units::Measure;
fn session() -> Session {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
s
}
fn units(s: &Session) -> Unit {
s.doc().unwrap().doc.units
}
#[test]
fn preferences_general_sets_the_open_documents_units_as_one_undo_step() {
let mut s = session();
assert_eq!(s.general_unit(), Unit::Points);
let undo = s.doc().unwrap().history.undo.len();
s.execute("prefs.set", &json!({"key": "unitsGeneral", "value": "Millimeters"})).unwrap();
assert_eq!((units(&s), s.general_unit(), s.default_units()), (Unit::Millimeters, Unit::Millimeters, Unit::Millimeters));
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
// Setting it again (the Preferences dialog's OK sends every value) adds no step.
s.execute("prefs.set", &json!({"values": {"unitsGeneral": "millimeters", "keyboardIncrement": 2}})).unwrap();
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(units(&s), Unit::Points);
assert_eq!(s.prefs.units_general, "millimeters", "undo is the document's; the preference stays");
}
#[test]
fn new_documents_start_in_the_preference_and_document_setup_only_changes_the_document() {
let mut s = Session::new();
assert_eq!(s.general_unit(), Unit::Points, "no document: the preference");
s.execute("prefs.set", &json!({"key": "unitsGeneral", "value": "inches"})).unwrap();
assert_eq!(s.general_unit(), Unit::Inches);
s.execute("file.new", &json!({"width": 100, "height": 100})).unwrap();
assert_eq!(units(&s), Unit::Inches);
s.execute("file.new", &json!({"width": 100, "height": 100, "units": "Pixels"})).unwrap();
assert_eq!((units(&s), s.general_unit()), (Unit::Pixels, Unit::Pixels), "given units win");
s.execute("document.setUnits", &json!({"units": "cm"})).unwrap();
assert_eq!((units(&s), s.default_units()), (Unit::Centimeters, Unit::Inches));
// Each document keeps its own units.
s.execute("document.activate", &json!({"index": 0})).unwrap();
assert_eq!(s.general_unit(), Unit::Inches);
assert!(s.execute("file.new", &json!({"units": "furlongs"})).is_err());
}
#[test]
fn document_setup_without_a_document_fails() {
let mut s = Session::new();
assert!(s.execute("document.setUnits", &json!({"units": "mm"})).is_err());
// The preference alone still changes.
s.execute("prefs.set", &json!({"key": "unitsGeneral", "value": "Millimeters"})).unwrap();
assert_eq!(s.prefs.units_general, "millimeters");
}
#[test]
fn every_unit_is_a_units_choice() {
let want: Vec<(&str, &str)> = Unit::ALL.iter().map(|u| (u.key(), u.label())).collect();
assert_eq!(UNITS, want.as_slice());
let mut s = session();
for u in Unit::ALL {
s.execute("prefs.set", &json!({"values": {"unitsGeneral": u.label(), "unitsStroke": u.key(), "unitsType": u.key()}})).unwrap();
assert_eq!((s.general_unit(), s.type_unit()), (u, u));
assert_eq!(s.stroke_unit(), u);
}
}
#[test]
fn stroke_and_type_units_are_preferences() {
let mut s = session();
s.execute("prefs.set", &json!({"values": {"unitsStroke": "millimeters", "unitsType": "pixels"}})).unwrap();
assert_eq!((s.unit(Measure::General), s.unit(Measure::Stroke), s.unit(Measure::Type)), (Unit::Points, Unit::Millimeters, Unit::Pixels));
assert_eq!(units(&s), Unit::Points, "they leave the document alone");
}
#[test]
fn length_preferences_take_typed_units_and_say_what_they_follow() {
let mut s = session();
s.execute("prefs.set", &json!({"values": {"keyboardIncrement": "1 mm", "gridlineEvery": "1 in", "typeSizeIncrement": 3}})).unwrap();
assert!((s.prefs.keyboard_increment - 72.0 / 25.4).abs() < 1e-9);
assert_eq!((s.prefs.gridline_every, s.prefs.type_size_increment), (72.0, 3.0));
assert!(s.execute("prefs.set", &json!({"key": "pasteOffset", "value": "1 yd"})).is_err(), "out of range");
let list = s.execute("prefs.list", &json!({})).unwrap();
let row = |k: &str| list.as_array().unwrap().iter().find(|r| r["key"] == k).unwrap().clone();
assert_eq!((row("keyboardIncrement")["measure"].clone(), row("keyboardIncrement")["unit"].clone()), (json!("general"), json!("pt")));
assert_eq!(row("baselineShiftIncrement")["measure"], json!("type"));
assert!(row("snappingTolerance")["measure"].is_null(), "a screen distance");
let lengths = PREF_SPECS.iter().filter(|p| matches!(p.kind, PrefKind::Length { .. })).count();
assert_eq!(lengths, 6);
}
#[test]
fn canvas_measurement_labels_follow_the_units() {
let mut s = session();
s.execute("prefs.set", &json!({"values": {"unitsGeneral": "millimeters", "unitsStroke": "inches"}})).unwrap();
let v = ViewInfo::default();
s.select_tool("rectangle", v).unwrap();
for (kind, x, y) in [(PointerKind::Down, 0.0, 0.0), (PointerKind::Drag, 72.0, 36.0)] {
s.pointer(&PointerEvent::new(kind, x, y).with_mods(Mods::default()), v).unwrap();
}
let labels: Vec<String> =
s.overlays(v).into_iter().filter_map(|o| if let Overlay::Measure { text, .. } = o { Some(text) } else { None }).collect();
assert_eq!(labels, ["W: 25.40 mm\nH: 12.70 mm"]);
}
#[test]
fn units_survive_a_native_round_trip() {
let mut s = session();
s.execute("document.setUnits", &json!({"units": "Feet & Inches"})).unwrap();
let back = vectorcraft_format::load(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap();
assert_eq!(back.units, Unit::FeetInches);
}
#[test]
fn distance_params_take_a_unit() {
let mut s = session();
let id = s.execute("text.create", &json!({"x": 10, "y": 10, "text": "Hi"})).unwrap()["id"].clone();
s.execute("select.set", &json!({ "ids": [id] })).unwrap();
s.execute("text.setStyle", &json!({"size": "0.5 in", "leading": "1in", "tracking": "20"})).unwrap();
let n = s.doc().unwrap().doc.node(NodeId(id.as_u64().unwrap())).unwrap().clone();
let NodeKind::Text(t) = &n.kind else { panic!("not text") };
let st = t.first_style();
assert_eq!((st.size, st.leading, st.tracking), (36.0, Some(72.0), 20.0));
}