mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 05:37:23 -06:00
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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0e31fc92a2
commit
3b99df3b9a
@@ -16,7 +16,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"New…",
|
||||
["File"],
|
||||
Some("Cmd+N"),
|
||||
"{width?: pt=612, height?: pt=792, units?: \"Points\"|\"Inches\"|\"Millimeters\"|\"Pixels\"…, title?, artboards?: n, colorMode?: \"rgb\"|\"cmyk\" (a CMYK document starts with CMYK swatches and stores the colours applied to it as CMYK)}",
|
||||
"{width?: pt=612, height?: pt=792, units?: \"Points\"|\"Inches\"|\"Millimeters\"|\"Pixels\"… (default: prefs unitsGeneral), title?, artboards?: n, colorMode?: \"rgb\"|\"cmyk\" (a CMYK document starts with CMYK swatches and stores the colours applied to it as CMYK)}",
|
||||
always,
|
||||
file_new
|
||||
),
|
||||
@@ -30,7 +30,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Units",
|
||||
["File", "Document Setup"],
|
||||
None,
|
||||
"{units: \"Points\"|\"Picas\"|\"Inches\"|\"Millimeters\"|\"Centimeters\"|\"Pixels\"}",
|
||||
"{units: \"Points\"|\"Picas\"|\"Inches\"|\"Millimeters\"|\"Centimeters\"|\"Pixels\"|\"Feet & Inches\"|\"Meters\"|\"Yards\"|\"Feet\"} the document's units: every length the UI shows and reads (General)",
|
||||
has_doc,
|
||||
set_units
|
||||
),
|
||||
@@ -64,9 +64,10 @@ fn file_new(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
};
|
||||
let mut d = Document::new_with_mode(w, h, mode);
|
||||
d.title = str_param(p, "title").map(str::to_string).unwrap_or_else(|| s.next_untitled());
|
||||
if let Some(u) = str_param(p, "units") {
|
||||
d.units = parse_unit(u).ok_or_else(|| bad("file.new", format!("unknown units `{u}`")))?;
|
||||
}
|
||||
d.units = match str_param(p, "units") {
|
||||
Some(u) => Unit::named(u).ok_or_else(|| bad("file.new", format!("unknown units `{u}`")))?,
|
||||
None => s.default_units(),
|
||||
};
|
||||
let n = p.get("artboards").and_then(Value::as_u64).unwrap_or(1).clamp(1, 1000) as usize;
|
||||
for i in 1..n {
|
||||
let r = vectorcraft_geom::Rect::new(0.0, 0.0, w, h) + Vec2::new(i as f64 * (w + 20.0), 0.0);
|
||||
@@ -82,11 +83,6 @@ fn file_new(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({ "index": i }))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_unit(u: &str) -> Option<Unit> {
|
||||
let n = u.to_ascii_lowercase();
|
||||
Unit::ALL.into_iter().find(|x| x.label().to_ascii_lowercase() == n || x.suffix() == n)
|
||||
}
|
||||
|
||||
fn file_close(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let i = p.get("index").and_then(Value::as_u64).map(|v| v as usize).or(s.active_index()).ok_or(EngineError::NoDocument)?;
|
||||
if !s.close_document(i) {
|
||||
@@ -107,11 +103,8 @@ fn doc_node(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
fn set_units(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let u = str_param(p, "units").and_then(parse_unit).ok_or_else(|| bad("document.setUnits", "unknown units"))?;
|
||||
s.edit("Document Setup", |d, _| {
|
||||
d.units = u;
|
||||
Ok(())
|
||||
})?;
|
||||
let u = str_param(p, "units").and_then(Unit::named).ok_or_else(|| bad("document.setUnits", "unknown units"))?;
|
||||
s.set_document_units(u)?;
|
||||
ok()
|
||||
}
|
||||
|
||||
|
||||
@@ -180,6 +180,15 @@ pub(crate) fn num_param(p: &Value, key: &str) -> Option<f64> {
|
||||
.filter(|v: &f64| v.is_finite())
|
||||
}
|
||||
|
||||
/// A distance param in points: a number, or a string with a unit (`"5 mm"`; bare numbers are
|
||||
/// points).
|
||||
pub(crate) fn len_param(p: &Value, key: &str) -> Option<f64> {
|
||||
match p.get(key)? {
|
||||
Value::String(s) => vectorcraft_doc::Unit::Points.parse(s).filter(|v| v.is_finite()).or_else(|| num_param(p, key)),
|
||||
_ => num_param(p, key),
|
||||
}
|
||||
}
|
||||
|
||||
fn ids_json(ids: &[NodeId]) -> Value {
|
||||
json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>() })
|
||||
}
|
||||
@@ -397,7 +406,7 @@ fn join_param(p: &Value, key: &str) -> po::Join {
|
||||
}
|
||||
|
||||
fn offset_path(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let delta = num_param(p, "offset").unwrap_or(10.0);
|
||||
let delta = len_param(p, "offset").unwrap_or(10.0);
|
||||
if delta.abs() > 1e5 {
|
||||
return Err(bad("object.path.offsetPath", "offset out of range"));
|
||||
}
|
||||
@@ -661,7 +670,7 @@ fn edit_paths(s: &mut Session, label: &str, f: impl Fn(&PathData) -> PathData) -
|
||||
|
||||
fn simplify(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let opts = po::SimplifyOptions {
|
||||
tolerance: num_param(p, "tolerance").unwrap_or(1.0).clamp(1e-4, 1e4),
|
||||
tolerance: len_param(p, "tolerance").unwrap_or(1.0).clamp(1e-4, 1e4),
|
||||
corner_angle_deg: num_param(p, "cornerAngle").unwrap_or(30.0).clamp(0.0, 180.0),
|
||||
straight_lines: bool_or(p, "straightLines", false),
|
||||
};
|
||||
@@ -682,7 +691,7 @@ fn split_into_grid(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if !(1.0..=500.0).contains(&rows) || !(1.0..=500.0).contains(&cols) {
|
||||
return Err(bad("object.path.splitIntoGrid", "rows and columns must be 1..500"));
|
||||
}
|
||||
let gutter = num_param(p, "gutter").unwrap_or(12.0).max(0.0);
|
||||
let gutter = len_param(p, "gutter").unwrap_or(12.0).max(0.0);
|
||||
let roots = selected_roots(s)?;
|
||||
let ids = s.edit("Split Into Grid", |d, sel| {
|
||||
let mut out = vec![];
|
||||
|
||||
@@ -7,6 +7,8 @@ use serde_json::{Map, Value, json};
|
||||
|
||||
use super::*;
|
||||
use crate::Prefs;
|
||||
use crate::units::Measure;
|
||||
use vectorcraft_doc::Unit;
|
||||
|
||||
/// What kind of value a preference holds.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -18,6 +20,13 @@ pub enum PrefKind {
|
||||
max: f64,
|
||||
unit: &'static str,
|
||||
},
|
||||
/// A length in points in `min..=max`, shown and typed in the unit of `measure` (also given
|
||||
/// as a string with a unit, "5 mm").
|
||||
Length {
|
||||
min: f64,
|
||||
max: f64,
|
||||
measure: Measure,
|
||||
},
|
||||
/// A whole number in `min..=max`.
|
||||
Int {
|
||||
min: i64,
|
||||
@@ -59,6 +68,7 @@ pub const PREF_CATEGORIES: &[&str] = &[
|
||||
"Devices",
|
||||
];
|
||||
|
||||
/// Every unit as (`Unit::key`, `Unit::label`).
|
||||
pub const UNITS: &[(&str, &str)] = &[
|
||||
("points", "Points"),
|
||||
("picas", "Picas"),
|
||||
@@ -66,6 +76,10 @@ pub const UNITS: &[(&str, &str)] = &[
|
||||
("millimeters", "Millimeters"),
|
||||
("centimeters", "Centimeters"),
|
||||
("pixels", "Pixels"),
|
||||
("feetInches", "Feet & Inches"),
|
||||
("meters", "Meters"),
|
||||
("yards", "Yards"),
|
||||
("feet", "Feet"),
|
||||
];
|
||||
const LINE_STYLE: &[(&str, &str)] = &[("lines", "Lines"), ("dots", "Dots")];
|
||||
const BLACK: &[(&str, &str)] = &[("accurate", "Display All Blacks Accurately"), ("rich", "Display All Blacks as Rich Black")];
|
||||
@@ -78,6 +92,9 @@ macro_rules! p {
|
||||
($key:literal, $cat:literal, $sec:literal, $label:literal, num($min:expr, $max:expr, $unit:literal)) => {
|
||||
PrefSpec { key: $key, category: $cat, section: $sec, label: $label, kind: PrefKind::Num { min: $min, max: $max, unit: $unit } }
|
||||
};
|
||||
($key:literal, $cat:literal, $sec:literal, $label:literal, len($min:expr, $max:expr, $m:ident)) => {
|
||||
PrefSpec { key: $key, category: $cat, section: $sec, label: $label, kind: PrefKind::Length { min: $min, max: $max, measure: Measure::$m } }
|
||||
};
|
||||
($key:literal, $cat:literal, $sec:literal, $label:literal, int($min:expr, $max:expr)) => {
|
||||
PrefSpec { key: $key, category: $cat, section: $sec, label: $label, kind: PrefKind::Int { min: $min, max: $max } }
|
||||
};
|
||||
@@ -94,10 +111,10 @@ macro_rules! p {
|
||||
|
||||
pub const PREF_SPECS: &[PrefSpec] = &[
|
||||
// General
|
||||
p!("keyboardIncrement", "General", "", "Keyboard Increment", num(0.001, 1296.0, "pt")),
|
||||
p!("keyboardIncrement", "General", "", "Keyboard Increment", len(0.001, 1296.0, General)),
|
||||
p!("constrainAngle", "General", "", "Constrain Angle", num(-360.0, 360.0, "°")),
|
||||
p!("cornerRadius", "General", "", "Corner Radius", num(0.0, 1296.0, "pt")),
|
||||
p!("pasteOffset", "General", "", "Paste Offset", num(0.0, 1296.0, "pt")),
|
||||
p!("cornerRadius", "General", "", "Corner Radius", len(0.0, 1296.0, General)),
|
||||
p!("pasteOffset", "General", "", "Paste Offset", len(0.0, 1296.0, General)),
|
||||
p!("disableAutoAddDelete", "General", "Options", "Disable Auto Add/Delete", bool),
|
||||
p!("usePreciseCursors", "General", "Options", "Use Precise Cursors", bool),
|
||||
p!("showToolTips", "General", "Options", "Show Tool Tips", bool),
|
||||
@@ -150,9 +167,9 @@ pub const PREF_SPECS: &[PrefSpec] = &[
|
||||
p!("penRubberBand", "Selection & Anchor Display", "Enable Rubber Band for", "Pen Tool", bool),
|
||||
p!("curvatureRubberBand", "Selection & Anchor Display", "Enable Rubber Band for", "Curvature Tool", bool),
|
||||
// Type
|
||||
p!("typeSizeIncrement", "Type", "", "Size/Leading", num(0.001, 1296.0, "pt")),
|
||||
p!("typeSizeIncrement", "Type", "", "Size/Leading", len(0.001, 1296.0, Type)),
|
||||
p!("trackingIncrement", "Type", "", "Tracking", num(1.0, 1000.0, "/1000 em")),
|
||||
p!("baselineShiftIncrement", "Type", "", "Baseline Shift", num(0.001, 1296.0, "pt")),
|
||||
p!("baselineShiftIncrement", "Type", "", "Baseline Shift", len(0.001, 1296.0, Type)),
|
||||
p!("showEastAsianOptions", "Type", "Language Options", "Show East Asian Options", bool),
|
||||
p!("showIndicOptions", "Type", "Language Options", "Show Indic Options", bool),
|
||||
p!("typeSelectionByPathOnly", "Type", "Options", "Type Object Selection by Path Only", bool),
|
||||
@@ -176,7 +193,7 @@ pub const PREF_SPECS: &[PrefSpec] = &[
|
||||
p!("guideStyle", "Guides & Grid", "Guides", "Style", choice(LINE_STYLE)),
|
||||
p!("gridColor", "Guides & Grid", "Grid", "Color", color),
|
||||
p!("gridStyle", "Guides & Grid", "Grid", "Style", choice(LINE_STYLE)),
|
||||
p!("gridlineEvery", "Guides & Grid", "Grid", "Gridline every", num(1.0, 16383.0, "pt")),
|
||||
p!("gridlineEvery", "Guides & Grid", "Grid", "Gridline every", len(1.0, 16383.0, General)),
|
||||
p!("gridSubdivisions", "Guides & Grid", "Grid", "Subdivisions", int(1, 1000)),
|
||||
p!("gridsInBack", "Guides & Grid", "Grid", "Grids In Back", bool),
|
||||
p!("showPixelGrid", "Guides & Grid", "Grid", "Show Pixel Grid (Above 600% Zoom)", bool),
|
||||
@@ -333,11 +350,8 @@ pub fn validate(key: &str, v: &Value) -> std::result::Result<Value, String> {
|
||||
Value::String(s) if s == "true" || s == "false" => Ok(json!(s == "true")),
|
||||
_ => Err(format!("`{key}` must be true or false")),
|
||||
},
|
||||
PrefKind::Num { min, max, .. } => match num() {
|
||||
Some(x) if x.is_finite() && (min..=max).contains(&x) => Ok(json!(x)),
|
||||
Some(x) => Err(format!("`{key}` must be between {min} and {max} (got {x})")),
|
||||
None => Err(format!("`{key}` must be a number")),
|
||||
},
|
||||
PrefKind::Num { min, max, .. } => in_range(key, num(), min, max),
|
||||
PrefKind::Length { min, max, .. } => in_range(key, v.as_f64().or_else(|| v.as_str().and_then(|s| Unit::Points.parse(s))), min, max),
|
||||
PrefKind::Int { min, max } => match num() {
|
||||
Some(x) if x.fract() == 0.0 && (min as f64..=max as f64).contains(&x) => Ok(json!(x as i64)),
|
||||
Some(x) => Err(format!("`{key}` must be a whole number between {min} and {max} (got {x})")),
|
||||
@@ -366,6 +380,15 @@ pub fn validate(key: &str, v: &Value) -> std::result::Result<Value, String> {
|
||||
}
|
||||
}
|
||||
|
||||
/// `x` (`None`: not a number) as the value of number preference `key` in `min..=max`.
|
||||
fn in_range(key: &str, x: Option<f64>, min: f64, max: f64) -> std::result::Result<Value, String> {
|
||||
match x {
|
||||
Some(x) if x.is_finite() && (min..=max).contains(&x) => Ok(json!(x)),
|
||||
Some(x) => Err(format!("`{key}` must be between {min} and {max} (got {x})")),
|
||||
None => Err(format!("`{key}` must be a number")),
|
||||
}
|
||||
}
|
||||
|
||||
impl Prefs {
|
||||
pub fn to_json(&self) -> Value {
|
||||
serde_json::to_value(self).unwrap_or(Value::Null)
|
||||
@@ -436,7 +459,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Set Preference",
|
||||
[],
|
||||
None,
|
||||
"{key, value} or {values: {key: value, …}} set preferences (validated; see prefs.list; the eyedropper group takes a partial object, as eyedropper.setOptions)",
|
||||
"{key, value} or {values: {key: value, …}} set preferences (validated; see prefs.list; lengths in pt or strings with a unit; unitsGeneral also sets the active document's units, one undo step; the eyedropper group takes a partial object, as eyedropper.setOptions)",
|
||||
always,
|
||||
set
|
||||
),
|
||||
@@ -465,6 +488,10 @@ fn set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mut next = s.prefs.clone();
|
||||
next.set_values(&values).map_err(|e| bad("prefs.set", e))?;
|
||||
s.apply_prefs(next);
|
||||
// Units ▸ General is also the open document's units (as Document Setup sets them).
|
||||
if values.contains_key("unitsGeneral") && s.active().is_some() {
|
||||
s.set_document_units(s.default_units())?;
|
||||
}
|
||||
let all = s.prefs.to_json();
|
||||
Ok(Value::Object(values.keys().map(|k| (k.clone(), all[k.as_str()].clone())).collect()))
|
||||
}
|
||||
@@ -497,6 +524,13 @@ fn list(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
o["max"] = json!(max);
|
||||
o["unit"] = json!(unit);
|
||||
}
|
||||
PrefKind::Length { min, max, measure } => {
|
||||
o["kind"] = json!("number");
|
||||
o["min"] = json!(min);
|
||||
o["max"] = json!(max);
|
||||
o["unit"] = json!("pt");
|
||||
o["measure"] = json!(measure.name());
|
||||
}
|
||||
PrefKind::Int { min, max } => {
|
||||
o["kind"] = json!("integer");
|
||||
o["min"] = json!(min);
|
||||
|
||||
@@ -241,7 +241,7 @@ fn stroke_set(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
impl Session {
|
||||
/// Units > Stroke (Preferences): the unit stroke weights and dash lengths show in.
|
||||
pub fn stroke_unit(&self) -> Unit {
|
||||
super::edit::parse_unit(&self.prefs.units_stroke).unwrap_or_default()
|
||||
self.unit(crate::units::Measure::Stroke)
|
||||
}
|
||||
|
||||
/// The stroke the Stroke panel, the Control bar and the Properties panel show, picked as the
|
||||
|
||||
@@ -8,7 +8,7 @@ use vectorcraft_doc::{Document, Justify, Node, NodeId, NodeKind, TextObject};
|
||||
use vectorcraft_geom::PathData;
|
||||
|
||||
use super::edit::selected_roots;
|
||||
use super::pathops::{num_param, shape_node};
|
||||
use super::pathops::{len_param, num_param, shape_node};
|
||||
use super::*;
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
@@ -169,14 +169,14 @@ fn set_style(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "text.setStyle";
|
||||
let font = str_param(p, "font").map(str::to_string);
|
||||
let style = str_param(p, "style").map(str::to_string);
|
||||
let size = num_param(p, "size");
|
||||
let size = len_param(p, "size");
|
||||
if size.is_some_and(|v| v <= 0.0) {
|
||||
return Err(bad(C, "size must be positive"));
|
||||
}
|
||||
let leading = match p.get("leading") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(Value::String(a)) if a.eq_ignore_ascii_case("auto") => Some(None),
|
||||
Some(_) => Some(Some(num_param(p, "leading").ok_or_else(|| bad(C, "leading must be a number or \"auto\""))?.clamp(0.1, 5000.0))),
|
||||
Some(_) => Some(Some(len_param(p, "leading").ok_or_else(|| bad(C, "leading must be a number or \"auto\""))?.clamp(0.1, 5000.0))),
|
||||
};
|
||||
let tracking = num_param(p, "tracking");
|
||||
let justify = match str_param(p, "justify") {
|
||||
|
||||
@@ -10,6 +10,7 @@ pub mod cmd;
|
||||
pub mod guard;
|
||||
pub mod inspect;
|
||||
mod tooling;
|
||||
pub mod units;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -1004,6 +1005,8 @@ mod tests_tints;
|
||||
#[cfg(test)]
|
||||
mod tests_transparencygrid;
|
||||
#[cfg(test)]
|
||||
mod tests_units;
|
||||
#[cfg(test)]
|
||||
mod tests_widthpoints;
|
||||
#[cfg(test)]
|
||||
mod tests_widthprofiles;
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
//! 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));
|
||||
}
|
||||
@@ -65,6 +65,7 @@ impl Session {
|
||||
R: Default,
|
||||
{
|
||||
let Some(st) = self.active.and_then(|i| self.docs.get(i)) else { return R::default() };
|
||||
let (unit, stroke_unit) = (self.general_unit(), self.stroke_unit());
|
||||
let cx = ToolContext {
|
||||
doc: &st.doc,
|
||||
selection: &st.selection,
|
||||
@@ -85,6 +86,8 @@ impl Session {
|
||||
freeform_point: self.selected_freeform_point(),
|
||||
raster_sample: self.prefs.eyedropper.sample_size,
|
||||
preview_bounds: self.prefs.use_preview_bounds,
|
||||
unit,
|
||||
stroke_unit,
|
||||
};
|
||||
let tool = &mut self.tool;
|
||||
match crate::guard::catch_panic(|| f(tool.as_mut(), &cx)) {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
//! Units (Preferences ▸ Units): the unit every length the UI shows and reads is in.
|
||||
//!
|
||||
//! General (rulers, positions, sizes, dialog distances, canvas readouts) is the active document's
|
||||
//! units, so they travel with the file: Document Setup (`document.setUnits`) changes them, and
|
||||
//! Preferences ▸ Units ▸ General changes them too and is also the units new documents start in (and
|
||||
//! the unit shown while no document is open). Stroke (weights, dashes) and Type (sizes, leading,
|
||||
//! baseline shift) are application preferences. Typed values with a unit ("5 mm", "12 pt") work in
|
||||
//! any unit.
|
||||
|
||||
use vectorcraft_doc::Unit;
|
||||
|
||||
use crate::{Result, Session};
|
||||
|
||||
/// Which Units preference a length follows.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Measure {
|
||||
General,
|
||||
Stroke,
|
||||
Type,
|
||||
}
|
||||
|
||||
impl Measure {
|
||||
/// Its name in `prefs.list` (`measure`).
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Measure::General => "general",
|
||||
Measure::Stroke => "stroke",
|
||||
Measure::Type => "type",
|
||||
}
|
||||
}
|
||||
|
||||
/// The preference that picks its unit.
|
||||
pub fn pref_key(self) -> &'static str {
|
||||
match self {
|
||||
Measure::General => "unitsGeneral",
|
||||
Measure::Stroke => "unitsStroke",
|
||||
Measure::Type => "unitsType",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The unit a Units preference value names (points when it names none).
|
||||
fn pref_unit(value: &str) -> Unit {
|
||||
Unit::named(value).unwrap_or_default()
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// The unit lengths of kind `m` show in and bare numbers typed for them are read in.
|
||||
pub fn unit(&self, m: Measure) -> Unit {
|
||||
match m {
|
||||
Measure::General => self.active().map_or_else(|| self.default_units(), |d| d.doc.units),
|
||||
Measure::Stroke => pref_unit(&self.prefs.units_stroke),
|
||||
Measure::Type => pref_unit(&self.prefs.units_type),
|
||||
}
|
||||
}
|
||||
|
||||
/// The General unit: the active document's units (Preferences ▸ Units ▸ General without one).
|
||||
pub fn general_unit(&self) -> Unit {
|
||||
self.unit(Measure::General)
|
||||
}
|
||||
|
||||
/// Units ▸ Type: the unit type sizes, leading and baseline shift show in.
|
||||
pub fn type_unit(&self) -> Unit {
|
||||
self.unit(Measure::Type)
|
||||
}
|
||||
|
||||
/// Preferences ▸ Units ▸ General: the units new documents start in.
|
||||
pub fn default_units(&self) -> Unit {
|
||||
pref_unit(&self.prefs.units_general)
|
||||
}
|
||||
|
||||
/// Set the active document's units (one undo step, as Document Setup does; none when they
|
||||
/// already are `u`).
|
||||
pub(crate) fn set_document_units(&mut self, u: Unit) -> Result<()> {
|
||||
if self.doc()?.doc.units == u {
|
||||
return Ok(());
|
||||
}
|
||||
self.edit("Document Setup", |d, _| {
|
||||
d.units = u;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user