From 60439d88b96b28a7fa4bb56d6807f9241f8a9f31 Mon Sep 17 00:00:00 2001 From: raytheengineer <2438856+raytheengineer@users.noreply.github.com> Date: Wed, 7 Oct 2026 22:19:20 -0500 Subject: [PATCH] Stroke: per-unit weight presets so mm/cm/in dropdowns show clean numbers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Stroke panel weight dropdown converted one pt-based preset list into whatever Units > Stroke was set to, which produced ugly rounded values like 0.088 mm, 0.265 mm, 10.583 mm from the pt ladder. Each unit now has its own native ladder (observed from Illustrator's running dropdowns and typed in), converted to pt once at use. Points, Millimeters, Centimeters, Inches, Pixels and Picas each get Illustrator's actual values; the exotic Unit variants (Feet & Inches, Meters, Yards, Feet) fall back to the pt ladder. Picas round-trip to exact pt weights (1 pt, 12 pt, 60 pt …) but display as decimal picas until Unit::number learns 0p1 / 1p3 notation; that is a system-wide formatter change left for a separate PR. Also bumps Unit::number precision from 3 to 4 decimals so Illustrator's sub-eighth-inch strokes (0.0078, 0.0156, 0.0313, 0.0625 in) show their authored values instead of rounding to 0.008 / 0.016 / 0.031 / 0.063. Trailing zeros are still trimmed, so 1 pt stays "1". Co-Authored-By: Claude Opus 4.7 --- crates/doc/src/lib.rs | 4 +- crates/doc/src/tests_units.rs | 6 ++- crates/engine/src/tests_strokeux.rs | 2 +- crates/ui-egui/src/panels/stroke.rs | 60 +++++++++++++++++++-- crates/ui-egui/src/panels/tests_strokeux.rs | 32 +++++++++-- crates/ui-egui/src/panels/tests_units.rs | 2 +- 6 files changed, 94 insertions(+), 12 deletions(-) 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.