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:
parent
0e31fc92a2
commit
3b99df3b9a
|
|
@ -149,23 +149,55 @@ impl Unit {
|
|||
}
|
||||
/// Format a point value in this unit the way fields show it (e.g. `12.5 pt`, `3 in`).
|
||||
pub fn format(self, pt: f64) -> String {
|
||||
let v = self.from_pt(pt);
|
||||
let s = format!("{:.3}", v);
|
||||
format!("{} {}", self.number(pt), self.suffix())
|
||||
}
|
||||
/// [`Unit::format`] without the suffix (`12.5`), for narrow fields.
|
||||
pub fn number(self, pt: f64) -> String {
|
||||
let s = format!("{:.3}", self.from_pt(pt));
|
||||
let s = s.trim_end_matches('0').trim_end_matches('.');
|
||||
let s = if s == "-0" { "0" } else { s };
|
||||
format!("{s} {}", self.suffix())
|
||||
if s == "-0" { "0".into() } else { s.into() }
|
||||
}
|
||||
/// Parse `12`, `12pt`, `1in`, `3 mm`, `2p6` (picas+points), simple `+ - * /` arithmetic.
|
||||
pub fn parse(self, s: &str) -> Option<f64> {
|
||||
parse_measure(s, self)
|
||||
}
|
||||
/// The value naming this unit in the Units preferences (`points`, `millimeters`,
|
||||
/// `feetInches`).
|
||||
pub fn key(self) -> &'static str {
|
||||
match self {
|
||||
Unit::Points => "points",
|
||||
Unit::Picas => "picas",
|
||||
Unit::Inches => "inches",
|
||||
Unit::Millimeters => "millimeters",
|
||||
Unit::Centimeters => "centimeters",
|
||||
Unit::Pixels => "pixels",
|
||||
Unit::FeetInches => "feetInches",
|
||||
Unit::Meters => "meters",
|
||||
Unit::Yards => "yards",
|
||||
Unit::Feet => "feet",
|
||||
}
|
||||
}
|
||||
/// The unit `name` names: its label, suffix or [`Unit::key`] in any case, ignoring spaces and
|
||||
/// punctuation (`Millimeters`, `mm`, `Feet & Inches`, `feetInches`).
|
||||
pub fn named(name: &str) -> Option<Unit> {
|
||||
let letters = |s: &'static str| s.chars().filter(char::is_ascii_alphanumeric).map(|c| c.to_ascii_lowercase());
|
||||
let given = || name.chars().filter(char::is_ascii_alphanumeric).map(|c| c.to_ascii_lowercase());
|
||||
Unit::ALL.into_iter().find(|u| letters(u.label()).eq(given()) || u.suffix().chars().eq(given()))
|
||||
}
|
||||
/// A point value as the canvas measurement labels show it: two decimals and the suffix
|
||||
/// (`12.50 mm`).
|
||||
pub fn readout(self, pt: f64) -> String {
|
||||
format!("{:.2} {}", self.from_pt(pt), self.suffix())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_measure(s: &str, default: Unit) -> Option<f64> {
|
||||
let s = s.trim();
|
||||
// Simple arithmetic on the right (Illustrator fields accept "10+5", "100/2", "3in*2").
|
||||
for op in ['+', '-', '*', '/'] {
|
||||
if let Some(i) = s[1.min(s.len())..].rfind(op).map(|i| i + 1.min(s.len())) {
|
||||
// Past the first character, so a leading sign isn't an operator.
|
||||
let skip = s.chars().next().map_or(0, char::len_utf8);
|
||||
if let Some(i) = s[skip..].rfind(op).map(|i| i + skip) {
|
||||
let (l, r) = (&s[..i], &s[i + 1..]);
|
||||
if l.trim().is_empty() {
|
||||
continue;
|
||||
|
|
@ -901,3 +933,6 @@ mod tests {
|
|||
assert_eq!(Unit::Inches.format(144.0), "2 in");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_units;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
//! Units: format/parse round-trips in every unit, typed units winning over the field's unit, names
|
||||
//! and canvas readouts.
|
||||
|
||||
use super::Unit;
|
||||
|
||||
#[test]
|
||||
fn every_unit_round_trips_through_its_field_text() {
|
||||
for u in Unit::ALL {
|
||||
// Field text keeps three decimals of the unit: half a step is the most a round trip loses.
|
||||
let tol = 0.0005 * u.points() + 1e-9;
|
||||
for pt in [0.0, 1.0, 12.5, 72.0, 100.0, 595.28, 1234.5678, -36.0] {
|
||||
let shown = u.format(pt);
|
||||
let back = u.parse(&shown).unwrap_or_else(|| panic!("{u:?} can't read back `{shown}`"));
|
||||
assert!((back - pt).abs() <= tol, "{u:?}: {pt} → `{shown}` → {back}");
|
||||
// The bare number reads in the field's unit, and in any unit with its suffix.
|
||||
let bare = u.parse(&u.number(pt)).unwrap();
|
||||
assert!((bare - pt).abs() <= tol, "{u:?}: bare `{}` → {bare}", u.number(pt));
|
||||
let pts = Unit::Points.parse(&shown).unwrap();
|
||||
assert!((pts - pt).abs() <= tol, "{u:?}: `{shown}` in a points field → {pts}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_units_win_over_the_fields_unit() {
|
||||
let mm = 72.0 / 25.4;
|
||||
assert_eq!(Unit::Millimeters.parse("10"), Some(10.0 * mm));
|
||||
assert_eq!(Unit::Millimeters.parse("12 pt"), Some(12.0));
|
||||
assert_eq!(Unit::Millimeters.parse("1in"), Some(72.0));
|
||||
assert_eq!(Unit::Points.parse("5 mm"), Some(5.0 * mm));
|
||||
assert_eq!(Unit::Centimeters.parse("2p6"), Some(30.0));
|
||||
assert_eq!(Unit::Millimeters.parse("10+5"), Some(15.0 * mm));
|
||||
assert_eq!(Unit::Inches.parse("1 + 36 pt"), Some(108.0));
|
||||
assert_eq!(Unit::Pixels.parse("-4"), Some(-4.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn odd_text_reads_as_nothing_without_panicking() {
|
||||
for s in ["", "é", "é5", "°+1", "×2", "5 ×", "—3", "mm", "1/0", "--", "+"] {
|
||||
assert_eq!(Unit::Millimeters.parse(s), None, "{s}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn units_are_named_by_label_key_or_suffix() {
|
||||
for u in Unit::ALL {
|
||||
assert_eq!(Unit::named(u.label()), Some(u));
|
||||
assert_eq!(Unit::named(u.key()), Some(u));
|
||||
assert_eq!(Unit::named(&u.label().to_uppercase()), Some(u));
|
||||
}
|
||||
assert_eq!(Unit::named("mm"), Some(Unit::Millimeters));
|
||||
assert_eq!(Unit::named("Feet & Inches"), Some(Unit::FeetInches));
|
||||
assert_eq!(Unit::named("feetInches"), Some(Unit::FeetInches));
|
||||
assert_eq!(Unit::named("furlongs"), None);
|
||||
assert_eq!(Unit::named(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readouts_show_two_decimals() {
|
||||
assert_eq!(Unit::Points.readout(15.0), "15.00 pt");
|
||||
assert_eq!(Unit::Millimeters.readout(72.0), "25.40 mm");
|
||||
assert_eq!(Unit::Inches.readout(-36.0), "-0.50 in");
|
||||
assert_eq!(Unit::Millimeters.number(1.0), "0.353");
|
||||
assert_eq!(Unit::Points.number(-0.0001), "0");
|
||||
}
|
||||
|
|
@ -83,6 +83,19 @@ pub struct Lengths {
|
|||
pub absolute: &'static [&'static str],
|
||||
}
|
||||
|
||||
impl Lengths {
|
||||
/// The distance parameters while the effect's `relative` parameter is `relative`.
|
||||
pub fn keys(&self, relative: bool) -> impl Iterator<Item = &'static str> + '_ {
|
||||
self.always.iter().chain(if relative { &[][..] } else { self.absolute }).copied()
|
||||
}
|
||||
}
|
||||
|
||||
/// Is parameter `key` of effect `id` a distance in points while its `relative` parameter is
|
||||
/// `relative`?
|
||||
pub fn is_length(id: &str, key: &str, relative: bool) -> bool {
|
||||
catalog_index().get(id).is_some_and(|info| info.lengths.keys(relative).any(|k| k == key))
|
||||
}
|
||||
|
||||
/// The distance parameters of effect `id` (none for unknown effects).
|
||||
fn lengths_of(id: &str) -> Lengths {
|
||||
let always = |always| Lengths { always, absolute: &[] };
|
||||
|
|
@ -316,14 +329,13 @@ pub fn scale_effect(e: &mut Effect, s: f64) {
|
|||
let Some(info) = catalog_index().get(e.id.as_str()) else { return };
|
||||
let merged = merged_params(&e.id, &e.params);
|
||||
let relative = util::flag(&merged, "relative", false);
|
||||
let keys = info.lengths.always.iter().chain(if relative { &[][..] } else { info.lengths.absolute });
|
||||
for k in keys {
|
||||
for k in info.lengths.keys(relative) {
|
||||
let v = util::num(&merged, k, f64::NAN);
|
||||
if v.is_finite() {
|
||||
if !e.params.is_object() {
|
||||
e.params = Value::Object(Map::new());
|
||||
}
|
||||
e.params[*k] = json!(v * s);
|
||||
e.params[k] = json!(v * s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -123,11 +123,11 @@ impl CornerDrag {
|
|||
}
|
||||
|
||||
/// The radius readout next to the pointer.
|
||||
pub fn overlays(&self) -> Vec<Overlay> {
|
||||
pub fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
|
||||
if !self.began {
|
||||
return vec![];
|
||||
}
|
||||
vec![Overlay::Measure { p: self.at, text: format!("Radius: {}", crate::xform::fmt_pt(self.radius)) }]
|
||||
vec![Overlay::Measure { p: self.at, text: format!("Radius: {}", cx.len(self.radius)) }]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -231,10 +231,10 @@ impl Tool for DirectSelectionTool {
|
|||
_ => vec![],
|
||||
}
|
||||
}
|
||||
fn overlays(&self, _cx: &ToolContext) -> Vec<Overlay> {
|
||||
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
|
||||
match &self.state {
|
||||
State::Marquee { start, cur, .. } => vec![Overlay::Marquee(Rect::from_points(*start, *cur))],
|
||||
State::Corner(c) => c.overlays(),
|
||||
State::Corner(c) => c.overlays(cx),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -386,7 +386,7 @@ impl Tool for WidthTool {
|
|||
let q = d.spot.p;
|
||||
out.push(Overlay::Line { a: q + d.spot.n * d.left, b: q - d.spot.n * d.right, color: BLUE, dashed: false });
|
||||
out.push(Overlay::Path { path: diamond(q, r), color: BLUE, width: 2.0, dashed: false });
|
||||
out.push(Overlay::Measure { p: q + d.spot.n * (d.left + r * 3.0), text: format!("W: {:.2} pt", d.left + d.right) });
|
||||
out.push(Overlay::Measure { p: q + d.spot.n * (d.left + r * 3.0), text: format!("W: {}", cx.stroke_unit.readout(d.left + d.right)) });
|
||||
} else if let Some(h) = &self.hover {
|
||||
out.push(Overlay::Line { a: h.p + h.n * h.left, b: h.p - h.n * h.right, color: BLUE, dashed: true });
|
||||
out.push(Overlay::Path { path: diamond(h.p, r), color: BLUE, width: 1.0, dashed: false });
|
||||
|
|
|
|||
|
|
@ -182,11 +182,11 @@ impl Tool for FamilyTool {
|
|||
vec![]
|
||||
}
|
||||
}
|
||||
fn overlays(&self, _cx: &ToolContext) -> Vec<Overlay> {
|
||||
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
|
||||
match self.start {
|
||||
Some(s) if self.began => {
|
||||
let d = self.last - s;
|
||||
vec![Overlay::Measure { p: self.last, text: format!("W: {:.2} pt\nH: {:.2} pt", d.x.abs(), d.y.abs()) }]
|
||||
vec![Overlay::Measure { p: self.last, text: cx.size_label(d.x.abs(), d.y.abs()) }]
|
||||
}
|
||||
_ => vec![],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ pub mod xform;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use vectorcraft_color::Paint;
|
||||
use vectorcraft_doc::{Document, NodeId, Selection};
|
||||
use vectorcraft_doc::{Document, NodeId, Selection, Unit};
|
||||
use vectorcraft_geom::{BezPath, Point, Rect};
|
||||
|
||||
pub use catalog::{TOOL_GROUPS, ToolInfo, tool_info};
|
||||
|
|
@ -201,6 +201,10 @@ pub struct ToolContext<'a> {
|
|||
pub raster_sample: u32,
|
||||
/// General → Use Preview Bounds: the bounding box measures visual bounds (strokes included).
|
||||
pub preview_bounds: bool,
|
||||
/// Units ▸ General: the unit measurement labels show lengths in.
|
||||
pub unit: Unit,
|
||||
/// Units ▸ Stroke: the unit measurement labels show stroke widths in.
|
||||
pub stroke_unit: Unit,
|
||||
}
|
||||
|
||||
impl ToolContext<'_> {
|
||||
|
|
@ -208,6 +212,18 @@ impl ToolContext<'_> {
|
|||
pub fn tol(&self, px: f64) -> f64 {
|
||||
px / self.zoom.max(1e-9)
|
||||
}
|
||||
/// A length as measurement labels show it, in the General unit (`12.50 mm`).
|
||||
pub fn len(&self, v: f64) -> String {
|
||||
self.unit.readout(v)
|
||||
}
|
||||
/// A size label: `W: …` over `H: …`.
|
||||
pub fn size_label(&self, w: f64, h: f64) -> String {
|
||||
format!("W: {}\nH: {}", self.len(w), self.len(h))
|
||||
}
|
||||
/// A move label: `dX: …` over `dY: …`.
|
||||
pub fn offset_label(&self, dx: f64, dy: f64) -> String {
|
||||
format!("dX: {}\ndY: {}", self.len(dx), self.len(dy))
|
||||
}
|
||||
pub fn hit_options(&self) -> vectorcraft_doc::hit::HitOptions {
|
||||
vectorcraft_doc::hit::HitOptions { tol: self.tol(3.0), outline: self.outline, path_only: false }
|
||||
}
|
||||
|
|
@ -384,6 +400,8 @@ pub(crate) mod testutil {
|
|||
freeform_point: None,
|
||||
raster_sample: 1,
|
||||
preview_bounds: false,
|
||||
unit: Unit::Points,
|
||||
stroke_unit: Unit::Points,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,14 +160,14 @@ impl Tool for SelectionTool {
|
|||
self.guides.clear();
|
||||
}
|
||||
self.state = State::Moving { start, began: true };
|
||||
self.measure = Some((p, format!("dX: {:.2} pt\ndY: {:.2} pt", d.x, d.y)));
|
||||
self.measure = Some((p, cx.offset_label(d.x, d.y)));
|
||||
out.push(Action::Preview("object.transform".into(), json!({ "matrix": matrix_json(Affine::translate(d)), "copy": m.alt })));
|
||||
out
|
||||
}
|
||||
(PointerKind::Drag, State::Scaling { handle, rect, .. }) => {
|
||||
let a = scale_for_drag(rect, handle, p, m.shift, m.alt);
|
||||
let nr = a.transform_rect_bbox(rect);
|
||||
self.measure = Some((p, format!("W: {:.2} pt\nH: {:.2} pt", nr.width(), nr.height())));
|
||||
self.measure = Some((p, cx.size_label(nr.width(), nr.height())));
|
||||
vec![Action::Preview("object.transform".into(), json!({ "matrix": matrix_json(a), "copy": false }))]
|
||||
}
|
||||
(PointerKind::Drag, State::Rotating { center, start, .. }) => {
|
||||
|
|
@ -217,11 +217,11 @@ impl Tool for SelectionTool {
|
|||
}
|
||||
}
|
||||
|
||||
fn overlays(&self, _cx: &ToolContext) -> Vec<Overlay> {
|
||||
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
|
||||
let mut o = vec![];
|
||||
match &self.state {
|
||||
State::Marquee { start, cur, .. } => o.push(Overlay::Marquee(Rect::from_points(*start, *cur))),
|
||||
State::Corner(c) => o.extend(c.overlays()),
|
||||
State::Corner(c) => o.extend(c.overlays(cx)),
|
||||
_ => {}
|
||||
}
|
||||
o.extend(self.guides.iter().cloned());
|
||||
|
|
|
|||
|
|
@ -185,12 +185,12 @@ impl Tool for ShapeTool {
|
|||
vec![]
|
||||
}
|
||||
}
|
||||
fn overlays(&self, _cx: &ToolContext) -> Vec<Overlay> {
|
||||
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
|
||||
match self.start {
|
||||
Some(s) if self.began => {
|
||||
let d = self.last - s;
|
||||
let mut o = self.guides.clone();
|
||||
o.push(Overlay::Measure { p: self.last, text: format!("W: {:.2} pt\nH: {:.2} pt", d.x.abs(), d.y.abs()) });
|
||||
o.push(Overlay::Measure { p: self.last, text: cx.size_label(d.x.abs(), d.y.abs()) });
|
||||
o
|
||||
}
|
||||
_ => vec![],
|
||||
|
|
|
|||
|
|
@ -86,7 +86,3 @@ pub fn paint_owner(doc: &Document, leaf: NodeId) -> NodeId {
|
|||
_ => leaf,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fmt_pt(v: f64) -> String {
|
||||
format!("{v:.2} pt")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -172,10 +172,7 @@ impl Tool for ArtboardTool {
|
|||
if let Some(Drag::Create { start, cur }) = self.drag {
|
||||
let r = Rect::from_points(start, cur);
|
||||
o.push(Overlay::Marquee(r));
|
||||
o.push(Overlay::Measure {
|
||||
p: cur + Vec2::new(cx.tol(12.0), cx.tol(12.0)),
|
||||
text: format!("W: {:.2} pt\nH: {:.2} pt", r.width(), r.height()),
|
||||
});
|
||||
o.push(Overlay::Measure { p: cur + Vec2::new(cx.tol(12.0), cx.tol(12.0)), text: cx.size_label(r.width(), r.height()) });
|
||||
return o;
|
||||
}
|
||||
let Some(ab) = cx.doc.artboards.get(self.active) else { return o };
|
||||
|
|
@ -188,7 +185,7 @@ impl Tool for ArtboardTool {
|
|||
if let Some(Drag::Resize { began: true, .. }) = self.drag {
|
||||
o.push(Overlay::Measure {
|
||||
p: Point::new(r.x1, r.y1) + Vec2::new(cx.tol(12.0), cx.tol(12.0)),
|
||||
text: format!("W: {:.2} pt\nH: {:.2} pt", r.width(), r.height()),
|
||||
text: cx.size_label(r.width(), r.height()),
|
||||
});
|
||||
}
|
||||
o
|
||||
|
|
|
|||
|
|
@ -148,7 +148,7 @@ impl FreeTransformTool {
|
|||
r.contains(p).then_some(Op::Move)
|
||||
}
|
||||
|
||||
fn preview(&mut self, d: Drag, p: Point, m: Mods) -> Action {
|
||||
fn preview(&mut self, cx: &ToolContext, d: Drag, p: Point, m: Mods) -> Action {
|
||||
let shift = m.shift || self.constrain;
|
||||
let delta = p - d.start;
|
||||
let xf = match d.op {
|
||||
|
|
@ -161,13 +161,13 @@ impl FreeTransformTool {
|
|||
}
|
||||
Op::Move => {
|
||||
let v = move_delta(d.start, p, shift);
|
||||
self.measure = Some((p, format!("dX: {:.2} pt\ndY: {:.2} pt", v.x, v.y)));
|
||||
self.measure = Some((p, cx.offset_label(v.x, v.y)));
|
||||
Affine::translate(v)
|
||||
}
|
||||
Op::Scale(h) => {
|
||||
let a = scale_for_drag(d.rect, h, p, shift, m.alt);
|
||||
let nr = a.transform_rect_bbox(d.rect);
|
||||
self.measure = Some((p, format!("W: {:.2} pt\nH: {:.2} pt", nr.width(), nr.height())));
|
||||
self.measure = Some((p, cx.size_label(nr.width(), nr.height())));
|
||||
a
|
||||
}
|
||||
Op::Shear(h) => shear_for_side(d.rect, h, delta, m.alt),
|
||||
|
|
@ -220,7 +220,7 @@ impl Tool for FreeTransformTool {
|
|||
out.push(Action::Begin(label.into()));
|
||||
}
|
||||
self.drag = Some(d);
|
||||
out.push(self.preview(d, p, ev.mods));
|
||||
out.push(self.preview(cx, d, p, ev.mods));
|
||||
out
|
||||
}
|
||||
PointerKind::Up => {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
use serde_json::{Value, json};
|
||||
use vectorcraft_geom::{Point, Vec2};
|
||||
|
||||
use super::{CYAN, fmt_pt};
|
||||
use super::CYAN;
|
||||
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext};
|
||||
|
||||
#[derive(Default)]
|
||||
|
|
@ -62,7 +62,7 @@ impl Tool for MeasureTool {
|
|||
Overlay::Line { a, b, color: CYAN, dashed: false },
|
||||
Overlay::Measure {
|
||||
p: b + Vec2::new(cx.tol(12.0), cx.tol(12.0)),
|
||||
text: format!("D: {}\nAngle: {ang:.1}°\ndX: {}\ndY: {}", fmt_pt(dist), fmt_pt(dx), fmt_pt(dy)),
|
||||
text: format!("D: {}\nAngle: {ang:.1}°\n{}", cx.len(dist), cx.offset_label(dx, dy)),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
use egui::{Color32, CornerRadius, Pos2, Sense, Shape, Stroke, StrokeKind, Ui, pos2, vec2};
|
||||
use serde_json::json;
|
||||
use vectorcraft_doc::{Node, NodeKind};
|
||||
use vectorcraft_doc::{Node, NodeKind, Unit};
|
||||
use vectorcraft_geom::{Affine, BezPath, PathEl, Point, Rect};
|
||||
use vectorcraft_tools::{Cursor, Mods, Overlay, PointerEvent, PointerKind};
|
||||
|
||||
|
|
@ -296,7 +296,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
}
|
||||
|
||||
if app.ui.view.rulers && app.ui.screen_mode < 3 {
|
||||
rulers(ui, full, &xf, app.hover_doc, &t);
|
||||
rulers(ui, full, &xf, app.hover_doc, app.session.general_unit(), &t);
|
||||
}
|
||||
if app.ui.task_bar && !app.session.tool_busy() && app.ui.screen_mode < 3 {
|
||||
task_bar(app, ui, &xf);
|
||||
|
|
@ -573,7 +573,16 @@ fn grid(p: &egui::Painter, xf: &Xf, spacing: f64, subdiv: u32) {
|
|||
}
|
||||
}
|
||||
|
||||
fn rulers(ui: &Ui, full: egui::Rect, xf: &Xf, hover: Option<Point>, t: &Tokens) {
|
||||
/// A ruler label for `v` (in the ruler's unit) on a ruler whose labels are `step` apart: whole
|
||||
/// numbers, or as many decimals as the step has.
|
||||
pub(crate) fn ruler_label(v: f64, step: f64) -> String {
|
||||
let decimals = if step >= 1.0 { 0 } else { (-step.log10()).ceil() as usize };
|
||||
let s = format!("{v:.decimals$}");
|
||||
if s.trim_start_matches('-').chars().all(|c| c == '0' || c == '.') { "0".into() } else { s }
|
||||
}
|
||||
|
||||
/// The rulers, numbered in `unit` (the General unit).
|
||||
fn rulers(ui: &Ui, full: egui::Rect, xf: &Xf, hover: Option<Point>, unit: Unit, t: &Tokens) {
|
||||
let p = ui.painter();
|
||||
let top = egui::Rect::from_min_max(pos2(full.left() + RULER, full.top()), pos2(full.right(), full.top() + RULER));
|
||||
let left = egui::Rect::from_min_max(pos2(full.left(), full.top() + RULER), pos2(full.left() + RULER, full.bottom()));
|
||||
|
|
@ -586,17 +595,19 @@ fn rulers(ui: &Ui, full: egui::Rect, xf: &Xf, hover: Option<Point>, t: &Tokens)
|
|||
// Crosshair in the origin box.
|
||||
p.line_segment([corner.center() - vec2(4.0, 0.0), corner.center() + vec2(4.0, 0.0)], Stroke::new(1.0, t.ruler_tick));
|
||||
p.line_segment([corner.center() - vec2(0.0, 4.0), corner.center() + vec2(0.0, 4.0)], Stroke::new(1.0, t.ruler_tick));
|
||||
// Pick a label step that gives ≥ 50 px between labels.
|
||||
let steps = [1.0, 2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 2000.0, 5000.0, 10000.0];
|
||||
let step = steps.iter().copied().find(|s| s * xf.zoom >= 50.0).unwrap_or(10000.0);
|
||||
// Pick a label step (in `unit`) that gives ≥ 50 px between labels; positions below are in `unit`
|
||||
// (`per` points each).
|
||||
let per = unit.points();
|
||||
let steps = [0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, 25.0, 50.0, 100.0, 200.0, 500.0, 1000.0, 2000.0, 5000.0, 10000.0];
|
||||
let step = steps.iter().copied().find(|s| s * per * xf.zoom >= 50.0).unwrap_or(10000.0);
|
||||
let minor = step / 10.0;
|
||||
let font = egui::FontId::proportional(9.5);
|
||||
let a = xf.to_doc(top.left_top());
|
||||
let b = xf.to_doc(top.right_top());
|
||||
let clip_top = p.with_clip_rect(top);
|
||||
let mut x = (a.x / minor).floor() * minor;
|
||||
while x <= b.x {
|
||||
let sx = xf.to_screen(Point::new(x, 0.0)).x;
|
||||
let mut x = (a.x / per / minor).floor() * minor;
|
||||
while x * per <= b.x {
|
||||
let sx = xf.to_screen(Point::new(x * per, 0.0)).x;
|
||||
let is_major = ((x / step).round() * step - x).abs() < minor * 0.01;
|
||||
let is_mid = ((x / (step / 2.0)).round() * (step / 2.0) - x).abs() < minor * 0.01;
|
||||
let len = if is_major {
|
||||
|
|
@ -608,16 +619,16 @@ fn rulers(ui: &Ui, full: egui::Rect, xf: &Xf, hover: Option<Point>, t: &Tokens)
|
|||
};
|
||||
clip_top.line_segment([pos2(sx, top.bottom() - len), pos2(sx, top.bottom())], Stroke::new(1.0, t.ruler_tick));
|
||||
if is_major {
|
||||
clip_top.text(pos2(sx + 2.0, top.top() + 1.0), egui::Align2::LEFT_TOP, format!("{}", x.round() as i64), font.clone(), t.ruler_tick);
|
||||
clip_top.text(pos2(sx + 2.0, top.top() + 1.0), egui::Align2::LEFT_TOP, ruler_label(x, step), font.clone(), t.ruler_tick);
|
||||
}
|
||||
x += minor;
|
||||
}
|
||||
let a = xf.to_doc(left.left_top());
|
||||
let b = xf.to_doc(left.left_bottom());
|
||||
let clip_left = p.with_clip_rect(left);
|
||||
let mut y = (a.y / minor).floor() * minor;
|
||||
while y <= b.y {
|
||||
let sy = xf.to_screen(Point::new(0.0, y)).y;
|
||||
let mut y = (a.y / per / minor).floor() * minor;
|
||||
while y * per <= b.y {
|
||||
let sy = xf.to_screen(Point::new(0.0, y * per)).y;
|
||||
let is_major = ((y / step).round() * step - y).abs() < minor * 0.01;
|
||||
let is_mid = ((y / (step / 2.0)).round() * (step / 2.0) - y).abs() < minor * 0.01;
|
||||
let len = if is_major {
|
||||
|
|
@ -630,7 +641,7 @@ fn rulers(ui: &Ui, full: egui::Rect, xf: &Xf, hover: Option<Point>, t: &Tokens)
|
|||
clip_left.line_segment([pos2(left.right() - len, sy), pos2(left.right(), sy)], Stroke::new(1.0, t.ruler_tick));
|
||||
if is_major {
|
||||
// Vertical labels read top-to-bottom, one digit per line like Illustrator.
|
||||
let s = format!("{}", y.round() as i64);
|
||||
let s = ruler_label(y, step);
|
||||
for (k, ch) in s.chars().enumerate() {
|
||||
clip_left.text(
|
||||
pos2(left.left() + 4.0, sy + 2.0 + k as f32 * 8.5),
|
||||
|
|
@ -1013,17 +1024,21 @@ fn home(app: &mut VectorcraftApp, ui: &mut Ui, rect: egui::Rect) {
|
|||
ui.add_space(28.0);
|
||||
ui.label(egui::RichText::new("Quickly start a new file").font(theme::semibold(14.0)).color(t.text));
|
||||
ui.add_space(10.0);
|
||||
let presets: [(&str, &str, f64, f64); 6] = [
|
||||
("Letter", "612 × 792 pt", 612.0, 792.0),
|
||||
("A4", "595.28 × 841.89 pt", 595.28, 841.89),
|
||||
("Web 1920", "1920 × 1080 px", 1920.0, 1080.0),
|
||||
("Mobile", "390 × 844 px", 390.0, 844.0),
|
||||
("Postcard", "288 × 432 pt", 432.0, 288.0),
|
||||
("Square", "1080 × 1080 px", 1080.0, 1080.0),
|
||||
// (name, width, height in points, in pixels); print sizes show in Preferences ▸ Units ▸ General.
|
||||
let presets: [(&str, f64, f64, bool); 6] = [
|
||||
("Letter", 612.0, 792.0, false),
|
||||
("A4", 595.28, 841.89, false),
|
||||
("Web 1920", 1920.0, 1080.0, true),
|
||||
("Mobile", 390.0, 844.0, true),
|
||||
("Postcard", 432.0, 288.0, false),
|
||||
("Square", 1080.0, 1080.0, true),
|
||||
];
|
||||
let print = app.session.default_units();
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing = vec2(14.0, 14.0);
|
||||
for (name, size, w, h) in presets {
|
||||
for (name, w, h, pixels) in presets {
|
||||
let unit = if pixels { Unit::Pixels } else { print };
|
||||
let size = format!("{} × {}", unit.number(w), unit.format(h));
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(120.0, 132.0), Sense::click());
|
||||
ui.painter().rect_filled(r, CornerRadius::same(8), if resp.hovered() { t.hover } else { t.panel });
|
||||
let s = (70.0 / w.max(h)) as f32;
|
||||
|
|
@ -1033,7 +1048,7 @@ fn home(app: &mut VectorcraftApp, ui: &mut Ui, rect: egui::Rect) {
|
|||
ui.painter().text(r.center_bottom() - vec2(0.0, 30.0), egui::Align2::CENTER_CENTER, name, theme::semibold(12.5), t.text);
|
||||
ui.painter().text(r.center_bottom() - vec2(0.0, 14.0), egui::Align2::CENTER_CENTER, size, egui::FontId::proportional(11.0), t.text_dim);
|
||||
if resp.clicked() {
|
||||
app.run("file.new", json!({"width": w, "height": h, "units": if size.ends_with("px") { "Pixels" } else { "Points" }})).ok();
|
||||
app.run("file.new", json!({"width": w, "height": h, "units": unit.label()})).ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
crate::brand::paint_mark(ui, r);
|
||||
ui.add_space(4.0);
|
||||
if widgets::icon_button(ui, "house", "Home", false, 24.0).clicked() {
|
||||
app.ui.dialog = Some(crate::state::Dialog::new("newDocument", json!({"preset": "Letter", "width": "612 pt", "height": "792 pt", "units": "Points", "artboards": 1, "colorMode": "RGB", "name": "Untitled-1"})));
|
||||
crate::dialogs::open_new_document(app);
|
||||
}
|
||||
ui.add_space(2.0);
|
||||
let menus_end = if app.native_menu {
|
||||
|
|
@ -66,9 +66,20 @@ pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
let open_palette = if search_full {
|
||||
let (r, resp) = ui.allocate_exact_size(vec2(200.0, 24.0), Sense::click());
|
||||
ui.painter().rect_filled(r, CornerRadius::same(12), t.input);
|
||||
ui.painter().rect_stroke(r, CornerRadius::same(12), Stroke::new(1.0, if resp.hovered() { t.input_border } else { t.divider }), StrokeKind::Inside);
|
||||
ui.painter().rect_stroke(
|
||||
r,
|
||||
CornerRadius::same(12),
|
||||
Stroke::new(1.0, if resp.hovered() { t.input_border } else { t.divider }),
|
||||
StrokeKind::Inside,
|
||||
);
|
||||
icons::paint(ui, "search", egui::Rect::from_center_size(r.left_center() + vec2(14.0, 0.0), vec2(13.0, 13.0)), t.text_dim);
|
||||
ui.painter().text(r.left_center() + vec2(26.0, 0.0), egui::Align2::LEFT_CENTER, "Search commands and tools", egui::FontId::proportional(11.5), t.text_dim);
|
||||
ui.painter().text(
|
||||
r.left_center() + vec2(26.0, 0.0),
|
||||
egui::Align2::LEFT_CENTER,
|
||||
"Search commands and tools",
|
||||
egui::FontId::proportional(11.5),
|
||||
t.text_dim,
|
||||
);
|
||||
resp.clicked()
|
||||
} else {
|
||||
widgets::icon_button(ui, "search", "Search commands and tools", false, 24.0).clicked()
|
||||
|
|
@ -106,7 +117,7 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
return;
|
||||
};
|
||||
let sel = st.selection.objects.clone();
|
||||
let units = st.doc.units;
|
||||
let units = app.session.general_unit();
|
||||
let first = sel.first().and_then(|id| st.doc.node(*id)).cloned();
|
||||
let anchor_mode = !st.selection.anchors.is_empty();
|
||||
let label = match &first {
|
||||
|
|
@ -185,7 +196,7 @@ pub fn control_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
[("x", "X:", b.center().x), ("y", "Y:", b.center().y), ("width", "W:", b.width()), ("height", "H:", b.height())]
|
||||
{
|
||||
ui.label(egui::RichText::new(lbl).size(12.0).color(t.text_dim));
|
||||
if let Some(nv) = widgets::num_field(ui, ("cb", k), Some(v), units, 74.0) {
|
||||
if let Some(nv) = widgets::num_field(ui, ("cb", k), Some(v), units, 80.0) {
|
||||
app.run("object.setBounds", json!({k: nv, "reference": 4})).ok();
|
||||
}
|
||||
}
|
||||
|
|
@ -360,7 +371,10 @@ pub fn status_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
.color(t.text_disabled),
|
||||
);
|
||||
if let Some(p) = app.hover_doc {
|
||||
ui.label(egui::RichText::new(format!("X: {:.2} Y: {:.2}", p.x, p.y)).font(theme::mono(10.5)).color(t.text_dim));
|
||||
let u = app.session.general_unit();
|
||||
ui.label(
|
||||
egui::RichText::new(format!("X: {} Y: {}", u.readout(p.x), u.readout(p.y))).font(theme::mono(10.5)).color(t.text_dim),
|
||||
);
|
||||
}
|
||||
if !app.ui.status.is_empty() {
|
||||
ui.label(egui::RichText::new(&app.ui.status).size(11.0).color(t.text));
|
||||
|
|
|
|||
|
|
@ -9,14 +9,28 @@ use crate::state::Dialog;
|
|||
|
||||
pub(super) const SPEC: DialogSpec = DialogSpec {
|
||||
heading: |d| d.str("__label"),
|
||||
body: |_, ui, d| {
|
||||
form::param_fields(ui, d);
|
||||
body: |app, ui, d| {
|
||||
let lengths = lengths(&d.str("__command"));
|
||||
form::param_fields(ui, d, &|k| lengths.contains(&k), app.session.general_unit());
|
||||
false
|
||||
},
|
||||
confirm,
|
||||
..DialogSpec::FORM
|
||||
};
|
||||
|
||||
/// The parameters of the commands this dialog edits that are distances.
|
||||
fn lengths(command: &str) -> &'static [&'static str] {
|
||||
match command {
|
||||
"graph.create" => &["width", "height"],
|
||||
"shape.flare" => &["diameter", "pathLength"],
|
||||
"artboard.rearrange" => &["spacing"],
|
||||
"perspective.grid.set" => &["cell", "distance"],
|
||||
"object.repeat.options" => &["radius", "hSpacing", "vSpacing"],
|
||||
"text.areaOptions" => &["gutter", "inset", "firstBaselineMin"],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes before running, so a dialog the command opens stays open.
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
let cmd = d.str("__command");
|
||||
|
|
|
|||
|
|
@ -111,7 +111,8 @@ fn confirm_exists(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String>
|
|||
}
|
||||
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
let changed = form::param_fields(ui, d);
|
||||
let (id, relative) = (d.str("__effect"), d.bool("relative"));
|
||||
let changed = form::param_fields(ui, d, &|k| vectorcraft_effects::is_length(&id, k, relative), app.session.general_unit());
|
||||
ui.add_space(6.0);
|
||||
let mut pv = d.bool("preview");
|
||||
let pv_changed = ui.checkbox(&mut pv, "Preview").changed();
|
||||
|
|
|
|||
|
|
@ -2,18 +2,56 @@
|
|||
//! and the typed parameter editor of the command and effect dialogs.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::Unit;
|
||||
|
||||
use crate::state::Dialog;
|
||||
use crate::theme::Tokens;
|
||||
|
||||
/// Width of a dialog's value fields ([`text`] and [`length`], frame included).
|
||||
const FIELD_W: f32 = 132.0;
|
||||
|
||||
/// A labelled text field bound to `d.fields[key]` (one grid row).
|
||||
pub(super) fn field(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
ui.label(egui::RichText::new(label).color(t.text_dim));
|
||||
text(ui, d, key, 120.0);
|
||||
text(ui, d, key, FIELD_W - 12.0);
|
||||
ui.end_row();
|
||||
}
|
||||
|
||||
/// A labelled [`length`] field (one grid row).
|
||||
pub(super) fn length_field(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str, unit: Unit) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
ui.label(egui::RichText::new(label).color(t.text_dim));
|
||||
length(ui, d, key, unit, FIELD_W);
|
||||
ui.end_row();
|
||||
}
|
||||
|
||||
/// A distance field `width` wide bound to `d.fields[key]`: shown and typed in `unit` (a typed unit,
|
||||
/// "5 mm", wins), kept in points (a string with a unit, "12 pt", also reads). Returns true when it
|
||||
/// changed.
|
||||
pub(super) fn length(ui: &mut egui::Ui, d: &mut Dialog, key: &str, unit: Unit, width: f32) -> bool {
|
||||
let value = d.fields.contains_key(key).then(|| d.f64(key, 0.0));
|
||||
let Some(v) = crate::widgets::num_field(ui, ("dlg-len", key), value, unit, width) else { return false };
|
||||
d.fields.insert(key.into(), json!(v));
|
||||
true
|
||||
}
|
||||
|
||||
/// 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"],
|
||||
"rectangle" | "ellipse" | "artboardOptions" => &["width", "height"],
|
||||
"roundedRectangle" => &["width", "height", "radius"],
|
||||
"polygon" => &["radius"],
|
||||
"star" => &["radius1", "radius2"],
|
||||
"lineSegment" => &["length"],
|
||||
"offsetPath" => &["offset"],
|
||||
"simplify" => &["tolerance"],
|
||||
"splitIntoGrid" => &["gutter"],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
|
||||
/// A text field `width` wide bound to `d.fields[key]`. Returns true when it changed.
|
||||
pub(super) fn text(ui: &mut egui::Ui, d: &mut Dialog, key: &str, width: f32) -> bool {
|
||||
text_edit(ui, d, key, width).changed()
|
||||
|
|
@ -43,15 +81,21 @@ pub(super) fn check(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str) {
|
|||
}
|
||||
}
|
||||
|
||||
/// The generic dialog body: a text field per non-boolean value (positions and indices hidden).
|
||||
pub(super) fn grid(ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
/// The generic dialog body: a field per non-boolean value (positions and indices hidden), the
|
||||
/// distances in `unit`.
|
||||
pub(super) fn grid(ui: &mut egui::Ui, d: &mut Dialog, unit: Unit) {
|
||||
let lengths = lengths(&d.kind);
|
||||
egui::Grid::new("dlg").num_columns(2).spacing([10.0, 8.0]).show(ui, |ui| {
|
||||
let keys: Vec<(String, Value)> = d.fields.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
|
||||
for (k, v) in keys {
|
||||
if k == "x" || k == "y" || k == "origin" || k == "index" || v.is_boolean() {
|
||||
continue;
|
||||
}
|
||||
field(ui, d, &k, &humanize(&k));
|
||||
if lengths.contains(&k.as_str()) {
|
||||
length_field(ui, d, &k, &humanize(&k), unit);
|
||||
} else {
|
||||
field(ui, d, &k, &humanize(&k));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -61,9 +105,10 @@ pub(super) fn params(d: &Dialog) -> Value {
|
|||
Value::Object(d.fields.iter().filter(|(k, _)| !k.starts_with("__") && k.as_str() != "preview").map(|(k, v)| (k.clone(), v.clone())).collect())
|
||||
}
|
||||
|
||||
/// Generic editor for command/effect parameters: numbers, booleans, strings and colours.
|
||||
/// Returns true when a value changed.
|
||||
pub(super) fn param_fields(ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
/// Generic editor for command/effect parameters: numbers, booleans, strings and colours; the
|
||||
/// parameters `is_length` names are distances shown and typed in `unit`. Returns true when a
|
||||
/// value changed.
|
||||
pub(super) fn param_fields(ui: &mut egui::Ui, d: &mut Dialog, is_length: &dyn Fn(&str) -> bool, unit: Unit) -> bool {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let mut changed = false;
|
||||
egui::Grid::new("fxgrid").num_columns(2).spacing([12.0, 8.0]).show(ui, |ui| {
|
||||
|
|
@ -79,6 +124,11 @@ pub(super) fn param_fields(ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
|||
ui.end_row();
|
||||
continue;
|
||||
}
|
||||
if is_length(&k) {
|
||||
changed |= length(ui, d, &k, unit, 140.0);
|
||||
ui.end_row();
|
||||
continue;
|
||||
}
|
||||
match v {
|
||||
Value::Number(n) => {
|
||||
let mut x = n.as_f64().unwrap_or(0.0);
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ use serde_json::{Value, json};
|
|||
|
||||
pub use color_picker::open as open_color_picker;
|
||||
pub use effect::open as open_effect_dialog;
|
||||
pub use new_document::open as open_new_document;
|
||||
pub use save_pdf::open as open_save_pdf;
|
||||
pub use tools::open_tool_dialog;
|
||||
|
||||
|
|
@ -82,8 +83,8 @@ impl DialogSpec {
|
|||
pub const FORM: Self = Self {
|
||||
window: None,
|
||||
heading: |_| "Dialog".into(),
|
||||
body: |_, ui, d| {
|
||||
form::grid(ui, d);
|
||||
body: |app, ui, d| {
|
||||
form::grid(ui, d, app.session.general_unit());
|
||||
false
|
||||
},
|
||||
confirm: |app, _| {
|
||||
|
|
|
|||
|
|
@ -1,39 +1,57 @@
|
|||
//! File → New: presets and the document details (name, size, artboards, colour mode).
|
||||
//! File → New: presets and the document details (name, size, units, artboards, colour mode).
|
||||
//!
|
||||
//! Fields: `preset`, `name`, `width` and `height` (points, or strings with a unit), `units` (a unit
|
||||
//! label; print presets start in Preferences ▸ Units ▸ General), `artboards`, `colorMode`.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::Unit;
|
||||
|
||||
use super::{DialogSpec, form};
|
||||
use crate::VectorcraftApp;
|
||||
use crate::state::Dialog;
|
||||
use crate::theme::{self, Tokens};
|
||||
use crate::{VectorcraftApp, widgets};
|
||||
|
||||
pub(super) const SPEC: DialogSpec =
|
||||
DialogSpec { heading: |_| "New Document".into(), body, confirm, ok: Some("Create"), min_width: 560.0, ..DialogSpec::FORM };
|
||||
|
||||
fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
/// The presets: (name, width, height in points, in pixels).
|
||||
const PRESETS: [(&str, f64, f64, bool); 8] = [
|
||||
("Letter", 612.0, 792.0, false),
|
||||
("Legal", 612.0, 1008.0, false),
|
||||
("Tabloid", 792.0, 1224.0, false),
|
||||
("A4", 595.28, 841.89, false),
|
||||
("A3", 841.89, 1190.55, false),
|
||||
("Web 1920", 1920.0, 1080.0, true),
|
||||
("Phone 390×844", 390.0, 844.0, true),
|
||||
("Square Post", 1080.0, 1080.0, true),
|
||||
];
|
||||
|
||||
/// The units a preset starts in: pixels for screen presets, else the General preference.
|
||||
fn preset_units(app: &VectorcraftApp, pixels: bool) -> Unit {
|
||||
if pixels { Unit::Pixels } else { app.session.default_units() }
|
||||
}
|
||||
|
||||
/// Open the dialog on the Letter preset.
|
||||
pub fn open(app: &mut VectorcraftApp) {
|
||||
let units = preset_units(app, false).label();
|
||||
let fields = json!({"preset": "Letter", "width": 612, "height": 792, "units": units, "artboards": 1, "colorMode": "RGB", "name": "Untitled-1"});
|
||||
app.ui.dialog = Some(Dialog::new("newDocument", fields));
|
||||
}
|
||||
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let presets: [(&str, &str, &str, &str); 8] = [
|
||||
("Letter", "612 pt", "792 pt", "Points"),
|
||||
("Legal", "612 pt", "1008 pt", "Points"),
|
||||
("Tabloid", "792 pt", "1224 pt", "Points"),
|
||||
("A4", "595.28 pt", "841.89 pt", "Points"),
|
||||
("A3", "841.89 pt", "1190.55 pt", "Points"),
|
||||
("Web 1920", "1920 px", "1080 px", "Pixels"),
|
||||
("Phone 390×844", "390 px", "844 px", "Pixels"),
|
||||
("Square Post", "1080 px", "1080 px", "Pixels"),
|
||||
];
|
||||
ui.horizontal(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.set_width(300.0);
|
||||
ui.label(egui::RichText::new("Presets").color(t.text_dim));
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
for (name, w, h, u) in presets {
|
||||
for (name, w, h, pixels) in PRESETS {
|
||||
let sel = d.str("preset") == name;
|
||||
if ui.selectable_label(sel, name).clicked() {
|
||||
d.fields.insert("preset".into(), json!(name));
|
||||
d.fields.insert("width".into(), json!(w));
|
||||
d.fields.insert("height".into(), json!(h));
|
||||
d.fields.insert("units".into(), json!(u));
|
||||
d.fields.insert("units".into(), json!(preset_units(app, pixels).label()));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -41,10 +59,17 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
|||
ui.separator();
|
||||
ui.vertical(|ui| {
|
||||
ui.label(egui::RichText::new("Preset Details").font(theme::semibold(12.5)));
|
||||
let unit = Unit::named(&d.str("units")).unwrap_or_default();
|
||||
egui::Grid::new("newdoc").num_columns(2).spacing([10.0, 8.0]).show(ui, |ui| {
|
||||
form::field(ui, d, "name", "Name:");
|
||||
form::field(ui, d, "width", "Width:");
|
||||
form::field(ui, d, "height", "Height:");
|
||||
form::length_field(ui, d, "width", "Width:", unit);
|
||||
form::length_field(ui, d, "height", "Height:", unit);
|
||||
ui.label(egui::RichText::new("Units:").color(t.text_dim));
|
||||
let labels: Vec<&str> = Unit::ALL.iter().map(|u| u.label()).collect();
|
||||
if let Some(i) = widgets::dropdown(ui, "newdoc-units", unit.label(), &labels, 132.0) {
|
||||
d.fields.insert("units".into(), json!(labels[i]));
|
||||
}
|
||||
ui.end_row();
|
||||
form::field(ui, d, "artboards", "Artboards:");
|
||||
form::field(ui, d, "colorMode", "Color Mode:");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -326,7 +326,7 @@ fn compression(ui: &mut egui::Ui, d: &mut Dialog) {
|
|||
}
|
||||
|
||||
fn marks_and_bleeds(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
let unit = app.session.active().map_or(Unit::Points, |s| s.doc.units);
|
||||
let unit = app.session.general_unit();
|
||||
const MARKS: [(&str, &str); 4] = [
|
||||
("marks.trim", "Trim marks"),
|
||||
("marks.registration", "Registration marks"),
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ fn title(kind: &str) -> &'static str {
|
|||
}
|
||||
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
form::grid(ui, d);
|
||||
form::grid(ui, d, app.session.general_unit());
|
||||
ui.add_space(6.0);
|
||||
if d.kind == "scale" {
|
||||
form::check(ui, d, "uniform", "Uniform");
|
||||
|
|
|
|||
|
|
@ -37,23 +37,20 @@ fn params(d: &Dialog) -> Value {
|
|||
})
|
||||
}
|
||||
|
||||
/// A labelled number row of the field grid: `key` shown with `suffix` (document units for "pt").
|
||||
/// A labelled number row of the field grid: `key` shown with `suffix` (a distance in `units` for
|
||||
/// "pt").
|
||||
fn number(ui: &mut egui::Ui, d: &mut Dialog, key: &str, label: &str, suffix: &str, units: vectorcraft_doc::Unit) {
|
||||
widgets::dim_label(ui, label);
|
||||
let v = d.f64(key, 0.0);
|
||||
let new = if suffix == "pt" {
|
||||
widgets::num_field(ui, ("te", key), Some(v), units, 90.0)
|
||||
} else {
|
||||
widgets::plain_field(ui, ("te", key), v, suffix, 2, 90.0)
|
||||
};
|
||||
if let Some(n) = new {
|
||||
if suffix == "pt" {
|
||||
form::length(ui, d, key, units, 90.0);
|
||||
} else if let Some(n) = widgets::plain_field(ui, ("te", key), d.f64(key, 0.0), suffix, 2, 90.0) {
|
||||
d.fields.insert(key.into(), json!(n));
|
||||
}
|
||||
ui.end_row();
|
||||
}
|
||||
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
let units = app.session.active().map_or(vectorcraft_doc::Unit::Points, |s| s.doc.units);
|
||||
let units = app.session.general_unit();
|
||||
ui.horizontal_top(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
egui::Grid::new("te-grid").num_columns(2).spacing([10.0, 6.0]).show(ui, |ui| {
|
||||
|
|
|
|||
|
|
@ -378,10 +378,7 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
|||
};
|
||||
let r = match id {
|
||||
"file.newDialog" => {
|
||||
app.ui.dialog = Some(crate::state::Dialog::new(
|
||||
"newDocument",
|
||||
json!({"preset": "Letter", "width": "612 pt", "height": "792 pt", "units": "Points", "artboards": 1, "colorMode": "RGB", "name": "Untitled-1"}),
|
||||
));
|
||||
crate::dialogs::open_new_document(app);
|
||||
Ok(Value::Null)
|
||||
}
|
||||
"file.open" => match s("path") {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
|
||||
use egui::Ui;
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::Unit;
|
||||
|
||||
use super::{pstate, selection_len, set_pstate};
|
||||
use crate::VectorcraftApp;
|
||||
|
|
@ -100,7 +99,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
app.run("object.distributeSpacing", json!({"axis": "horizontal", "spacing": sp})).ok();
|
||||
}
|
||||
ui.add_enabled_ui(key, |ui| {
|
||||
if let Some(v) = widgets::spin_field(ui, "align-spacing", Some(spacing), Unit::Points, 70.0, 1.0, 0.0, &[]) {
|
||||
if let Some(v) = widgets::spin_field(ui, "align-spacing", Some(spacing), app.session.general_unit(), 70.0, 1.0, 0.0, &[]) {
|
||||
set_pstate(ui.ctx(), "align-spacing", Some(v));
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -328,7 +328,7 @@ fn default_stack(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
}
|
||||
chip(ui, Rect::from_min_size(pos2(r.left() + EYE_W + 76.0, r.center().y - 9.0), vec2(18.0, 18.0)), it.paint());
|
||||
if let AppearanceItem::Stroke(st) = it {
|
||||
let w = format!("{} pt", st.width);
|
||||
let w = app.session.stroke_unit().format(st.width);
|
||||
text(ui, pos2(r.left() + EYE_W + 106.0, r.center().y), &w, false);
|
||||
stroke_notes(ui, Rect::from_min_max(pos2(r.left() + EYE_W + 166.0, r.top()), pos2(r.right() - 4.0, r.bottom())), st);
|
||||
}
|
||||
|
|
@ -428,7 +428,7 @@ fn stack(app: &mut VectorcraftApp, ui: &mut Ui, n: &Node, sel: Sel, object_row:
|
|||
if let AppearanceItem::Stroke(st) = it {
|
||||
let fr = Rect::from_min_size(pos2(r.left() + EYE_W + 104.0, r.center().y - 12.0), vec2(56.0, 24.0));
|
||||
let mut child = ui.new_child(egui::UiBuilder::new().max_rect(fr).layout(egui::Layout::left_to_right(egui::Align::Center)));
|
||||
if let Some(w) = widgets::num_field(&mut child, ("ap-w", i), Some(st.width), vectorcraft_doc::Unit::Points, 56.0) {
|
||||
if let Some(w) = widgets::num_field(&mut child, ("ap-w", i), Some(st.width), app.session.stroke_unit(), 56.0) {
|
||||
app.run("appearance.setItem", json!({"index": i, "weight": w})).ok();
|
||||
}
|
||||
stroke_notes(ui, Rect::from_min_max(pos2(fr.right() + 6.0, r.top()), pos2(r.right() - 4.0, r.bottom())), st);
|
||||
|
|
@ -712,6 +712,7 @@ fn effect_editor(app: &mut VectorcraftApp, ui: &mut Ui, item: Option<usize>, k:
|
|||
}
|
||||
}
|
||||
let mut change: Option<(String, Value)> = None;
|
||||
let (unit, relative) = (app.session.general_unit(), params.get("relative").and_then(Value::as_bool).unwrap_or(false));
|
||||
egui::Frame::NONE.fill(t.panel_darker).inner_margin(egui::Margin { left: (EYE_W + 12.0) as i8, right: 6, top: 4, bottom: 4 }).show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing.y = 3.0;
|
||||
if params.is_empty() {
|
||||
|
|
@ -734,8 +735,13 @@ fn effect_editor(app: &mut VectorcraftApp, ui: &mut Ui, item: Option<usize>, k:
|
|||
}
|
||||
}
|
||||
Value::Number(n) => {
|
||||
let x = n.as_f64().unwrap_or(0.0);
|
||||
if let Some(nx) = widgets::plain_field(ui, ("fx", item, k, key.as_str()), x, "", 2, 70.0) {
|
||||
let (x, id) = (n.as_f64().unwrap_or(0.0), ("fx", item, k, key.as_str()));
|
||||
let nx = if vectorcraft_effects::is_length(&e.id, key, relative) {
|
||||
widgets::num_field(ui, id, Some(x), unit, 70.0)
|
||||
} else {
|
||||
widgets::plain_field(ui, id, x, "", 2, 70.0)
|
||||
};
|
||||
if let Some(nx) = nx {
|
||||
change = Some((key.clone(), json!(nx)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use std::sync::{Mutex, OnceLock};
|
|||
|
||||
use egui::{Key, Ui, vec2};
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{CharStyle, NodeId, NodeKind, Unit};
|
||||
use vectorcraft_doc::{CharStyle, NodeId, NodeKind};
|
||||
use vectorcraft_tools::{Mods, ToolKey};
|
||||
|
||||
use super::{first_selected, pstate, set_pstate};
|
||||
|
|
@ -253,15 +253,16 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
}
|
||||
ui.add_space(2.0);
|
||||
let fw = ((w - 66.0) / 2.0).clamp(60.0, 100.0);
|
||||
let unit = app.session.type_unit();
|
||||
egui::Grid::new("ch-grid").num_columns(2).spacing([6.0, 4.0]).show(ui, |ui| {
|
||||
cell(ui, "T", "Font Size", |ui| {
|
||||
if let Some(v) = widgets::spin_field(ui, "ch-size", Some(s.size), Unit::Points, fw, 1.0, 0.1, &SIZE_PRESETS) {
|
||||
if let Some(v) = widgets::spin_field(ui, "ch-size", Some(s.size), unit, fw, 1.0, 0.1, &SIZE_PRESETS) {
|
||||
style(app, json!({"size": v}));
|
||||
}
|
||||
});
|
||||
cell(ui, "A↕", "Leading", |ui| {
|
||||
let presets: Vec<f64> = SIZE_PRESETS.iter().map(|v| v * 1.2).collect();
|
||||
if let Some(v) = widgets::spin_field(ui, "ch-lead", Some(s.effective_leading()), Unit::Points, fw, 1.0, 0.1, &presets) {
|
||||
if let Some(v) = widgets::spin_field(ui, "ch-lead", Some(s.effective_leading()), unit, fw, 1.0, 0.1, &presets) {
|
||||
style(app, json!({"leading": v}));
|
||||
}
|
||||
});
|
||||
|
|
@ -290,7 +291,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
});
|
||||
ui.end_row();
|
||||
cell(ui, "Aª", "Baseline Shift", |ui| {
|
||||
if let Some(v) = widgets::spin_field(ui, "ch-bs", Some(s.baseline_shift), Unit::Points, fw, 1.0, -1296.0, &[]) {
|
||||
if let Some(v) = widgets::spin_field(ui, "ch-bs", Some(s.baseline_shift), unit, fw, 1.0, -1296.0, &[]) {
|
||||
format(app, json!({"baselineShift": v}));
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ fn paint_block(ui: &mut Ui, p: &Paint) {
|
|||
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let units = app.session.active().map(|d| d.doc.units).unwrap_or_default();
|
||||
let units = app.session.general_unit();
|
||||
let p = app.hover_doc.unwrap_or_default();
|
||||
let bounds = app.session.active().and_then(|st| st.doc.bounds_of(&st.selection.objects, false));
|
||||
let mono = |ui: &mut Ui, s: String| {
|
||||
|
|
@ -80,7 +80,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
widgets::divider(ui);
|
||||
let st = tx.first_style();
|
||||
mono(ui, format!("{} {}", st.font_family, st.font_style));
|
||||
mono(ui, format!("Size: {} Tracking: {:.0}", units.format(st.size), st.tracking));
|
||||
mono(ui, format!("Size: {} Tracking: {:.0}", app.session.type_unit().format(st.size), st.tracking));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,19 +13,31 @@ fn options(app: &mut VectorcraftApp) -> Value {
|
|||
app.session.execute("magicWand.options", &json!({})).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// A tolerance: a plain number with a suffix, or a stroke weight (in the Stroke unit).
|
||||
#[derive(Clone, Copy)]
|
||||
enum Tol {
|
||||
Plain(&'static str),
|
||||
Weight,
|
||||
}
|
||||
|
||||
/// One row: checkbox + tolerance field.
|
||||
fn row(app: &mut VectorcraftApp, ui: &mut Ui, o: &Value, label: &str, flag: &str, tol: Option<(&str, &str, f64)>) {
|
||||
fn row(app: &mut VectorcraftApp, ui: &mut Ui, o: &Value, label: &str, flag: &str, tol: Option<(&str, Tol, f64)>) {
|
||||
let on = o[flag].as_bool().unwrap_or(false);
|
||||
ui.horizontal(|ui| {
|
||||
ui.set_min_height(26.0);
|
||||
if widgets::check(ui, label, on, true) {
|
||||
app.run("magicWand.set", json!({ flag: !on })).ok();
|
||||
}
|
||||
if let Some((key, unit, max)) = tol {
|
||||
if let Some((key, kind, max)) = tol {
|
||||
let stroke_unit = app.session.stroke_unit();
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
ui.add_enabled_ui(on, |ui| {
|
||||
let v = o[key].as_f64().unwrap_or(0.0);
|
||||
if let Some(n) = widgets::spin_plain(ui, ("wand", key), v, unit, 0, 64.0, 1.0, 0.0, &[]) {
|
||||
let n = match kind {
|
||||
Tol::Plain(suffix) => widgets::spin_plain(ui, ("wand", key), v, suffix, 0, 64.0, 1.0, 0.0, &[]),
|
||||
Tol::Weight => widgets::spin_field(ui, ("wand", key), Some(v), stroke_unit, 64.0, 1.0, 0.0, &[]),
|
||||
};
|
||||
if let Some(n) = n {
|
||||
app.run("magicWand.set", json!({ key: n.clamp(0.0, max) })).ok();
|
||||
}
|
||||
});
|
||||
|
|
@ -37,15 +49,15 @@ fn row(app: &mut VectorcraftApp, ui: &mut Ui, o: &Value, label: &str, flag: &str
|
|||
|
||||
pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let o = options(app);
|
||||
row(app, ui, &o, "Fill Color", "fillColor", Some(("fillTolerance", "", 255.0)));
|
||||
row(app, ui, &o, "Fill Color", "fillColor", Some(("fillTolerance", Tol::Plain(""), 255.0)));
|
||||
if !pstate::<bool>(ui.ctx(), "wand-hide-stroke") {
|
||||
widgets::divider(ui);
|
||||
row(app, ui, &o, "Stroke Color", "strokeColor", Some(("strokeTolerance", "", 255.0)));
|
||||
row(app, ui, &o, "Stroke Weight", "strokeWeight", Some(("weightTolerance", " pt", 1000.0)));
|
||||
row(app, ui, &o, "Stroke Color", "strokeColor", Some(("strokeTolerance", Tol::Plain(""), 255.0)));
|
||||
row(app, ui, &o, "Stroke Weight", "strokeWeight", Some(("weightTolerance", Tol::Weight, 1000.0)));
|
||||
}
|
||||
if !pstate::<bool>(ui.ctx(), "wand-hide-transparency") {
|
||||
widgets::divider(ui);
|
||||
row(app, ui, &o, "Opacity", "opacity", Some(("opacityTolerance", "%", 100.0)));
|
||||
row(app, ui, &o, "Opacity", "opacity", Some(("opacityTolerance", Tol::Plain("%"), 100.0)));
|
||||
row(app, ui, &o, "Blending Mode", "blendingMode", None);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -454,3 +454,5 @@ mod tests_strokedepth;
|
|||
mod tests_strokeux;
|
||||
#[cfg(test)]
|
||||
mod tests_style_libraries;
|
||||
#[cfg(test)]
|
||||
mod tests_units;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use egui::{Ui, vec2};
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{Justify, Unit};
|
||||
use vectorcraft_doc::Justify;
|
||||
|
||||
use super::character::text_style;
|
||||
use super::{pstate, set_pstate};
|
||||
|
|
@ -49,32 +49,34 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
});
|
||||
ui.add_space(4.0);
|
||||
let fw = ((ui.available_width() - 66.0) / 2.0).clamp(60.0, 100.0);
|
||||
// Indents and paragraph spacing are distances (General); type sizes follow Units ▸ Type.
|
||||
let unit = app.session.general_unit();
|
||||
let label = |ui: &mut Ui, s: &str, tip: &str| {
|
||||
ui.add_sized(vec2(22.0, 24.0), egui::Label::new(egui::RichText::new(s).size(11.5).strong().color(t.text))).on_hover_text(tip);
|
||||
};
|
||||
egui::Grid::new("para-grid").num_columns(4).spacing([4.0, 4.0]).show(ui, |ui| {
|
||||
label(ui, "→|", "Left Indent");
|
||||
if let Some(v) = widgets::spin_field(ui, "pa-li", Some(para.left_indent), Unit::Points, fw, 1.0, -1296.0, &[]) {
|
||||
if let Some(v) = widgets::spin_field(ui, "pa-li", Some(para.left_indent), unit, fw, 1.0, -1296.0, &[]) {
|
||||
format(app, json!({"leftIndent": v}));
|
||||
}
|
||||
label(ui, "|←", "Right Indent");
|
||||
if let Some(v) = widgets::spin_field(ui, "pa-ri", Some(para.right_indent), Unit::Points, fw, 1.0, -1296.0, &[]) {
|
||||
if let Some(v) = widgets::spin_field(ui, "pa-ri", Some(para.right_indent), unit, fw, 1.0, -1296.0, &[]) {
|
||||
format(app, json!({"rightIndent": v}));
|
||||
}
|
||||
ui.end_row();
|
||||
label(ui, "1→", "First-line Left Indent");
|
||||
if let Some(v) = widgets::spin_field(ui, "pa-fi", Some(para.first_line_indent), Unit::Points, fw, 1.0, -1296.0, &[]) {
|
||||
if let Some(v) = widgets::spin_field(ui, "pa-fi", Some(para.first_line_indent), unit, fw, 1.0, -1296.0, &[]) {
|
||||
format(app, json!({"firstLineIndent": v}));
|
||||
}
|
||||
ui.label("");
|
||||
ui.label("");
|
||||
ui.end_row();
|
||||
label(ui, "↑¶", "Space Before Paragraph");
|
||||
if let Some(v) = widgets::spin_field(ui, "pa-sb", Some(para.space_before), Unit::Points, fw, 1.0, 0.0, &[]) {
|
||||
if let Some(v) = widgets::spin_field(ui, "pa-sb", Some(para.space_before), unit, fw, 1.0, 0.0, &[]) {
|
||||
format(app, json!({"spaceBefore": v}));
|
||||
}
|
||||
label(ui, "↓¶", "Space After Paragraph");
|
||||
if let Some(v) = widgets::spin_field(ui, "pa-sa", Some(para.space_after), Unit::Points, fw, 1.0, 0.0, &[]) {
|
||||
if let Some(v) = widgets::spin_field(ui, "pa-sa", Some(para.space_after), unit, fw, 1.0, 0.0, &[]) {
|
||||
format(app, json!({"spaceAfter": v}));
|
||||
}
|
||||
ui.end_row();
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ const COPIES: [(&str, u32); 4] = [("3 x 3", 3), ("5 x 5", 5), ("7 x 7", 7), ("9
|
|||
fn editing(app: &VectorcraftApp) -> Option<(PatternDef, Unit)> {
|
||||
let st = app.session.active()?;
|
||||
let pe = st.doc.pattern_edit.as_ref()?;
|
||||
Some((st.doc.pattern(&pe.pattern)?.clone(), st.doc.units))
|
||||
Some((st.doc.pattern(&pe.pattern)?.clone(), app.session.general_unit()))
|
||||
}
|
||||
|
||||
fn row(ui: &mut Ui, label: &str, add: impl FnOnce(&mut Ui)) {
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ fn multi_color(app: &mut VectorcraftApp, ctx: &egui::Context) -> bool {
|
|||
}
|
||||
|
||||
fn document_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let units = app.session.active().map(|d| d.doc.units).unwrap_or_default();
|
||||
let units = app.session.general_unit();
|
||||
section_header(ui, "Document");
|
||||
ui.horizontal(|ui| {
|
||||
dim_label(ui, "Units");
|
||||
|
|
@ -207,7 +207,7 @@ fn document_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
|
||||
pub fn transform_section(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let Some(st) = app.session.active() else { return };
|
||||
let units = st.doc.units;
|
||||
let units = app.session.general_unit();
|
||||
let Some(b) = app.session.transform_bounds(&st.selection.objects) else {
|
||||
dim_label(ui, "No Selection");
|
||||
return;
|
||||
|
|
@ -337,11 +337,12 @@ pub fn type_sections(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
}
|
||||
ui.horizontal(|ui| {
|
||||
dim_label(ui, "Size");
|
||||
if let Some(v) = widgets::num_field(ui, "fsize", Some(s.size), Unit::Points, 70.0) {
|
||||
let type_unit = app.session.type_unit();
|
||||
if let Some(v) = widgets::num_field(ui, "fsize", Some(s.size), type_unit, 70.0) {
|
||||
app.run("text.setStyle", json!({"size": v})).ok();
|
||||
}
|
||||
dim_label(ui, "Leading");
|
||||
if let Some(v) = widgets::num_field(ui, "lead", Some(s.effective_leading()), Unit::Points, 70.0) {
|
||||
if let Some(v) = widgets::num_field(ui, "lead", Some(s.effective_leading()), type_unit, 70.0) {
|
||||
app.run("text.setStyle", json!({"leading": v})).ok();
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
use egui::{Sense, Stroke, Ui, vec2};
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{NodeKind, TabAlign, TabStop, Unit};
|
||||
use vectorcraft_doc::{NodeKind, TabAlign, TabStop};
|
||||
|
||||
use super::{first_selected, pstate, set_pstate};
|
||||
use crate::VectorcraftApp;
|
||||
|
|
@ -85,7 +85,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
egui::Grid::new("tabs-grid").num_columns(2).spacing([6.0, 4.0]).show(ui, |ui| {
|
||||
ui.label(egui::RichText::new("X:").color(t.text));
|
||||
let x = stops.get(sel).map(|s| s.position);
|
||||
if let Some(v) = widgets::spin_field(ui, "tabs-x", x, Unit::Points, 90.0, 1.0, 0.0, &[])
|
||||
if let Some(v) = widgets::spin_field(ui, "tabs-x", x, app.session.general_unit(), 90.0, 1.0, 0.0, &[])
|
||||
&& let Some(s) = stops.get_mut(sel)
|
||||
{
|
||||
s.position = v.max(0.0);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,128 @@
|
|||
//! Headless frames of Preferences ▸ Units: the General unit in the Transform panel, Properties,
|
||||
//! the Control bar, the Info panel, dialogs, the Preferences dialog itself and New Document; the
|
||||
//! Type unit in the Character panel; the change showing in the next frame.
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_doc::Unit;
|
||||
|
||||
use super::tests_appearance::{app_with_rect, frame_events, run};
|
||||
use super::*;
|
||||
|
||||
/// Every text drawn in the second headless frame of `f` (windows size themselves in the first).
|
||||
fn texts(app: &mut VectorcraftApp, mut f: impl FnMut(&mut VectorcraftApp, &mut Ui)) -> Vec<String> {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
frame_events(&ctx, app, vec![], &mut f);
|
||||
frame_events(&ctx, app, vec![], f).into_iter().map(|(s, _)| s).collect()
|
||||
}
|
||||
|
||||
/// Every text drawn by the open dialog in one headless frame.
|
||||
fn dialog_texts(app: &mut VectorcraftApp) -> Vec<String> {
|
||||
texts(app, |app, ui| crate::dialogs::show(app, ui.ctx()))
|
||||
}
|
||||
|
||||
fn shows(t: &[String], s: &str) -> bool {
|
||||
t.iter().any(|x| x == s)
|
||||
}
|
||||
|
||||
/// The 100 pt square of [`app_with_rect`] in millimetres.
|
||||
fn side_mm() -> String {
|
||||
Unit::Millimeters.format(100.0)
|
||||
}
|
||||
|
||||
fn general_mm(app: &mut VectorcraftApp) {
|
||||
app.run("prefs.set", json!({"key": "unitsGeneral", "value": "Millimeters"})).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn panels_show_millimetres_after_the_preference_changes() {
|
||||
let mut app = app_with_rect();
|
||||
assert_eq!(side_mm(), "35.278 mm");
|
||||
assert!(shows(&texts(&mut app, transform::show), "100 pt"), "points first");
|
||||
general_mm(&mut app);
|
||||
// The next frame: no restart, nothing cached.
|
||||
let t = texts(&mut app, transform::show);
|
||||
assert!(shows(&t, &side_mm()) && !shows(&t, "100 pt"), "Transform panel: {t:?}");
|
||||
assert!(shows(&texts(&mut app, properties::show), &side_mm()), "Properties panel");
|
||||
assert!(shows(&texts(&mut app, crate::chrome::control_bar), &side_mm()), "Control bar");
|
||||
assert!(shows(&texts(&mut app, info::show), &format!("W: {}", side_mm())), "Info panel");
|
||||
// Document Setup back to points: everything follows the document.
|
||||
run(&mut app, "document.setUnits", json!({"units": "Points"}));
|
||||
assert!(shows(&texts(&mut app, transform::show), "100 pt"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dialog_lengths_show_in_the_general_unit() {
|
||||
let mut app = app_with_rect();
|
||||
general_mm(&mut app);
|
||||
crate::menus::invoke(&mut app, "object.move", json!({}));
|
||||
let t = dialog_texts(&mut app);
|
||||
assert!(shows(&t, "0 mm"), "Move: {t:?}");
|
||||
// A typed unit wins; the move is in points.
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("dx".into(), json!("10 mm"));
|
||||
assert!(shows(&dialog_texts(&mut app), "10 mm"));
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
let st = app.session.active().unwrap();
|
||||
let b = st.doc.bounds_of(&st.selection.objects, false).unwrap();
|
||||
assert!((b.x0 - (10.0 + 10.0 * 72.0 / 25.4)).abs() < 1e-6, "{b:?}");
|
||||
// A shape tool's size dialog and an effect's distances.
|
||||
crate::dialogs::open_tool_dialog(&mut app, "rectangle", json!({"x": 0, "y": 0}));
|
||||
assert!(shows(&dialog_texts(&mut app), &side_mm()), "Rectangle");
|
||||
app.run("effect.dialog", json!({"effect": "stylize.dropShadow"})).unwrap();
|
||||
let x = vectorcraft_effects::default_params("stylize.dropShadow").unwrap()["x"].as_f64().unwrap();
|
||||
let t = dialog_texts(&mut app);
|
||||
assert!(shows(&t, &Unit::Millimeters.format(x)), "Drop Shadow: {t:?}");
|
||||
crate::dialogs::cancel(&mut app);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn type_sizes_follow_units_type() {
|
||||
let mut app = app_with_rect();
|
||||
let id = run(&mut app, "text.create", json!({"x": 10, "y": 150, "text": "Hi", "size": 24}))["id"].clone();
|
||||
run(&mut app, "select.set", json!({ "ids": [id] }));
|
||||
general_mm(&mut app);
|
||||
assert!(shows(&texts(&mut app, character::show), "24 pt"), "General leaves type alone");
|
||||
app.run("prefs.set", json!({"key": "unitsType", "value": "inches"})).unwrap();
|
||||
let t = texts(&mut app, character::show);
|
||||
assert!(shows(&t, &Unit::Inches.format(24.0)), "Character panel: {t:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_preferences_dialog_shows_the_documents_units() {
|
||||
let mut app = app_with_rect();
|
||||
run(&mut app, "document.setUnits", json!({"units": "Millimeters"}));
|
||||
crate::prefs_dialog::open(&mut app, Some("General"));
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["unitsGeneral"], json!("millimeters"));
|
||||
let inc = app.session.prefs.keyboard_increment;
|
||||
let frame = |app: &mut VectorcraftApp| texts(app, |app, ui| crate::prefs_dialog::show(app, ui.ctx()));
|
||||
assert!(shows(&frame(&mut app), &Unit::Millimeters.format(inc)), "Keyboard Increment in mm");
|
||||
// Choosing another General unit in the dialog shows at once, and OK applies it.
|
||||
app.ui.dialog.as_mut().unwrap().fields.insert("unitsGeneral".into(), json!("inches"));
|
||||
assert!(shows(&frame(&mut app), &Unit::Inches.format(inc)));
|
||||
crate::prefs_dialog::confirm(&mut app).unwrap();
|
||||
assert_eq!(app.session.active().unwrap().doc.units, Unit::Inches);
|
||||
assert_eq!(app.session.prefs.units_general, "inches");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_documents_start_in_the_general_preference() {
|
||||
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default());
|
||||
general_mm(&mut app);
|
||||
app.run("file.newDialog", json!({})).unwrap();
|
||||
assert_eq!(app.ui.dialog.as_ref().unwrap().fields["units"], json!("Millimeters"));
|
||||
let t = dialog_texts(&mut app);
|
||||
assert!(shows(&t, &Unit::Millimeters.format(612.0)), "Letter in mm: {t:?}");
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
let d = &app.session.active().unwrap().doc;
|
||||
assert_eq!((d.units, d.artboards[0].rect.width()), (Unit::Millimeters, 612.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ruler_labels_have_the_steps_decimals() {
|
||||
use crate::canvas::ruler_label;
|
||||
assert_eq!(ruler_label(100.0000001, 100.0), "100");
|
||||
assert_eq!(ruler_label(0.30000000004, 0.1), "0.3");
|
||||
assert_eq!(ruler_label(1.25, 0.05), "1.25");
|
||||
assert_eq!(ruler_label(-0.0000001, 0.5), "0");
|
||||
assert_eq!(ruler_label(-2.5, 0.5), "-2.5");
|
||||
}
|
||||
|
|
@ -27,7 +27,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
widgets::dim_label(ui, "No document");
|
||||
return;
|
||||
};
|
||||
let units = st.doc.units;
|
||||
let units = app.session.general_unit();
|
||||
let bounds = app.session.transform_bounds(&st.selection.objects);
|
||||
let refi: usize = ui.data(|d| d.get_temp(egui::Id::new("refpt"))).unwrap_or(4);
|
||||
let link: bool = pstate(ui.ctx(), "xf-link");
|
||||
|
|
@ -41,20 +41,20 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
|||
ui.add_enabled_ui(has, |ui| {
|
||||
egui::Grid::new("xfp-grid").num_columns(4).spacing([4.0, 6.0]).min_col_width(0.0).show(ui, |ui| {
|
||||
widgets::dim_label(ui, "X:");
|
||||
if let Some(v) = widgets::num_field(ui, "xfp-x", rp.map(|p| p.x), units, 74.0) {
|
||||
if let Some(v) = widgets::num_field(ui, "xfp-x", rp.map(|p| p.x), units, 80.0) {
|
||||
app.run("object.setBounds", json!({"x": v, "reference": refi})).ok();
|
||||
}
|
||||
widgets::dim_label(ui, "W:");
|
||||
if let Some(v) = widgets::num_field(ui, "xfp-w", bounds.map(|b| b.width()), units, 74.0) {
|
||||
if let Some(v) = widgets::num_field(ui, "xfp-w", bounds.map(|b| b.width()), units, 80.0) {
|
||||
app.run("object.setBounds", json!({"width": v, "reference": refi, "proportional": link})).ok();
|
||||
}
|
||||
ui.end_row();
|
||||
widgets::dim_label(ui, "Y:");
|
||||
if let Some(v) = widgets::num_field(ui, "xfp-y", rp.map(|p| p.y), units, 74.0) {
|
||||
if let Some(v) = widgets::num_field(ui, "xfp-y", rp.map(|p| p.y), units, 80.0) {
|
||||
app.run("object.setBounds", json!({"y": v, "reference": refi})).ok();
|
||||
}
|
||||
widgets::dim_label(ui, "H:");
|
||||
if let Some(v) = widgets::num_field(ui, "xfp-h", bounds.map(|b| b.height()), units, 74.0) {
|
||||
if let Some(v) = widgets::num_field(ui, "xfp-h", bounds.map(|b| b.height()), units, 80.0) {
|
||||
app.run("object.setBounds", json!({"height": v, "reference": refi, "proportional": link})).ok();
|
||||
}
|
||||
ui.end_row();
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ pub fn open(app: &mut VectorcraftApp, category: Option<&str>) {
|
|||
// One field per preference key or group (the flattener presets have commands of their own).
|
||||
let mut fields: Map<String, Value> = app.session.prefs.to_json().as_object().cloned().unwrap_or_default();
|
||||
fields.retain(|k, _| vectorcraft_engine::cmd::prefscmds::spec(k).is_some() || PREF_GROUPS.contains(&k.as_str()));
|
||||
// Units ▸ General shows (and OK sets) the open document's units.
|
||||
if let Some(st) = app.session.active() {
|
||||
fields.insert("unitsGeneral".into(), json!(st.doc.units.key()));
|
||||
}
|
||||
let cat = category.and_then(|c| PREF_CATEGORIES.iter().find(|x| x.eq_ignore_ascii_case(c))).copied().unwrap_or(PREF_CATEGORIES[0]);
|
||||
fields.insert("__category".into(), json!(cat));
|
||||
fields.insert("__smartGuides".into(), json!(app.ui.view.smart_guides));
|
||||
|
|
@ -234,6 +238,16 @@ fn category_fields(ui: &mut egui::Ui, d: &mut Dialog, cat: &str) {
|
|||
}
|
||||
});
|
||||
}
|
||||
PrefKind::Length { min, max, measure } => {
|
||||
// In the dialog's own Units choice, so a change there shows right away.
|
||||
let unit = vectorcraft_doc::Unit::named(&d.str(measure.pref_key())).unwrap_or_default();
|
||||
let x = d.f64(sp.key, min);
|
||||
labeled(ui, sp.label, |ui| {
|
||||
if let Some(x) = widgets::num_field(ui, sp.key, Some(x), unit, 110.0) {
|
||||
d.fields.insert(sp.key.into(), json!(x.clamp(min, max)));
|
||||
}
|
||||
});
|
||||
}
|
||||
PrefKind::Int { min, max } => {
|
||||
labeled(ui, sp.label, |ui| {
|
||||
let mut x = v.as_i64().unwrap_or(min);
|
||||
|
|
|
|||
|
|
@ -978,7 +978,7 @@ pub fn menu_item(ui: &mut Ui, label: &str, enabled: bool, checked: bool) -> bool
|
|||
pub fn opt_field(ui: &mut Ui, id: impl std::hash::Hash + std::fmt::Debug, value: Option<f64>, unit: Unit, width: f32) -> Option<Option<f64>> {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let id = ui.id().with(id);
|
||||
let shown = value.map(|v| unit.format(v).trim_end_matches(unit.suffix()).trim_end().to_string()).unwrap_or_default();
|
||||
let shown = value.map(|v| unit.number(v)).unwrap_or_default();
|
||||
let editing = ui.memory(|m| m.has_focus(id));
|
||||
let mut buf: String = if editing { ui.data_mut(|d| d.get_temp::<String>(id)).unwrap_or_else(|| shown.clone()) } else { shown.clone() };
|
||||
let enabled = ui.is_enabled();
|
||||
|
|
|
|||
|
|
@ -196,3 +196,10 @@ Width Point Edit: double-clicking a width point with the Width tool, or `ui.widt
|
|||
`widthPoint` dialog (fields `id`, `index`, `t`, `side1` and `side2`: the left and right widths in points, `linked`,
|
||||
`adjustAdjoining`). `ui.dialog.confirm` runs `stroke.widthPoint.set` with them; `ui.dialog.set {field: "discard",
|
||||
value: true}` then confirm (the Delete button) removes the point with `stroke.widthPoint.remove`.
|
||||
|
||||
Units: dialog distance fields (Move's `dx`/`dy`, shape sizes, Offset Path's `offset`, Split Into Grid's `gutter`,
|
||||
Artboard Options' sizes, effects' distances, `transformEach`'s `moveH`/`moveV`, New Document's `width`/`height`, the
|
||||
length preferences in `preferences`) show in the General unit (the active document's units, or `unitsGeneral` without a
|
||||
document). Through `ui.dialog.set` a number is in points and a string may carry its unit (`"10 mm"`; a bare number in a
|
||||
string is points). The `preferences` dialog's `unitsGeneral` starts as the active document's units, and OK sets them.
|
||||
`newDocument` has `units` (a unit label; print presets start in `unitsGeneral`, screen presets in Pixels).
|
||||
|
|
|
|||
19
docs/mcp.md
19
docs/mcp.md
|
|
@ -752,3 +752,22 @@ report warn about it).
|
|||
```json
|
||||
{"name":"run_command","arguments":{"command":"paint.editGradient","params":{"stroke":true,"strokeMode":"along"}}}
|
||||
```
|
||||
|
||||
## Units
|
||||
|
||||
Commands take and return lengths in points (the distances of `object.path.offsetPath`, `object.path.simplify`,
|
||||
`object.path.splitIntoGrid` and `text.setStyle` `size`/`leading` also take a string with a unit, `"5 mm"`). The units
|
||||
only change what the UI shows and how it reads typed numbers. Preferences ▸ Units has
|
||||
three: General (rulers, positions and sizes, the Info panel, dialog distances, canvas measurement labels), Stroke
|
||||
(weights, dashes) and Type (font size, leading, baseline shift). General is the active document's units
|
||||
(`document.inspect` → `units`): `document.setUnits {units}` (Document Setup) sets it for that document, and
|
||||
`prefs.set {key: "unitsGeneral", value}` sets it for the active document too (one undo step) and is the units
|
||||
`file.new` starts in when it gets no `units`. Stroke and Type are the preferences `unitsStroke` and `unitsType`.
|
||||
`prefs.list` marks the length preferences (`keyboardIncrement`, `cornerRadius`, `pasteOffset`, `gridlineEvery`,
|
||||
`typeSizeIncrement`, `baselineShiftIncrement`) with `measure: "general"|"type"`; they are kept in points and also take
|
||||
a string with a unit. Unit names: `Points`, `Picas`, `Inches`, `Millimeters`, `Centimeters`, `Pixels`, `Feet & Inches`,
|
||||
`Meters`, `Yards`, `Feet` (the preferences use `points`, `picas`, … `feetInches`, `meters`, `yards`, `feet`).
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"prefs.set","params":{"key":"unitsGeneral","value":"millimeters"}}}
|
||||
```
|
||||
|
|
|
|||
Loading…
Reference in New Issue