mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 09:20:56 -06:00
Merge remote-tracking branch 'origin/main' into fix/ruler-double-click-guide
This commit is contained in:
@@ -544,7 +544,11 @@ fn merge(chain: &[&Piece]) -> std::result::Result<TextObject, usize> {
|
||||
for (k, w) in off.windows(2).enumerate() {
|
||||
let (delta, Some(&(s, e))) = (w[1] - w[0], before.get(k)) else { continue };
|
||||
if delta.abs() > 1e-9 {
|
||||
edit::style_range(&mut m.runs, s, e, |st| st.tracking += delta / st.size.max(1e-6) * 1000.0);
|
||||
// Thousandths of the em as the Horizontal Scale draws it (`spacing_scale`).
|
||||
let vertical = m.vertical;
|
||||
edit::style_range(&mut m.runs, s, e, |st| {
|
||||
st.tracking += delta / (st.size * vectorcraft_text::spacing_scale(st, vertical)).max(1e-6) * 1000.0;
|
||||
});
|
||||
if !TRACKING.contains(&edit::style_at(&m.runs, s).tracking) {
|
||||
return Err(k + 1);
|
||||
}
|
||||
|
||||
@@ -176,6 +176,24 @@ fn combine_joins_only_pieces_on_one_line_turned_alike() {
|
||||
assert!(other(s.execute("addon.cleanup.combineText", &json!({}))).contains("combine"));
|
||||
}
|
||||
|
||||
/// Pieces in horizontally scaled type: the tracking that puts each back where it was is measured in
|
||||
/// the em as the Horizontal Scale draws it (`vectorcraft_text::spacing_scale`), as layout spaces it.
|
||||
#[test]
|
||||
fn combine_puts_horizontally_scaled_pieces_back_where_they_were() {
|
||||
let mut s = session();
|
||||
let a = text(&mut s, json!({"x": 50, "y": 100, "text": "Left", "size": 20}));
|
||||
let b = text(&mut s, json!({"x": 160, "y": 100, "text": "Right", "size": 20}));
|
||||
for id in [a, b] {
|
||||
s.execute("text.setRangeStyle", &json!({"id": id.0, "hScale": 200})).unwrap();
|
||||
}
|
||||
let was = glyphs(&s, &[a, b]);
|
||||
select(&mut s, &[a, b]);
|
||||
let r = s.execute("addon.cleanup.combineText", &json!({})).unwrap();
|
||||
assert_eq!(ids(&r), [a]);
|
||||
assert_eq!(obj(&s, a).plain_text(), "Left Right");
|
||||
assert_same_places(&was, &glyphs(&s, &[a]));
|
||||
}
|
||||
|
||||
/// Join Point Text makes one object of stacked one-line pieces: lines top to bottom as paragraphs,
|
||||
/// pieces on a line left to right, each line's baseline where it was (its leading is the distance
|
||||
/// from the line above), in one undo step. A piece turned unlike the top-left one stays.
|
||||
|
||||
@@ -1578,7 +1578,8 @@ fn reshape(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some(v) if v.is_array() => super::select::checked_refs(Some(v), "path.reshape")?.into_iter().collect(),
|
||||
Some(_) => return Err(bad("path.reshape", "`focal` must be an array of [subpath, anchor] pairs")),
|
||||
};
|
||||
if radius.is_some() && !focal.is_empty() {
|
||||
// An empty `focal` chooses the focal-point rule too.
|
||||
if radius.is_some() && !matches!(p.get("focal"), None | Some(Value::Null)) {
|
||||
return Err(bad("path.reshape", "`focal` and `radius` cannot be used together"));
|
||||
}
|
||||
// A step that names neither `focal` nor `radius` (as the Reshape tool recorded one before it
|
||||
|
||||
@@ -1087,12 +1087,16 @@ fn set_range_style(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
/// The em a laid-out glyph's kerning and tracking are measured in, in text space: the size it
|
||||
/// was shaped at (superscript, small caps and Shrink Text to Fit included).
|
||||
/// was shaped at (superscript, small caps and Shrink Text to Fit included), in horizontal type
|
||||
/// as its Horizontal Scale draws it ([`vectorcraft_text::spacing_scale`]).
|
||||
fn glyph_em(db: &vectorcraft_text::FontDb, t: &TextObject, g: &vectorcraft_text::PositionedGlyph) -> Option<f64> {
|
||||
let face = db.face_by_id(g.font_id)?;
|
||||
let h_scale = edit::slice_runs(&t.runs, g.byte, g.byte + g.len).first()?.style.h_scale / 100.0;
|
||||
let runs = edit::slice_runs(&t.runs, g.byte, g.byte + g.len);
|
||||
let style = &runs.first()?.style;
|
||||
// The glyph's own scale takes in the Horizontal Scale.
|
||||
let h_scale = style.h_scale / 100.0;
|
||||
let [a, b, ..] = g.xf.as_coeffs();
|
||||
Some(a.hypot(b) * face.units_per_em() / h_scale).filter(|em| em.is_finite() && *em > 0.0)
|
||||
Some(a.hypot(b) * face.units_per_em() / h_scale * vectorcraft_text::spacing_scale(style, t.vertical)).filter(|em| em.is_finite() && *em > 0.0)
|
||||
}
|
||||
|
||||
/// The current pair correction in thousandths of an em, including an automatic correction.
|
||||
|
||||
@@ -3622,9 +3622,12 @@ fn reshape_refuses_a_focal_point_that_is_not_an_anchor() {
|
||||
}
|
||||
assert_eq!(reshape_anchors(&s, id), before);
|
||||
let steps = s.doc().unwrap().history.undo.len();
|
||||
let e =
|
||||
s.execute("path.reshape", &json!({"id": id.0, "x": 420.0, "y": 0.0, "dx": 0.0, "dy": 10.0, "focal": [[0, 2]], "radius": 50.0})).unwrap_err();
|
||||
assert!(e.to_string().contains("`focal` and `radius` cannot be used together"), "{e}");
|
||||
// An empty `focal` chooses the focal-point rule too, so it cannot go with `radius` either.
|
||||
for focal in [json!([[0, 2]]), json!([])] {
|
||||
let e =
|
||||
s.execute("path.reshape", &json!({"id": id.0, "x": 420.0, "y": 0.0, "dx": 0.0, "dy": 10.0, "focal": focal, "radius": 50.0})).unwrap_err();
|
||||
assert!(e.to_string().contains("`focal` and `radius` cannot be used together"), "{focal}: {e}");
|
||||
}
|
||||
assert_eq!(reshape_anchors(&s, id), before);
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), steps);
|
||||
}
|
||||
|
||||
@@ -280,6 +280,37 @@ fn metrics_pair_step_preserves_the_drawn_auto_position() {
|
||||
assert!((moved - s.prefs.tracking_increment * 0.1).abs() < 0.01, "V moves by one kerning increment: {moved}");
|
||||
}
|
||||
|
||||
/// At 50% Horizontal Scale the font's pair reads as the same thousandths of an em as at 100%:
|
||||
/// kerning is measured in the em as the scale draws it (`vectorcraft_text::spacing_scale`), so the
|
||||
/// value read keeps V where the font's kerning draws it, and a step moves V half as far as at 100%.
|
||||
#[test]
|
||||
fn horizontally_scaled_pair_reads_as_the_kerning_that_keeps_it() {
|
||||
use vectorcraft_text::test_fonts::{PAIR_KERNING_FAMILY, pair_kerning_font};
|
||||
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
db.add_font(pair_kerning_font().unwrap());
|
||||
let mut s = session();
|
||||
let id = NodeId(
|
||||
s.execute("text.create", &json!({"x": 10, "y": 10, "text": "AV", "size": 100, "font": PAIR_KERNING_FAMILY})).unwrap()["id"].as_u64().unwrap(),
|
||||
);
|
||||
s.execute("text.setRangeStyle", &json!({"id": id.0, "start": 0, "end": 2, "hScale": 50})).unwrap();
|
||||
let before = obj(&s, id);
|
||||
assert_eq!(before.runs[0].style.h_scale, 50.0);
|
||||
let auto = vectorcraft_text::layout(db, &before);
|
||||
let drawn_v = |layout: &vectorcraft_text::TextLayout| layout.glyphs.iter().find(|g| g.byte == 1).unwrap().xf.as_coeffs()[4];
|
||||
let read = crate::cmd::effective_kerning(db, &before, &auto, 0, 1);
|
||||
assert!((read + 160.0).abs() < 0.01, "the font's -160/1000 em: {read}");
|
||||
let mut same = before.clone();
|
||||
vectorcraft_text::edit::style_range(&mut same.runs, 0, 1, |st| st.kerning = Some(read));
|
||||
assert!((drawn_v(&vectorcraft_text::layout(db, &same)) - drawn_v(&auto)).abs() < 0.01);
|
||||
|
||||
s.execute("type.step", &json!({"id": id.0, "start": 1, "end": 1, "attribute": "kerning", "by": 1})).unwrap();
|
||||
let after = obj(&s, id);
|
||||
assert!((after.runs[0].style.kerning.unwrap() + 160.0 - s.prefs.tracking_increment).abs() < 0.01);
|
||||
let moved = drawn_v(&vectorcraft_text::layout(db, &after)) - drawn_v(&auto);
|
||||
assert!((moved - s.prefs.tracking_increment * 0.05).abs() < 0.01, "V moves by half a kerning increment at 100%: {moved}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_auto_kerning_at_area_line_end_survives_unkerned_wrap() {
|
||||
use vectorcraft_text::test_fonts::{PAIR_KERNING_FAMILY, pair_kerning_font};
|
||||
|
||||
@@ -345,3 +345,27 @@ fn scale_text_attributes_bakes_group_scale_and_undoes() {
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
near(text(&s, id).runs[0].style.size, 12.0);
|
||||
}
|
||||
|
||||
/// Scale Text Attributes bakes a horizontal stretch into the Horizontal Scale: tracked type stays
|
||||
/// where the stretch drew it, as tracking is measured in the em the Horizontal Scale draws
|
||||
/// (`vectorcraft_text::spacing_scale`). It used to lose the stretch's share of the tracking.
|
||||
#[test]
|
||||
fn scale_text_attributes_keeps_tracked_type_where_it_was() {
|
||||
let mut s = session();
|
||||
let id = create(&mut s, 20.0);
|
||||
s.execute("text.setRangeStyle", &json!({"id": id.0, "tracking": 300})).unwrap();
|
||||
s.execute("object.scale", &json!({"sx": 200, "sy": 100})).unwrap();
|
||||
let pens = |s: &Session| {
|
||||
let t = text(s, id);
|
||||
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t);
|
||||
lay.glyphs.iter().map(|g| t.xf * (g.xf * Point::ZERO)).collect::<Vec<_>>()
|
||||
};
|
||||
let before = pens(&s);
|
||||
s.execute("type.scaleTextAttributes", &json!({})).unwrap();
|
||||
near(text(&s, id).runs[0].style.h_scale, 200.0);
|
||||
let after = pens(&s);
|
||||
assert_eq!(before.len(), after.len());
|
||||
for (b, a) in before.iter().zip(&after) {
|
||||
assert!((*a - *b).hypot() < 1e-6, "{before:?} {after:?}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user