mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Stroke: per-unit weight presets so mm/cm/in dropdowns show clean numbers
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 <[email protected]>
This commit is contained in:
1 parent
2366000e19
commit
60439d88b9
6 files changed
+94
-12
No files matched your search
@@ -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() }
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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<f64> {
|
||||
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<f64>, 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}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> = 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:?}");
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user