diff --git a/crates/doc/src/lib.rs b/crates/doc/src/lib.rs index 96e7cace..640f72a1 100644 --- a/crates/doc/src/lib.rs +++ b/crates/doc/src/lib.rs @@ -191,7 +191,9 @@ impl Unit { } /// [`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)); + // Four decimals so inches shows `0.0078`, `0.0156`, `0.0313`, `0.0625` from Illustrator's + // sub-eighth-inch stroke ladder; trailing zeros are trimmed so `1 pt` stays `1`. + let s = format!("{:.4}", self.from_pt(pt)); let s = s.trim_end_matches('0').trim_end_matches('.'); if s == "-0" { "0".into() } else { s.into() } } diff --git a/crates/doc/src/tests_units.rs b/crates/doc/src/tests_units.rs index 25692284..ed6637fc 100644 --- a/crates/doc/src/tests_units.rs +++ b/crates/doc/src/tests_units.rs @@ -60,6 +60,8 @@ 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"); + assert_eq!(Unit::Millimeters.number(1.0), "0.3528"); + // Inches reach four decimals for Illustrator's sub-eighth-inch stroke ladder. + assert_eq!(Unit::Inches.number(0.5616), "0.0078"); + assert_eq!(Unit::Points.number(-0.00001), "0"); } diff --git a/crates/engine/src/tests_strokeux.rs b/crates/engine/src/tests_strokeux.rs index 16f81617..7dd73cbe 100644 --- a/crates/engine/src/tests_strokeux.rs +++ b/crates/engine/src/tests_strokeux.rs @@ -73,7 +73,7 @@ fn stroke_unit_follows_the_preference() { assert_eq!(s.stroke_unit(), Unit::Points); s.prefs.units_stroke = "millimeters".into(); assert_eq!(s.stroke_unit(), Unit::Millimeters); - assert_eq!(s.stroke_unit().format(1.0), "0.353 mm"); + assert_eq!(s.stroke_unit().format(1.0), "0.3528 mm"); } #[test] diff --git a/crates/ui-egui/src/panels/stroke.rs b/crates/ui-egui/src/panels/stroke.rs index 1fb4d3b3..70deaacf 100644 --- a/crates/ui-egui/src/panels/stroke.rs +++ b/crates/ui-egui/src/panels/stroke.rs @@ -4,7 +4,7 @@ use egui::{Color32, Rect, Sense, Stroke, StrokeKind, Ui, pos2, vec2}; use serde_json::{Value, json}; use vectorcraft_color::{Color, Paint}; -use vectorcraft_doc::{Appearance, ArrowAlign, Arrowhead, Dash, Document, LineCap, LineJoin, Node, StrokeAlign, StrokeLayer, WidthProfile}; +use vectorcraft_doc::{Appearance, ArrowAlign, Arrowhead, Dash, Document, LineCap, LineJoin, Node, StrokeAlign, StrokeLayer, Unit, WidthProfile}; use vectorcraft_engine::inspect::StrokeMixed; use super::{character, current_stroke, pstate, set_pstate, stroke_mixed}; @@ -12,8 +12,58 @@ use crate::theme::Tokens; use crate::widgets::{self, menu_item}; use crate::{VectorcraftApp, icons}; -pub const WEIGHT_PRESETS: [f64; 22] = - [0.25, 0.5, 0.75, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0, 100.0]; +/// Stroke weight dropdown presets per unit, in that unit's native values; the dropdown converts +/// each to points for the model. One pt list converted into every unit produced junk like +/// `0.353 mm` for a 1-pt stroke; each unit now has its own ladder so the numbers read cleanly. +pub fn weight_presets(unit: Unit) -> Vec { + let native: &[f64] = match unit { + Unit::Points => &PT_PRESETS, + Unit::Millimeters => &MM_PRESETS, + Unit::Centimeters => &CM_PRESETS, + Unit::Inches => &IN_PRESETS, + Unit::Pixels => &PX_PRESETS, + Unit::Picas => &PC_PRESETS, + // Feet & Inches, Meters, Yards, Feet keep the pt ladder as a reasonable fallback. + _ => &PT_PRESETS, + }; + native.iter().map(|v| unit.to_pt(*v)).collect() +} + +const PT_PRESETS: [f64; 22] = + [0.25, 0.5, 0.75, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 12.0, 14.0, 16.0, 18.0, 20.0, 40.0, 60.0, 80.0, 100.0]; +const MM_PRESETS: [f64; 22] = [0.1, 0.25, 0.35, 0.5, 0.75, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 15.0, 20.0, 25.0, 30.0]; +const CM_PRESETS: [f64; 22] = [0.01, 0.02, 0.03, 0.05, 0.06, 0.07, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0]; +const IN_PRESETS: [f64; 22] = + [0.0078, 0.0156, 0.0313, 0.0625, 0.125, 0.25, 0.325, 0.5, 0.625, 0.75, 0.875, 1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0]; +const PX_PRESETS: [f64; 22] = + [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 30.0, 40.0]; +/// Picas authored as `points / 12` so each entry round-trips to an exact pt weight (Illustrator's +/// pica ladder is 0p1 … 5p, i.e. 1 pt … 60 pt in 1-pt / 3-pt / 12-pt steps). The dropdown labels +/// read as decimal picas (`0.083 p`) until `Unit::number` learns `ApB` notation. +const PC_PRESETS: [f64; 22] = [ + 1.0 / 12.0, + 2.0 / 12.0, + 3.0 / 12.0, + 4.0 / 12.0, + 5.0 / 12.0, + 6.0 / 12.0, + 7.0 / 12.0, + 8.0 / 12.0, + 9.0 / 12.0, + 10.0 / 12.0, + 11.0 / 12.0, + 1.0, + 15.0 / 12.0, + 18.0 / 12.0, + 21.0 / 12.0, + 2.0, + 27.0 / 12.0, + 30.0 / 12.0, + 33.0 / 12.0, + 3.0, + 4.0, + 5.0, +]; /// A profile's points as its silhouette: none (the plain bar) for the uniform stroke. fn silhouette(points: &[(f64, f64, f64)]) -> Option<&[(f64, f64, f64)]> { @@ -87,7 +137,9 @@ pub(crate) fn shown_weight(app: &VectorcraftApp, st: Option<&StrokeLayer>, mixed /// The weight spinner (Stroke panel, Control bar): Units > Stroke, presets, blank when mixed. pub(crate) fn weight_field(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, weight: Option, width: f32) { - if let Some(w) = widgets::spin_field(ui, id, weight, app.session.stroke_unit(), width, 1.0, 0.0, &WEIGHT_PRESETS) { + let unit = app.session.stroke_unit(); + let presets = weight_presets(unit); + if let Some(w) = widgets::spin_field(ui, id, weight, unit, width, 1.0, 0.0, &presets) { set(app, json!({"weight": w})); } } diff --git a/crates/ui-egui/src/panels/tests_strokeux.rs b/crates/ui-egui/src/panels/tests_strokeux.rs index a6ecc918..e2dfd7ea 100644 --- a/crates/ui-egui/src/panels/tests_strokeux.rs +++ b/crates/ui-egui/src/panels/tests_strokeux.rs @@ -32,10 +32,10 @@ fn weights_and_dashes_show_in_the_stroke_unit_everywhere() { app.session.prefs.units_stroke = "millimeters".into(); run(&mut app, "stroke.set", json!({"dash": [12, 6]})); let mm = vectorcraft_doc::Unit::Millimeters.format(1.0); - assert_eq!(mm, "0.353 mm"); + assert_eq!(mm, "0.3528 mm"); let t = texts(&ctx, &mut app, stroke::show); assert!(shows(&t, &mm), "Stroke panel: {t:?}"); - assert!(shows(&t, "4.233") && shows(&t, "2.117"), "dash and gap in mm: {t:?}"); + assert!(shows(&t, "4.2333") && shows(&t, "2.1167"), "dash and gap in mm: {t:?}"); assert!(shows(&texts(&ctx, &mut app, properties::show), &mm), "Properties panel"); assert!(shows(&texts(&ctx, &mut app, crate::chrome::control_bar), &mm), "Control bar"); } @@ -75,7 +75,8 @@ fn inside_and_outside_are_off_for_open_paths_and_type() { #[test] fn the_weight_presets_run_from_a_quarter_point_to_a_hundred() { - assert!(stroke::WEIGHT_PRESETS.contains(&0.25) && stroke::WEIGHT_PRESETS.contains(&100.0)); + let pt = stroke::weight_presets(vectorcraft_doc::Unit::Points); + assert!(pt.contains(&0.25) && pt.contains(&100.0)); let ctx = egui::Context::default(); let mut app = app_with_rect(); // The chevron at the right end of the 120 px weight spinner opens them. @@ -86,3 +87,28 @@ fn the_weight_presets_run_from_a_quarter_point_to_a_hundred() { let t = texts(&ctx, &mut app, stroke::show); assert!(shows(&t, "0.25 pt") && shows(&t, "100 pt"), "{t:?}"); } + +#[test] +fn weight_presets_display_cleanly_in_their_native_unit() { + use vectorcraft_doc::Unit; + // Round-tripping a native-unit value through to_pt → from_pt → number() must give the same + // string the ladder was authored with: no "0.353 mm" surprises in the dropdown. + for (unit, want) in [ + (Unit::Millimeters, ["0.1", "0.25", "0.35", "0.5", "0.75", "1", "30"].as_slice()), + (Unit::Centimeters, ["0.01", "0.05", "0.1", "0.5", "1", "5"].as_slice()), + (Unit::Pixels, ["1", "10", "20", "40"].as_slice()), + (Unit::Points, ["0.25", "0.5", "1", "12", "100"].as_slice()), + ] { + let presets = stroke::weight_presets(unit); + let shown: Vec = presets.iter().map(|pt| unit.number(*pt)).collect(); + for w in want { + assert!(shown.iter().any(|s| s == w), "{unit:?} preset {w:?} missing from {shown:?}"); + } + } + // Picas round-trip to exact pt values (Illustrator's 0p1=1pt, 1p=12pt, 5p=60pt): display + // formatting is a separate concern, but the underlying weight must be right. + let pc = stroke::weight_presets(Unit::Picas); + for want_pt in [1.0, 12.0, 60.0] { + assert!(pc.iter().any(|pt| (pt - want_pt).abs() < 1e-9), "pica preset {want_pt} pt missing from {pc:?}"); + } +} diff --git a/crates/ui-egui/src/panels/tests_units.rs b/crates/ui-egui/src/panels/tests_units.rs index 2e70e487..06ca2063 100644 --- a/crates/ui-egui/src/panels/tests_units.rs +++ b/crates/ui-egui/src/panels/tests_units.rs @@ -37,7 +37,7 @@ fn general_mm(app: &mut VectorcraftApp) { #[test] fn panels_show_millimetres_after_the_preference_changes() { let mut app = app_with_rect(); - assert_eq!(side_mm(), "35.278 mm"); + assert_eq!(side_mm(), "35.2778 mm"); assert!(shows(&texts(&mut app, transform::show), "100 pt"), "points first"); general_mm(&mut app); // The next frame: no restart, nothing cached.