mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 13:47:21 -06:00
Type: Kerning has an Optical option that spaces characters by their shapes
Squashed from #4825 onto main.
This commit is contained in:
@@ -388,6 +388,18 @@ fn char_position_round_trips() {
|
||||
assert!((scale - 0.583).abs() < 1e-9 && (shift + 3.33).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optical_kerning_round_trips_and_old_style_defaults_to_auto() {
|
||||
let st = CharStyle { optical_kerning: true, kerning: Some(120.0), ..CharStyle::default() };
|
||||
let value = serde_json::to_value(&st).unwrap();
|
||||
assert_eq!(value["opticalKerning"], true);
|
||||
assert_eq!(serde_json::from_value::<CharStyle>(value.clone()).unwrap(), st);
|
||||
let mut old = value;
|
||||
old.as_object_mut().unwrap().remove("opticalKerning");
|
||||
assert!(!serde_json::from_value::<CharStyle>(old).unwrap().optical_kerning);
|
||||
assert!(serde_json::to_value(CharStyle::default()).unwrap().get("opticalKerning").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn units() {
|
||||
assert_eq!(Unit::Inches.parse("1"), Some(72.0));
|
||||
|
||||
@@ -25,9 +25,12 @@ pub struct CharStyle {
|
||||
/// Tracking in 1/1000 em.
|
||||
#[serde(default)]
|
||||
pub tracking: f64,
|
||||
/// Kerning: None = Auto (metrics), Some(v) = manual in 1/1000 em.
|
||||
/// Manual pair spacing in 1/1000 em; None uses the automatic mode.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub kerning: Option<f64>,
|
||||
/// Use glyph shapes for automatic pair spacing; false uses font metrics.
|
||||
#[serde(default, rename = "opticalKerning", skip_serializing_if = "crate::skip::is_default")]
|
||||
pub optical_kerning: bool,
|
||||
#[serde(default)]
|
||||
pub baseline_shift: f64,
|
||||
#[serde(default = "hundred")]
|
||||
@@ -266,6 +269,7 @@ impl Default for CharStyle {
|
||||
leading: None,
|
||||
tracking: 0.0,
|
||||
kerning: None,
|
||||
optical_kerning: false,
|
||||
baseline_shift: 0.0,
|
||||
h_scale: 100.0,
|
||||
v_scale: 100.0,
|
||||
|
||||
@@ -223,6 +223,7 @@ impl ParaList {
|
||||
position: CharPosition::Normal,
|
||||
tracking: 0.0,
|
||||
kerning: None,
|
||||
optical_kerning: first.optical_kerning,
|
||||
tate_chu_yoko: false,
|
||||
underline: false,
|
||||
strikethrough: false,
|
||||
|
||||
@@ -87,6 +87,7 @@ pub mod swatchlib;
|
||||
pub mod symbollib;
|
||||
pub(crate) mod tabs;
|
||||
mod textedit;
|
||||
pub use textedit::effective_kerning;
|
||||
pub mod textstyles;
|
||||
pub(crate) mod textwrap;
|
||||
pub(crate) mod threads;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -39,7 +39,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Character",
|
||||
[],
|
||||
None,
|
||||
"{id? (default: the one selected type object), start?: byte, end?: byte (default: all text), font?, style?, size?: pt, leading?: pt|\"auto\", tracking?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, fill?: colour|\"none\", stroke?: colour|\"none\", keepModel?: bool (fill and stroke, as paint.setFill), strokeWidth?: pt, strokeOptions?: {weight?, cap?, join?, miterLimit?, align?, dash?, dashOffset?, alignDashes?} (as stroke.set: the character stroke), underline?, strikethrough?, allCaps?: bool, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), features?: [\"dlig\", \"-liga\", …], charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\", proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type), tateChuYoko?: bool (Character panel menu › Tate-chu-yoko, with Show East Asian Options: in vertical type those characters stand side by side across one em of the column; kept on the characters and ignored in horizontal type), noBreak?: bool (Character panel menu › No Break: these characters stay on one line, including across a space inside the run), charDirection?: \"default\"|\"leftToRight\"|\"rightToLeft\" (Character panel menu › Character Direction: the characters are set left to right or right to left inside their paragraph, as a Unicode bidi embedding; default: as the bidi algorithm sets them), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed), fontAxes?: {\"wght\": 650, …} (Character panel › Variable Font: the setting of each variation axis named, in the axis's units and clamped to its range; null: back to the style's own setting; other axes keep theirs; tags the font has no axis for are left out; a font or style change clears them; text.fontList {family, style} lists the axes)} style a character range (size/leading/baselineShift are document points and hScale includes the object transform; runs are split at the range ends) → {id, runs, warning?: text (the font or style set isn't available: its type is drawn in the fallback font or the family's closest style)}",
|
||||
"{id? (default: the one selected type object), start?: byte, end?: byte (default: all text), font?, style?, size?: pt, leading?: pt|\"auto\", tracking?, kerning?: 1/1000 em|\"auto\"|\"optical\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, fill?: colour|\"none\", stroke?: colour|\"none\", keepModel?: bool (fill and stroke, as paint.setFill), strokeWidth?: pt, strokeOptions?: {weight?, cap?, join?, miterLimit?, align?, dash?, dashOffset?, alignDashes?} (as stroke.set: the character stroke), underline?, strikethrough?, allCaps?: bool, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), features?: [\"dlig\", \"-liga\", …], charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\"|\"icfTop\"|\"icfBottom\", proportionalMetrics?: bool (full-width glyphs on the font's proportional widths: `palt` in horizontal type, `vpal` in vertical type), tateChuYoko?: bool (Character panel menu › Tate-chu-yoko, with Show East Asian Options: in vertical type those characters stand side by side across one em of the column; kept on the characters and ignored in horizontal type), noBreak?: bool (Character panel menu › No Break: these characters stay on one line, including across a space inside the run), charDirection?: \"default\"|\"leftToRight\"|\"rightToLeft\" (Character panel menu › Character Direction: the characters are set left to right or right to left inside their paragraph, as a Unicode bidi embedding; default: as the bidi algorithm sets them), digits?: \"default\"|\"arabic\"|\"hindi\"|\"farsi\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed), fontAxes?: {\"wght\": 650, …} (Character panel › Variable Font: the setting of each variation axis named, in the axis's units and clamped to its range; null: back to the style's own setting; other axes keep theirs; tags the font has no axis for are left out; a font or style change clears them; text.fontList {family, style} lists the axes)} style a character range (size/leading/baselineShift are document points and hScale includes the object transform; runs are split at the range ends) → {id, runs, warning?: text (the font or style set isn't available: its type is drawn in the fallback font or the family's closest style)}",
|
||||
has_doc,
|
||||
set_range_style
|
||||
),
|
||||
@@ -129,7 +129,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Reset Tracking and Kerning",
|
||||
[],
|
||||
Some("Cmd+Alt+Q"),
|
||||
"{ids?: [id…]} set tracking to 0 and kerning to Auto (the font's own kerning, which the Character panel shows as 0) in all the text of the type objects in `ids`, else in the text the Type tool has selected (at a caret: only the kerning of the character before it), else in every selected type object, as one undo step → {ids}",
|
||||
"{ids?: [id…]} set tracking to 0 and clear manual kerning without changing the automatic method in all the text of the type objects in `ids`, else in the text the Type tool has selected (at a caret: only the kerning of the character before it), else in every selected type object, as one undo step → {ids}",
|
||||
has_selection,
|
||||
reset_tracking
|
||||
),
|
||||
@@ -300,6 +300,7 @@ pub(crate) struct CharChange {
|
||||
leading: Option<Option<f64>>,
|
||||
tracking: Option<f64>,
|
||||
kerning: Option<Option<f64>>,
|
||||
optical_kerning: Option<bool>,
|
||||
baseline_shift: Option<f64>,
|
||||
h_scale: Option<f64>,
|
||||
v_scale: Option<f64>,
|
||||
@@ -476,7 +477,17 @@ impl CharChange {
|
||||
size: num("size"),
|
||||
leading: auto_or_num(p, "leading", cmd)?,
|
||||
tracking: num("tracking"),
|
||||
kerning: auto_or_num(p, "kerning", cmd)?,
|
||||
kerning: match p.get("kerning") {
|
||||
None | Some(Value::Null) => None,
|
||||
Some(Value::String(v)) if v.eq_ignore_ascii_case("optical") => None,
|
||||
Some(Value::String(v)) if v.eq_ignore_ascii_case("auto") => Some(None),
|
||||
Some(v) => Some(Some(v.as_f64().ok_or_else(|| bad(cmd, "kerning must be a number, \"auto\", or \"optical\""))?)),
|
||||
},
|
||||
optical_kerning: match p.get("kerning") {
|
||||
Some(Value::String(v)) if v.eq_ignore_ascii_case("optical") => Some(true),
|
||||
Some(Value::String(v)) if v.eq_ignore_ascii_case("auto") => Some(false),
|
||||
_ => None,
|
||||
},
|
||||
baseline_shift: num("baselineShift"),
|
||||
h_scale: num("hScale"),
|
||||
v_scale: num("vScale"),
|
||||
@@ -521,6 +532,7 @@ impl CharChange {
|
||||
&& self.leading.is_none()
|
||||
&& self.tracking.is_none()
|
||||
&& self.kerning.is_none()
|
||||
&& self.optical_kerning.is_none()
|
||||
&& self.baseline_shift.is_none()
|
||||
&& self.h_scale.is_none()
|
||||
&& self.v_scale.is_none()
|
||||
@@ -583,6 +595,9 @@ impl CharChange {
|
||||
if let Some(v) = self.kerning {
|
||||
st.kerning = v.map(|k| k.clamp(-1000.0, 10000.0));
|
||||
}
|
||||
if let Some(v) = self.optical_kerning {
|
||||
st.optical_kerning = v;
|
||||
}
|
||||
if let Some(v) = self.baseline_shift {
|
||||
st.baseline_shift = v;
|
||||
}
|
||||
@@ -734,18 +749,53 @@ fn set_range_style(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(w) = super::fonts::set_font_warning(set) {
|
||||
out["warning"] = json!(w);
|
||||
}
|
||||
if let Some(kerning) = p.get("kerning") {
|
||||
out["kerning"] = kerning.clone();
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// 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).
|
||||
fn glyph_em(t: &TextObject, g: &vectorcraft_text::PositionedGlyph) -> Option<f64> {
|
||||
let face = vectorcraft_text::FontDb::global().face_by_id(g.font_id)?;
|
||||
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 [a, b, ..] = g.xf.as_coeffs();
|
||||
Some(a.hypot(b) * face.units_per_em() / h_scale).filter(|em| em.is_finite() && *em > 0.0)
|
||||
}
|
||||
|
||||
/// The current pair correction in thousandths of an em, including an automatic correction.
|
||||
pub fn effective_kerning(db: &vectorcraft_text::FontDb, t: &TextObject, lay: &vectorcraft_text::TextLayout, x: usize, y: usize) -> f64 {
|
||||
if let Some(value) = edit::slice_runs(&t.runs, x, y).first().and_then(|r| r.style.kerning) {
|
||||
return value;
|
||||
}
|
||||
// Measure both layouts without wrapping so the same pair stays on one line.
|
||||
let mut trial = t.clone();
|
||||
trial.kind = TextKind::Point;
|
||||
let unwrapped = (!matches!(t.kind, TextKind::Point)).then(|| vectorcraft_text::layout(db, &trial));
|
||||
let original = unwrapped.as_ref().unwrap_or(lay);
|
||||
edit::style_range(&mut trial.runs, x, y, |st| st.kerning = Some(0.0));
|
||||
let base = vectorcraft_text::layout(db, &trial);
|
||||
let owner = original.glyphs.iter().find(|g| g.byte == x);
|
||||
let rtl = owner.is_some_and(|g| g.rtl);
|
||||
let drawn_gap = |layout: &vectorcraft_text::TextLayout| {
|
||||
let first = layout.glyphs.iter().find(|g| g.byte == if rtl { y } else { x })?;
|
||||
let following = layout.glyphs.iter().find(|g| g.byte == if rtl { x } else { y })?;
|
||||
if first.line != following.line {
|
||||
return None;
|
||||
}
|
||||
let axis = vectorcraft_geom::Vec2::new(first.angle.cos(), first.angle.sin());
|
||||
let along = if rtl { -axis } else { axis };
|
||||
let drawn = |g: &vectorcraft_text::PositionedGlyph| {
|
||||
let [_, _, _, _, tx, ty] = g.xf.as_coeffs();
|
||||
Point::new(tx, ty)
|
||||
};
|
||||
Some((drawn(following) - drawn(first)).dot(along))
|
||||
};
|
||||
let em = owner.and_then(|g| glyph_em(db, t, g)).unwrap_or(0.0);
|
||||
if em > 0.0 { drawn_gap(original).zip(drawn_gap(&base)).map_or(0.0, |(a, b)| (a - b) * 1000.0 / em) } else { 0.0 }
|
||||
}
|
||||
|
||||
fn touch_type(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "text.touchType";
|
||||
let id = id_param(p, "id").ok_or_else(|| bad(C, "missing `id`"))?;
|
||||
@@ -780,18 +830,21 @@ fn touch_type(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// A list marker's glyphs (length 0) stand for no character.
|
||||
let prev = lay.glyphs.iter().find(|h| h.line == g.line && h.len > 0 && h.byte + h.len == a);
|
||||
let pr = prev.map(|h| (h.byte, h.byte + h.len));
|
||||
let em = glyph_em(&t, g).ok_or_else(|| bad(C, "the character's size can't be measured"))?;
|
||||
let db = vectorcraft_text::FontDb::global();
|
||||
let em = glyph_em(db, &t, g).ok_or_else(|| bad(C, "the character's size can't be measured"))?;
|
||||
let em_prev = match prev {
|
||||
Some(h) => Some(glyph_em(&t, h).ok_or_else(|| bad(C, "the character's size can't be measured"))?),
|
||||
Some(h) => Some(glyph_em(db, &t, h).ok_or_else(|| bad(C, "the character's size can't be measured"))?),
|
||||
None if dx != 0.0 => return Err(bad(C, "the first character of a line has no character before it to kern: it moves only up and down (dy)")),
|
||||
None => None,
|
||||
};
|
||||
let kerning_in = |(x, y): (usize, usize)| edit::slice_runs(&t.runs, x, y).first().and_then(|r| r.style.kerning).unwrap_or(0.0);
|
||||
let kerning_in = |(x, y): (usize, usize)| effective_kerning(db, &t, &lay, x, y);
|
||||
let original_before = pr.map(kerning_in);
|
||||
let original_own = kerning_in((a, b));
|
||||
// The move along the line, in text space, limited to what kerning from -1000 to 10000 (1/1000
|
||||
// em) on the character before it and on the character itself can give.
|
||||
let mut moved = dx / doc_len(along);
|
||||
if let (Some(range), Some(ep)) = (pr, em_prev) {
|
||||
let (kp, kc) = (kerning_in(range), kerning_in((a, b)));
|
||||
if let (Some(_range), Some(ep)) = (pr, em_prev) {
|
||||
let (kp, kc) = (original_before.unwrap_or(0.0), original_own);
|
||||
let hi = ((10000.0 - kp) * ep).min((kc + 1000.0) * em) / 1000.0;
|
||||
let lo = ((-1000.0 - kp) * ep).max((kc - 10000.0) * em) / 1000.0;
|
||||
if lo <= hi {
|
||||
@@ -803,7 +856,8 @@ fn touch_type(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let touch = |t: &mut TextObject, before: Option<f64>, own: Option<f64>, raise: f64| {
|
||||
for (range, by) in [(pr, before), (Some((a, b)), own)] {
|
||||
if let (Some((x, y)), Some(by)) = (range, by) {
|
||||
edit::style_range(&mut t.runs, x, y, |st| st.kerning = Some((st.kerning.unwrap_or(0.0) + by).clamp(-1000.0, 10000.0)));
|
||||
let original = if (x, y) == (a, b) { original_own } else { original_before.unwrap_or(0.0) };
|
||||
edit::style_range(&mut t.runs, x, y, |st| st.kerning = Some((original + by).clamp(-1000.0, 10000.0)));
|
||||
}
|
||||
}
|
||||
edit::style_range(&mut t.runs, a, b, |st| {
|
||||
@@ -1086,9 +1140,20 @@ fn type_step(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
_ => None,
|
||||
})?;
|
||||
let mut caret_kerning = Vec::with_capacity(ranges.len());
|
||||
for &(id, a, b, caret) in &ranges {
|
||||
let value = if matches!(step, Step::Kerning) && caret {
|
||||
let t = text_ref(s, id)?;
|
||||
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t);
|
||||
Some(effective_kerning(vectorcraft_text::FontDb::global(), &t, &lay, a, b))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
caret_kerning.push(value);
|
||||
}
|
||||
if !ranges.is_empty() {
|
||||
s.edit("Character", |d, _| {
|
||||
for &(id, a, b, _) in &ranges {
|
||||
for (range_index, &(id, a, b, _)) in ranges.iter().enumerate() {
|
||||
let t = text_mut(d, id).ok_or(EngineError::NoNode(id))?;
|
||||
let points = if matches!(step, Step::Size | Step::Leading | Step::BaselineShift) {
|
||||
t.style_scale().ok_or_else(|| bad(C, "text transform is collapsed or unrepresentable"))?.points
|
||||
@@ -1127,7 +1192,10 @@ fn type_step(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
st.leading = Some(super::typecmd::local_type_value(t, leading * points + inc, (0.1, 5000.0), false, C)?);
|
||||
}
|
||||
Step::Tracking => st.tracking = (st.tracking + inc).clamp(-1000.0, 10000.0),
|
||||
Step::Kerning => st.kerning = Some((st.kerning.unwrap_or(0.0) + inc).clamp(-1000.0, 10000.0)),
|
||||
Step::Kerning => {
|
||||
let current = caret_kerning.get(range_index).copied().flatten().unwrap_or_else(|| st.kerning.unwrap_or(0.0));
|
||||
st.kerning = Some((current + inc).clamp(-1000.0, 10000.0));
|
||||
}
|
||||
Step::BaselineShift => {
|
||||
st.baseline_shift = super::typecmd::local_type_value(t, st.baseline_shift * points + inc, (-1296.0, 1296.0), false, C)?;
|
||||
}
|
||||
@@ -1142,9 +1210,10 @@ fn type_step(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({ "ids": ranges.iter().map(|(id, ..)| id.0).collect::<Vec<_>>() }))
|
||||
}
|
||||
|
||||
/// Cmd/Ctrl+Alt+Q: tracking 0 and kerning Auto (0 in the Character panel) in the type objects in
|
||||
/// Cmd/Ctrl+Alt+Q: tracking 0 and manual kerning cleared in the type objects in
|
||||
/// `ids`, else in the Type tool's selected text, else in every selected type object. At a caret,
|
||||
/// only the kerning of the character before it changes, as Alt+←/→ in horizontal type or Alt+↓/↑ in vertical type kerns there.
|
||||
/// The automatic method remains selected.
|
||||
fn reset_tracking(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ranges = type_ranges(s, p, "type.resetTracking", |text, a| Some((edit::prev_char(text, a), a)))?;
|
||||
if !ranges.is_empty() {
|
||||
|
||||
@@ -152,6 +152,7 @@ pub fn char_attrs(v: &CharStyle) -> Map<String, Value> {
|
||||
("fontAxes", json!(v.font_axes)),
|
||||
("leading", json!(v.leading)),
|
||||
("kerning", json!(v.kerning)),
|
||||
("opticalKerning", json!(v.optical_kerning)),
|
||||
("features", json!(v.features)),
|
||||
("overprint_fill", json!(v.overprint_fill)),
|
||||
("overprint_stroke", json!(v.overprint_stroke)),
|
||||
|
||||
@@ -460,6 +460,20 @@ fn auto_leading_new_distinguishes_unset_attributes() {
|
||||
assert_eq!(s.doc().unwrap().doc.char_styles[2].attrs.get("leading"), Some(&Value::Null));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optical_character_style_captures_and_redefines_automatic_mode() {
|
||||
let (mut s, id) = session_with_text("III");
|
||||
s.execute("text.setRangeStyle", &json!({"id": id, "kerning": "optical"})).unwrap();
|
||||
s.execute("charStyle.new", &json!({"name": "Profile", "id": id})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.char_styles[0].attrs.get("opticalKerning"), Some(&json!(true)));
|
||||
s.execute("text.setRangeStyle", &json!({"id": id, "kerning": "auto"})).unwrap();
|
||||
s.execute("charStyle.apply", &json!({"name": "Profile", "id": id})).unwrap();
|
||||
assert!(runs(&s, id)[0].style.optical_kerning);
|
||||
s.execute("text.setRangeStyle", &json!({"id": id, "kerning": "auto"})).unwrap();
|
||||
s.execute("charStyle.redefine", &json!({"name": "Profile", "id": id})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.char_styles[0].attrs.get("opticalKerning"), Some(&json!(false)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paragraph_styles_and_survive_save() {
|
||||
let (mut s, id) = session_with_text("Body");
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//! on-path type from paths, Fit Headline, Convert To Area/Point Type.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_doc::{NodeId, NodeKind, TextKind, TextObject};
|
||||
use vectorcraft_doc::{CharStyle, NodeId, NodeKind, TextKind, TextObject};
|
||||
use vectorcraft_geom::Point;
|
||||
use vectorcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey};
|
||||
|
||||
@@ -87,6 +87,168 @@ fn set_range_style_splits_runs() {
|
||||
assert!(s.execute("text.setRangeStyle", &json!({"id": id.0, "size": -1})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optical_command_manual_pair_undo_redo_and_native_reopen() {
|
||||
let mut s = session();
|
||||
let id = text(&mut s, "III");
|
||||
s.execute("text.setRangeStyle", &json!({"id": id.0, "kerning": 70})).unwrap();
|
||||
let result = s.execute("text.setRangeStyle", &json!({"id": id.0, "kerning": "optical"})).unwrap();
|
||||
assert_eq!(result["kerning"], "optical");
|
||||
assert!(obj(&s, id).runs[0].style.optical_kerning);
|
||||
assert_eq!(obj(&s, id).runs[0].style.kerning, Some(70.0));
|
||||
s.execute("text.setRangeStyle", &json!({"id": id.0, "start": 1, "end": 2, "kerning": 0})).unwrap();
|
||||
let styled = obj(&s, id);
|
||||
assert_eq!(styled.runs.iter().map(|r| r.style.kerning).collect::<Vec<_>>(), vec![Some(70.0), Some(0.0), Some(70.0)]);
|
||||
assert!(styled.runs.iter().all(|r| r.style.optical_kerning));
|
||||
let bytes = vectorcraft_format::save_file(&s.doc().unwrap().doc);
|
||||
let opened = vectorcraft_format::load(&bytes).unwrap();
|
||||
let NodeKind::Text(reopened) = &opened.node(id).unwrap().kind else { panic!("text missing") };
|
||||
assert_eq!(
|
||||
reopened.runs.iter().map(|r| (r.style.optical_kerning, r.style.kerning)).collect::<Vec<_>>(),
|
||||
styled.runs.iter().map(|r| (r.style.optical_kerning, r.style.kerning)).collect::<Vec<_>>()
|
||||
);
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(obj(&s, id).runs[0].style.kerning, Some(70.0));
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(obj(&s, id).runs[1].style.kerning, Some(0.0));
|
||||
assert!(obj(&s, id).runs.iter().all(|r| r.style.optical_kerning));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optical_set_format_keeps_existing_pair_override() {
|
||||
let mut s = session();
|
||||
let id = text(&mut s, "III");
|
||||
s.execute("text.setRangeStyle", &json!({"id": id.0, "start": 0, "end": 1, "kerning": 40})).unwrap();
|
||||
let result = s.execute("text.setFormat", &json!({"ids": [id.0], "kerning": "optical"})).unwrap();
|
||||
assert_eq!(result["kerning"], "optical");
|
||||
let t = obj(&s, id);
|
||||
assert!(t.runs.iter().all(|r| r.style.optical_kerning));
|
||||
assert_eq!(t.runs[0].style.kerning, Some(40.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optical_kerning_step_starts_from_the_automatic_pair() {
|
||||
let mut s = session();
|
||||
let id = NodeId(
|
||||
s.execute("text.create", &json!({"x": 10, "y": 10, "text": "AVA", "size": 100, "font": "JetBrains Mono"})).unwrap()["id"].as_u64().unwrap(),
|
||||
);
|
||||
s.execute("text.setRangeStyle", &json!({"id": id.0, "kerning": "optical"})).unwrap();
|
||||
let t = obj(&s, id);
|
||||
let optical = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t);
|
||||
let mut manual = t.clone();
|
||||
vectorcraft_text::edit::style_range(&mut manual.runs, 0, 1, |st| st.kerning = Some(0.0));
|
||||
let unkerned = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &manual);
|
||||
let auto = (optical.glyphs[0].advance - unkerned.glyphs[0].advance) * 1000.0 / 100.0;
|
||||
assert!(auto.abs() > 1.0, "the font needs a measurable shape correction");
|
||||
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() - auto - s.prefs.tracking_increment).abs() < 0.01);
|
||||
assert!(after.runs.iter().all(|r| r.style.optical_kerning));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metrics_pair_step_preserves_the_drawn_auto_position() {
|
||||
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(),
|
||||
);
|
||||
let before = obj(&s, id);
|
||||
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];
|
||||
assert!((crate::cmd::effective_kerning(db, &before, &auto, 0, 1) + 160.0).abs() < 0.01);
|
||||
let mut same = before.clone();
|
||||
vectorcraft_text::edit::style_range(&mut same.runs, 0, 1, |st| st.kerning = Some(-160.0));
|
||||
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.1).abs() < 0.01, "V moves by one kerning increment: {moved}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_auto_kerning_at_area_line_end_survives_unkerned_wrap() {
|
||||
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 style = CharStyle { font_family: PAIR_KERNING_FAMILY.into(), size: 100.0, ..CharStyle::default() };
|
||||
let mut text = TextObject::point(Point::ZERO, "AV", style);
|
||||
let mut wrapping_pair = None;
|
||||
for width in 80..160 {
|
||||
text.kind = TextKind::Area { frame: vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(0.0, 0.0, f64::from(width), 300.0)) };
|
||||
let auto = vectorcraft_text::layout(db, &text);
|
||||
let mut trial = text.clone();
|
||||
vectorcraft_text::edit::style_range(&mut trial.runs, 0, 1, |st| st.kerning = Some(0.0));
|
||||
let unkerned = vectorcraft_text::layout(db, &trial);
|
||||
if auto.glyphs.len() >= 2
|
||||
&& unkerned.glyphs.len() >= 2
|
||||
&& auto.glyphs[0].line == auto.glyphs[1].line
|
||||
&& unkerned.glyphs[0].line != unkerned.glyphs[1].line
|
||||
{
|
||||
wrapping_pair = Some((text.clone(), auto, unkerned));
|
||||
break;
|
||||
}
|
||||
}
|
||||
let (text, auto, unkerned) = wrapping_pair.expect("the kerned pair fits on a line that wraps the unkerned pair");
|
||||
assert_ne!(unkerned.glyphs[0].line, unkerned.glyphs[1].line);
|
||||
assert!((crate::cmd::effective_kerning(db, &text, &auto, 0, 1) + 160.0).abs() < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_optical_kerning_reads_the_right_to_left_pair() {
|
||||
use vectorcraft_text::test_fonts::{RIGHT_TO_LEFT_OPTICAL_FAMILY, right_to_left_optical_font};
|
||||
|
||||
let db = vectorcraft_text::FontDb::with_font_dirs(vec![]);
|
||||
assert_eq!(db.add_font(right_to_left_optical_font().unwrap()), 1);
|
||||
let style = CharStyle { font_family: RIGHT_TO_LEFT_OPTICAL_FAMILY.into(), size: 100.0, optical_kerning: true, ..CharStyle::default() };
|
||||
let text = TextObject::point(Point::ZERO, "אב", style);
|
||||
let optical = vectorcraft_text::layout(&db, &text);
|
||||
let mut manual = text.clone();
|
||||
manual.runs[0].style.kerning = Some(0.0);
|
||||
let unkerned = vectorcraft_text::layout(&db, &manual);
|
||||
let drawn_gap = |lay: &vectorcraft_text::TextLayout| {
|
||||
let drawn = |byte| lay.glyphs.iter().find(|g| g.byte == byte).unwrap().xf.as_coeffs()[4];
|
||||
drawn(2) - drawn(0)
|
||||
};
|
||||
let expected = (drawn_gap(&optical) - drawn_gap(&unkerned)) * 10.0;
|
||||
assert!(expected.abs() > 1.0, "the outlined pair has a measurable Optical correction");
|
||||
let actual = crate::cmd::effective_kerning(&db, &text, &optical, 0, 2);
|
||||
assert!((actual - expected).abs() < 0.01, "the caret reads the adjacent right-to-left pair");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reset_tracking_clears_manual_pairs_without_changing_optical_mode() {
|
||||
let mut s = session();
|
||||
let id = text(&mut s, "III");
|
||||
s.execute("text.setRangeStyle", &json!({"id": id.0, "kerning": "optical"})).unwrap();
|
||||
s.execute("text.setRangeStyle", &json!({"id": id.0, "start": 0, "end": 1, "kerning": 80})).unwrap();
|
||||
s.select_tool("type", ViewInfo::default()).unwrap();
|
||||
let t = obj(&s, id);
|
||||
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t);
|
||||
let (a, b) = vectorcraft_text::caret_position(&lay, 1);
|
||||
let p = t.xf * a.midpoint(b);
|
||||
click(&mut s, p.x, p.y);
|
||||
assert!(s.tool_wants_text());
|
||||
s.set_tool_option("select", &json!({"start": 1, "end": 1}));
|
||||
s.execute("type.resetTracking", &json!({})).unwrap();
|
||||
let caret = obj(&s, id);
|
||||
assert_eq!(caret.runs[0].style.kerning, None);
|
||||
assert!(caret.runs.iter().all(|r| r.style.optical_kerning));
|
||||
|
||||
s.execute("text.setRangeStyle", &json!({"id": id.0, "kerning": 60, "tracking": 30})).unwrap();
|
||||
s.set_tool_option("select", &json!({"start": 0, "end": 3}));
|
||||
s.execute("type.resetTracking", &json!({})).unwrap();
|
||||
let range = obj(&s, id);
|
||||
assert!(range.runs.iter().all(|r| r.style.kerning.is_none() && r.style.tracking == 0.0));
|
||||
assert!(range.runs.iter().all(|r| r.style.optical_kerning));
|
||||
}
|
||||
|
||||
/// A character style applied from byte 2^32 to 2^32 + 5 styles no character, as a range past the
|
||||
/// end does. A 32-bit build read the range as bytes 0 to 5 and styled "Hello".
|
||||
#[test]
|
||||
|
||||
+277
-5
@@ -1,5 +1,6 @@
|
||||
//! Shaping: text runs -> positioned glyphs in points (before line breaking).
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -8,6 +9,7 @@ use unicode_script::{Script, UnicodeScript};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
use harfrust::{Direction, Feature, ShapeOptions, Tag, UnicodeBuffer};
|
||||
use kurbo::Shape;
|
||||
use skrifa::MetadataProvider;
|
||||
use skrifa::charmap::Charmap;
|
||||
use skrifa::instance::Size;
|
||||
@@ -446,6 +448,216 @@ pub(crate) fn shape_range(
|
||||
{
|
||||
o.sort_by_key(|g| g.byte);
|
||||
}
|
||||
if !feats.vertical {
|
||||
optical_pairs(db, text, out.get_mut(output_start..).unwrap_or_default(), runs);
|
||||
}
|
||||
}
|
||||
|
||||
/// Line segments of a glyph outline in font units, cached for the current shaping pass.
|
||||
type Profile = Vec<(f64, f64, f64, f64)>;
|
||||
type CachedProfile = (Profile, (f64, f64));
|
||||
|
||||
fn outline_profile(db: &FontDb, g: &SGlyph, cache: &mut HashMap<(u32, u32), Option<Arc<CachedProfile>>>) -> Option<Arc<CachedProfile>> {
|
||||
let key = (g.face.id(), g.gid);
|
||||
cache
|
||||
.entry(key)
|
||||
.or_insert_with(|| {
|
||||
let path = db.outline(&g.face, g.gid);
|
||||
let bounds = path.bounding_box();
|
||||
if path.elements().is_empty()
|
||||
|| path.elements().len() > 512
|
||||
|| ![bounds.x0, bounds.y0, bounds.x1, bounds.y1].iter().all(|v| v.is_finite() && v.abs() <= 100_000.0)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let mut lines = Vec::new();
|
||||
let mut previous = None;
|
||||
let mut contour_start = None;
|
||||
kurbo::flatten(path.iter(), 2.0, |el| match el {
|
||||
kurbo::PathEl::MoveTo(p) => {
|
||||
previous = Some(p);
|
||||
contour_start = Some(p);
|
||||
}
|
||||
kurbo::PathEl::LineTo(p) => {
|
||||
if let Some(a) = previous
|
||||
&& lines.len() < 8192
|
||||
{
|
||||
lines.push((a.x, a.y, p.x, p.y));
|
||||
}
|
||||
previous = Some(p);
|
||||
}
|
||||
kurbo::PathEl::ClosePath => {
|
||||
if let Some((a, b)) = previous.zip(contour_start)
|
||||
&& lines.len() < 8192
|
||||
{
|
||||
lines.push((a.x, a.y, b.x, b.y));
|
||||
}
|
||||
previous = None;
|
||||
contour_start = None;
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
(!lines.is_empty() && lines.len() < 8192).then(|| Arc::new((lines, (bounds.y0, bounds.y1))))
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// The left and right ink edges where a horizontal slice crosses an outline.
|
||||
fn profile_edges(lines: &[(f64, f64, f64, f64)], y: f64, sx: f64, sy: f64, dx: f64, dy: f64) -> Option<(f64, f64)> {
|
||||
let mut left = f64::INFINITY;
|
||||
let mut right = f64::NEG_INFINITY;
|
||||
for &(x0, y0, x1, y1) in lines {
|
||||
let (a, b) = (y0 * sy + dy, y1 * sy + dy);
|
||||
if (a <= y && y < b) || (b <= y && y < a) {
|
||||
let x = (x0 + (x1 - x0) * (y - a) / (b - a)) * sx + dx;
|
||||
left = left.min(x);
|
||||
right = right.max(x);
|
||||
}
|
||||
}
|
||||
(left.is_finite() && right.is_finite()).then_some((left, right))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn profile_correction(
|
||||
a: &[(f64, f64, f64, f64)],
|
||||
b: &[(f64, f64, f64, f64)],
|
||||
left: (f64, f64, f64, f64),
|
||||
right: (f64, f64, f64, f64),
|
||||
advance: f64,
|
||||
em: f64,
|
||||
upem: f64,
|
||||
low: f64,
|
||||
high: f64,
|
||||
) -> Option<f64> {
|
||||
if !low.is_finite() || !high.is_finite() || high <= low || !em.is_finite() || em <= 0.0 || !upem.is_finite() || upem <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let mut gaps = Vec::new();
|
||||
for j in 0..24 {
|
||||
let y = low + (high - low) * (f64::from(j) + 0.5) / 24.0;
|
||||
if let (Some((_, ar)), Some((bl, _))) =
|
||||
(profile_edges(a, y, left.0, left.1, left.2, left.3), profile_edges(b, y, right.0, right.1, right.2, right.3))
|
||||
{
|
||||
gaps.push(advance + bl - ar);
|
||||
}
|
||||
}
|
||||
if gaps.is_empty() {
|
||||
return None;
|
||||
}
|
||||
gaps.sort_by(f64::total_cmp);
|
||||
let gap = gaps.get(gaps.len() / 2).copied().unwrap_or(0.0);
|
||||
let unit = em / upem;
|
||||
let correction = ((0.08 * em - gap).clamp(-0.25 * em, 0.25 * em) / unit).round() * unit;
|
||||
let correction = correction.clamp(-0.25 * em, 0.25 * em);
|
||||
correction.is_finite().then_some(correction)
|
||||
}
|
||||
|
||||
/// Move each Optical pair toward an ink gap of 0.08 em, at 24 overlapping vertical slices.
|
||||
/// The median avoids a single narrow tip deciding the entire pair. Corrections are rounded to
|
||||
/// one font unit and limited to 1/4 em in either direction. Missing ink gives zero correction.
|
||||
fn optical_pairs(db: &FontDb, text: &str, glyphs: &mut [SGlyph], runs: &[(Range<usize>, &CharStyle)]) {
|
||||
let mut cache = HashMap::new();
|
||||
let mut corrections: HashMap<[u64; 15], Option<f64>> = HashMap::new();
|
||||
let shaped_advances: Vec<f64> = glyphs.iter().map(|g| g.adv).collect();
|
||||
for i in 0..glyphs.len().saturating_sub(1) {
|
||||
let Some((a, b)) = glyphs.get(i).zip(glyphs.get(i + 1)) else { continue };
|
||||
if a.level != b.level {
|
||||
continue;
|
||||
}
|
||||
let Some(owner) = runs.get(a.run).map(|(_, s)| *s) else { continue };
|
||||
let invisible = |ch: char| matches!(ch, SOFT_HYPHEN | '\u{2028}') || ch.is_control();
|
||||
if !owner.optical_kerning
|
||||
|| a.byte == b.byte
|
||||
|| invisible(a.ch)
|
||||
|| invisible(b.ch)
|
||||
|| text.get(a.byte..b.byte).is_some_and(|between| between.chars().skip(1).any(invisible))
|
||||
|| a.rotation != 0.0
|
||||
|| b.rotation != 0.0
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if owner.kerning.is_some() {
|
||||
continue;
|
||||
}
|
||||
let mut start = i;
|
||||
while start > 0 && glyphs[start - 1].byte == a.byte {
|
||||
start -= 1;
|
||||
}
|
||||
let mut end = i + 1;
|
||||
while end + 1 < glyphs.len() && glyphs[end + 1].byte == b.byte {
|
||||
end += 1;
|
||||
}
|
||||
// A mark may precede its base inside either direction's shaped cluster.
|
||||
let first_base = glyphs[start..=i]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by(|(a, _), (b, _)| shaped_advances[start + *a].total_cmp(&shaped_advances[start + *b]))
|
||||
.map_or(start, |(offset, _)| start + offset);
|
||||
let second_base = glyphs[i + 1..=end]
|
||||
.iter()
|
||||
.enumerate()
|
||||
.max_by(|(a, _), (b, _)| shaped_advances[i + 1 + *a].total_cmp(&shaped_advances[i + 1 + *b]))
|
||||
.map_or(i + 1, |(offset, _)| i + 1 + offset);
|
||||
let (left_index, right_index, target, cluster_start, cluster_end) =
|
||||
if a.level.is_rtl() { (second_base, first_base, end, i + 1, end) } else { (first_base, second_base, i, start, i) };
|
||||
let (left, right) = (&glyphs[left_index], &glyphs[right_index]);
|
||||
if left.byte == right.byte
|
||||
|| left.is_space()
|
||||
|| right.is_space()
|
||||
|| is_mark(right.ch)
|
||||
|| left.inline.is_some()
|
||||
|| right.inline.is_some()
|
||||
|| left.ch == '\n'
|
||||
|| right.ch == '\n'
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let left_y = left.dy - left.bshift;
|
||||
let right_y = right.dy - right.bshift;
|
||||
let Some(left_style) = runs.get(left.run).map(|(_, s)| *s) else { continue };
|
||||
let em = left.sx * left.face.upem / (left_style.h_scale / 100.0);
|
||||
let tracking = left_style.tracking / 1000.0 * em;
|
||||
let manual = left_style.kerning.unwrap_or(0.0) / 1000.0 * em;
|
||||
let advance = glyphs[cluster_start..=cluster_end].iter().map(|g| g.adv).sum::<f64>() - tracking - manual;
|
||||
let key = [
|
||||
u64::from(left.face.id()),
|
||||
u64::from(left.gid),
|
||||
u64::from(right.face.id()),
|
||||
u64::from(right.gid),
|
||||
left.sx.to_bits(),
|
||||
left.sy.to_bits(),
|
||||
left.dx.to_bits(),
|
||||
left_y.to_bits(),
|
||||
right.sx.to_bits(),
|
||||
right.sy.to_bits(),
|
||||
right.dx.to_bits(),
|
||||
right_y.to_bits(),
|
||||
advance.to_bits(),
|
||||
em.to_bits(),
|
||||
left.face.upem.to_bits(),
|
||||
];
|
||||
let correction = *corrections.entry(key).or_insert_with(|| {
|
||||
let (Some(a), Some(b)) = (outline_profile(db, left, &mut cache), outline_profile(db, right, &mut cache)) else { return None };
|
||||
let low = (a.1.0 * left.sy + left_y).max(b.1.0 * right.sy + right_y);
|
||||
let high = (a.1.1 * left.sy + left_y).min(b.1.1 * right.sy + right_y);
|
||||
profile_correction(
|
||||
&a.0,
|
||||
&b.0,
|
||||
(left.sx, left.sy, left.dx, left_y),
|
||||
(right.sx, right.sy, right.dx, right_y),
|
||||
advance,
|
||||
em,
|
||||
left.face.upem,
|
||||
low,
|
||||
high,
|
||||
)
|
||||
});
|
||||
if let (Some(correction), Some(glyph)) = (correction, glyphs.get_mut(target))
|
||||
&& !invisible(glyph.ch)
|
||||
{
|
||||
glyph.adv += correction;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Index in the original run list containing `byte`, limited to one coalesced shaping span.
|
||||
@@ -474,6 +686,7 @@ fn shapes_alike(a: &CharStyle, b: &CharStyle) -> bool {
|
||||
leading: _,
|
||||
tracking,
|
||||
kerning: _,
|
||||
optical_kerning: _,
|
||||
baseline_shift: _,
|
||||
h_scale,
|
||||
v_scale,
|
||||
@@ -696,6 +909,15 @@ fn shape_segment(text: &str, context: &Range<usize>, seg: &Segment, feats: &OtFe
|
||||
};
|
||||
let manual = |byte: usize| run_at(byte).1.kerning.is_some();
|
||||
let mixed_kerning = boundaries().any(|b| manual(b) != manual(range.start));
|
||||
let optical = |byte: usize| run_at(byte).1.optical_kerning;
|
||||
let mode_boundaries: Vec<usize> = boundaries().filter(|&b| !feats.vertical && optical(b - 1) && !optical(b)).collect();
|
||||
if !mode_boundaries.is_empty() {
|
||||
return shape_apart(mode_boundaries, out);
|
||||
}
|
||||
let manual_ends: Vec<usize> = boundaries().filter(|&b| manual(b - 1) && !manual(b)).collect();
|
||||
if !manual_ends.is_empty() {
|
||||
return shape_apart(manual_ends, out);
|
||||
}
|
||||
|
||||
// Shape the segment as if all of it were in `st`, with the features `more` on top.
|
||||
let shape_in = |st: &CharStyle, more: &[Feature]| {
|
||||
@@ -793,17 +1015,43 @@ fn shape_segment(text: &str, context: &Range<usize>, seg: &Segment, feats: &OtFe
|
||||
}
|
||||
shaped
|
||||
};
|
||||
let Shaped { raw, proportional, vpal } = if mixed_kerning {
|
||||
let first_optical = if optical(range.start) { Some(range.start) } else { boundaries().find(|&b| optical(b)) };
|
||||
let Shaped { raw, proportional, vpal } = if let Some(first) = first_optical.filter(|_| !feats.vertical) {
|
||||
// The pair entering Optical keeps the left character's font kerning. Subsequent
|
||||
// Optical pairs use outline spacing, so font pair features stop at its second character.
|
||||
let optical_off = if first == range.start {
|
||||
first
|
||||
} else {
|
||||
text.get(first..range.end).and_then(|s| s.chars().next()).map_or(range.end, |c| first + c.len_utf8())
|
||||
};
|
||||
let first_manual = if manual(range.start) { Some(range.start) } else { boundaries().find(|&b| manual(b)) };
|
||||
if let Some(start) = first_manual.filter(|&b| b > range.start)
|
||||
&& upper
|
||||
&& first_char(start).to_uppercase().nth(1).is_some()
|
||||
{
|
||||
return shape_apart(vec![start], out);
|
||||
}
|
||||
let manual_off = first_manual.map_or(range.end, |start| {
|
||||
if start == range.start {
|
||||
start
|
||||
} else {
|
||||
text.get(start..range.end).and_then(|s| s.chars().next()).map_or(range.end, |c| start + c.len_utf8())
|
||||
}
|
||||
});
|
||||
let off = optical_off.min(manual_off);
|
||||
let more: Vec<Feature> = [b"kern", b"apkn", b"vkrn", b"vapk"]
|
||||
.into_iter()
|
||||
.filter(|_| off < range.end)
|
||||
.map(|t| Feature::new(Tag::new(t), 0, off..range.end))
|
||||
.collect();
|
||||
shape_in(&CharStyle { kerning: None, ..run_at(range.start).1.clone() }, &more)
|
||||
} else if mixed_kerning {
|
||||
// A font's kerning pair applies after a character with automatic kerning and not after one
|
||||
// with manual kerning. The segment is shaped apart where manual kerning ends, so each piece
|
||||
// is text with automatic kerning followed by text with manual kerning. A piece is shaped
|
||||
// with the font's kerning on, and off (`kern`, and the `apkn`, `vkrn` and `vapk` that
|
||||
// `OtFeatures` turns off with it) from the second character with manual kerning on: a
|
||||
// pair is applied only when both of its glyphs have the feature on.
|
||||
let ends: Vec<usize> = boundaries().filter(|&b| manual(b - 1) && !manual(b)).collect();
|
||||
if !ends.is_empty() {
|
||||
return shape_apart(ends, out);
|
||||
}
|
||||
let first = boundaries().find(|&b| manual(b)).unwrap_or(range.end);
|
||||
// A first character with manual kerning that All Caps or Small Caps sets as several letters
|
||||
// (ß as SS) gives them one cluster, which a feature range cannot split, so the font's
|
||||
@@ -879,3 +1127,27 @@ fn shape_segment(text: &str, context: &Range<usize>, seg: &Segment, feats: &OtFe
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod optical_tests {
|
||||
use super::*;
|
||||
|
||||
fn box_profile(left: f64, right: f64) -> Profile {
|
||||
vec![(left, 0.0, left, 100.0), (right, 0.0, right, 100.0)]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optical_side_profiles_tighten_loosen_and_stay_bounded() {
|
||||
let a = box_profile(0.0, 80.0);
|
||||
let b = box_profile(0.0, 20.0);
|
||||
let place = (1.0, 1.0, 0.0, 0.0);
|
||||
let tight = profile_correction(&a, &b, place, place, 120.0, 100.0, 1000.0, 0.0, 100.0).unwrap();
|
||||
let loose = profile_correction(&a, &b, place, place, 60.0, 100.0, 1000.0, 0.0, 100.0).unwrap();
|
||||
assert_eq!(tight, -25.0);
|
||||
assert_eq!(loose, 25.0);
|
||||
assert_eq!(profile_correction(&a, &b, place, place, 88.0, 100.0, 1000.0, 0.0, 100.0), Some(0.0));
|
||||
assert_eq!(profile_correction(&a, &b, place, place, 120.0, 100.0, 1000.0, 0.0, 100.0), Some(tight));
|
||||
assert_eq!(profile_correction(&a, &b, place, place, 120.0, 100.0, 1000.0, 100.0, 100.0), None);
|
||||
assert_eq!(profile_correction(&[], &b, place, place, 120.0, 100.0, 1000.0, 0.0, 100.0), None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,6 +412,81 @@ pub fn lam_alef_font(feature: &[u8; 4]) -> Option<Vec<u8>> {
|
||||
/// The family of [`pair_kerning_font`].
|
||||
pub const PAIR_KERNING_FAMILY: &str = "Pairtest Sans";
|
||||
|
||||
/// The family of [`no_pair_kerning_font`].
|
||||
pub const NO_PAIR_KERNING_FAMILY: &str = "No Pairtest Sans";
|
||||
|
||||
/// A font with the bundled Source Sans 3 outlines and no `GPOS` or `kern` table.
|
||||
pub fn no_pair_kerning_font() -> Option<Vec<u8>> {
|
||||
const BASE: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf");
|
||||
let font = skrifa::FontRef::new(BASE).ok()?;
|
||||
let family = NO_PAIR_KERNING_FAMILY;
|
||||
let name = name_table(&[(1, family), (2, "Regular"), (4, "No Pairtest Sans Regular"), (6, "NoPairtestSans-Regular")])?;
|
||||
let mut builder = FontBuilder::new();
|
||||
builder.add_raw(Tag::new(b"name"), name);
|
||||
for r in font.table_directory.table_records() {
|
||||
let tag = Tag::new(&r.tag().to_be_bytes());
|
||||
if !matches!(tag, t if t == Tag::new(b"name") || t == Tag::new(b"GPOS") || t == Tag::new(b"kern")) {
|
||||
builder.add_raw(tag, font.table_data(r.tag())?.as_bytes());
|
||||
}
|
||||
}
|
||||
Some(builder.build())
|
||||
}
|
||||
|
||||
/// The family of [`right_to_left_optical_font`].
|
||||
pub const RIGHT_TO_LEFT_OPTICAL_FAMILY: &str = "Right-to-left Profile Sans";
|
||||
|
||||
/// Two Hebrew characters mapped to the bundled Source Sans 3 A and V outlines.
|
||||
pub fn right_to_left_optical_font() -> Option<Vec<u8>> {
|
||||
use write_fonts::tables::cmap::Cmap;
|
||||
|
||||
const BASE: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf");
|
||||
let font = skrifa::FontRef::new(BASE).ok()?;
|
||||
let charmap = font.charmap();
|
||||
let mapping = [('א', 'A'), ('ב', 'V')];
|
||||
let mappings = mapping.into_iter().map(|(c, source)| Some((c, write_fonts::types::GlyphId::new(charmap.map(source)?.to_u32()))));
|
||||
let cmap = Cmap::from_mappings(mappings.collect::<Option<Vec<_>>>()?).ok()?;
|
||||
let family = RIGHT_TO_LEFT_OPTICAL_FAMILY;
|
||||
let name = name_table(&[(1, family), (2, "Regular"), (4, "Right-to-left Profile Sans Regular"), (6, "RtlProfileSans-Regular")])?;
|
||||
let mut builder = FontBuilder::new();
|
||||
builder.add_table(&cmap).ok()?;
|
||||
builder.add_raw(Tag::new(b"name"), name);
|
||||
for r in font.table_directory.table_records() {
|
||||
let tag = Tag::new(&r.tag().to_be_bytes());
|
||||
if tag != Tag::new(b"GPOS") && tag != Tag::new(b"kern") && !builder.contains(tag) {
|
||||
builder.add_raw(tag, font.table_data(r.tag())?.as_bytes());
|
||||
}
|
||||
}
|
||||
Some(builder.build())
|
||||
}
|
||||
|
||||
/// The family of [`soft_hyphen_outline_font`].
|
||||
pub const SOFT_HYPHEN_OUTLINE_FAMILY: &str = "Soft Hyphen Profile Sans";
|
||||
|
||||
/// A face that maps the invisible soft hyphen to the bundled hyphen outline.
|
||||
pub fn soft_hyphen_outline_font() -> Option<Vec<u8>> {
|
||||
use write_fonts::tables::cmap::Cmap;
|
||||
|
||||
const BASE: &[u8] = include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf");
|
||||
let font = skrifa::FontRef::new(BASE).ok()?;
|
||||
let charmap = font.charmap();
|
||||
let mappings = [('A', 'A'), ('V', 'V'), ('-', '-'), ('\u{00AD}', '-')]
|
||||
.into_iter()
|
||||
.map(|(c, source)| Some((c, write_fonts::types::GlyphId::new(charmap.map(source)?.to_u32()))));
|
||||
let cmap = Cmap::from_mappings(mappings.collect::<Option<Vec<_>>>()?).ok()?;
|
||||
let family = SOFT_HYPHEN_OUTLINE_FAMILY;
|
||||
let name = name_table(&[(1, family), (2, "Regular"), (4, "Soft Hyphen Profile Sans Regular"), (6, "SoftHyphenProfileSans-Regular")])?;
|
||||
let mut builder = FontBuilder::new();
|
||||
builder.add_table(&cmap).ok()?;
|
||||
builder.add_raw(Tag::new(b"name"), name);
|
||||
for r in font.table_directory.table_records() {
|
||||
let tag = Tag::new(&r.tag().to_be_bytes());
|
||||
if tag != Tag::new(b"GPOS") && tag != Tag::new(b"kern") && !builder.contains(tag) {
|
||||
builder.add_raw(tag, font.table_data(r.tag())?.as_bytes());
|
||||
}
|
||||
}
|
||||
Some(builder.build())
|
||||
}
|
||||
|
||||
/// The bundled Source Sans 3 Regular as family [`PAIR_KERNING_FAMILY`], its `GPOS` replaced by two
|
||||
/// `kern` pairs, A followed by V and S followed by S: each takes 60 units off the first glyph's
|
||||
/// advance (the pair's first value record) and moves the second 100 units left (its second).
|
||||
|
||||
@@ -18,6 +18,267 @@ fn run(text: &str, size: f64) -> TextRun {
|
||||
TextRun { text: text.into(), style: style(size), inline: None }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optical_pairs_cross_paint_runs_and_manual_and_tracking_are_independent() {
|
||||
let st = CharStyle { font_family: "JetBrains Mono".into(), size: 100.0, optical_kerning: true, ..CharStyle::default() };
|
||||
let mut t = TextObject::point(Point::ZERO, "III", st);
|
||||
let advance = |t: &TextObject| layout(db(), t).glyphs.iter().map(|g| g.advance).collect::<Vec<_>>();
|
||||
let optical = advance(&t);
|
||||
let mut plain = t.clone();
|
||||
plain.runs[0].style.kerning = Some(0.0);
|
||||
let unkerned = advance(&plain);
|
||||
assert!((optical[0] - unkerned[0]).abs() > 0.1, "Optical changes the monospaced pair");
|
||||
edit::style_range(&mut t.runs, 1, 2, |s| s.fill = vectorcraft_doc::color::Paint::solid(vectorcraft_doc::color::Color::rgb(1.0, 0.0, 0.0)));
|
||||
let painted = advance(&t);
|
||||
assert!((painted[0] - optical[0]).abs() < 0.01, "paint does not interrupt Optical");
|
||||
edit::style_range(&mut t.runs, 0, 1, |s| s.kerning = Some(0.0));
|
||||
let manual = advance(&t);
|
||||
assert!((manual[0] - unkerned[0]).abs() < 0.01);
|
||||
assert!((manual[1] - optical[1]).abs() < 0.01, "the next pair remains Optical");
|
||||
edit::style_range(&mut t.runs, 0, 3, |s| s.tracking = 100.0);
|
||||
let tracked = advance(&t);
|
||||
assert!((tracked[0] - manual[0] - 10.0).abs() < 0.01);
|
||||
assert!((tracked[1] - manual[1] - 10.0).abs() < 0.01);
|
||||
|
||||
for text in ["I I", "I\u{301}"] {
|
||||
let sample = TextObject::point(Point::ZERO, text, CharStyle { kerning: None, ..t.runs[0].style.clone() });
|
||||
let mut zero = sample.clone();
|
||||
zero.runs[0].style.kerning = Some(0.0);
|
||||
assert_eq!(advance(&sample), advance(&zero), "spaces and marks inside a cluster have no Optical correction");
|
||||
}
|
||||
let mut vertical = TextObject::point(Point::ZERO, "II", CharStyle { kerning: None, ..t.runs[0].style.clone() });
|
||||
vertical.vertical = true;
|
||||
let mut vertical_zero = vertical.clone();
|
||||
vertical_zero.runs[0].style.kerning = Some(0.0);
|
||||
assert_eq!(advance(&vertical), advance(&vertical_zero), "vertical type keeps a neutral correction");
|
||||
let rtl = TextObject::point(Point::ZERO, "אב", CharStyle { kerning: None, ..t.runs[0].style.clone() });
|
||||
let rtl_advances = advance(&rtl);
|
||||
assert!(rtl_advances.len() >= 2 && rtl_advances.iter().all(|v| v.is_finite()));
|
||||
let mut rtl_zero = rtl.clone();
|
||||
rtl_zero.runs[0].style.kerning = Some(0.0);
|
||||
assert!((rtl_advances[1] - advance(&rtl_zero)[1]).abs() <= 25.0, "right-to-left Optical corrections remain bounded");
|
||||
let mut rtl_manual = rtl.clone();
|
||||
edit::style_range(&mut rtl_manual.runs, 0, 2, |s| s.kerning = Some(200.0));
|
||||
let mut rtl_pair_zero = rtl.clone();
|
||||
edit::style_range(&mut rtl_pair_zero.runs, 0, 2, |s| s.kerning = Some(0.0));
|
||||
let pair_advance = |t: &TextObject| layout(db(), t).glyphs.iter().find(|g| g.byte == 0).map(|g| g.advance).unwrap();
|
||||
assert!((pair_advance(&rtl_pair_zero) - pair_advance(&rtl_zero)).abs() < 0.01, "manual zero replaces the right-to-left Optical correction");
|
||||
assert!((pair_advance(&rtl_manual) - pair_advance(&rtl_pair_zero) - 20.0).abs() < 0.01, "manual spacing changes the adjacent right-to-left pair");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optical_pair_skips_an_invisible_soft_hyphen_with_an_outline() {
|
||||
use crate::test_fonts::{SOFT_HYPHEN_OUTLINE_FAMILY, soft_hyphen_outline_font};
|
||||
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
assert_eq!(db.add_font(soft_hyphen_outline_font().unwrap()), 1);
|
||||
let style = CharStyle { font_family: SOFT_HYPHEN_OUTLINE_FAMILY.into(), size: 100.0, optical_kerning: true, ..CharStyle::default() };
|
||||
let face = db.style_face(&style).unwrap();
|
||||
assert_ne!(face.glyph_for('\u{00AD}'), 0);
|
||||
assert_eq!(face.glyph_for('\u{00AD}'), face.glyph_for('-'));
|
||||
let paired = layout(&db, &TextObject::point(Point::ZERO, "AV", style.clone()));
|
||||
let separated = layout(&db, &TextObject::point(Point::ZERO, "A\u{00AD}V", style.clone()));
|
||||
let mut manual = style;
|
||||
manual.kerning = Some(0.0);
|
||||
let plain = layout(&db, &TextObject::point(Point::ZERO, "A\u{00AD}V", manual));
|
||||
|
||||
assert!((paired.glyphs[0].advance - plain.glyphs[0].advance).abs() > 0.1, "A-V has an Optical correction");
|
||||
// A keeps its uncorrected advance because neither pair across the invisible glyph is measured.
|
||||
assert!((separated.glyphs[0].advance - plain.glyphs[0].advance).abs() < 0.01);
|
||||
assert!(separated.glyphs.iter().filter(|g| g.byte == 1).all(|g| g.advance == 0.0), "the soft hyphen remains zero width if emitted");
|
||||
let v = |lay: &TextLayout| lay.glyphs.iter().find(|g| g.byte == 3).unwrap().origin.x;
|
||||
assert!((v(&separated) - v(&plain)).abs() < 0.01, "the soft hyphen adds no spacing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optical_pair_after_separate_combining_mark_uses_base_outline() {
|
||||
let st = CharStyle { font_family: "Source Sans 3".into(), size: 100.0, optical_kerning: true, ..CharStyle::default() };
|
||||
let advances = |text: &str, manual: bool| {
|
||||
let mut style = st.clone();
|
||||
if manual {
|
||||
style.kerning = Some(0.0);
|
||||
}
|
||||
layout(db(), &TextObject::point(Point::ZERO, text, style)).glyphs.iter().map(|g| (g.byte, g.advance)).collect::<Vec<_>>()
|
||||
};
|
||||
let plain = advances("II", false);
|
||||
let plain_zero = advances("II", true);
|
||||
let accented = advances("I\u{30d}I", false);
|
||||
let accented_zero = advances("I\u{30d}I", true);
|
||||
assert_eq!(accented.len(), 3, "the accent has its own glyph");
|
||||
assert_eq!(accented[0].0, accented[1].0, "the base and mark share a cluster");
|
||||
assert!((accented[0].1 - accented_zero[0].1).abs() < 0.01, "the base keeps its advance");
|
||||
let correction = accented[1].1 - accented_zero[1].1;
|
||||
assert!((correction - (plain[0].1 - plain_zero[0].1)).abs() < 0.01, "the marked cluster uses the base outline");
|
||||
assert!(correction.abs() > 0.1, "Optical adjusts the pair after the accent");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optical_pair_after_rtl_separate_mark_uses_base_outline() {
|
||||
let st = CharStyle { font_family: "Noto Sans Arabic".into(), size: 100.0, optical_kerning: true, ..CharStyle::default() };
|
||||
let shaped = |text: &str, manual: bool| {
|
||||
let mut style = st.clone();
|
||||
if manual {
|
||||
style.kerning = Some(0.0);
|
||||
}
|
||||
layout(db(), &TextObject::point(Point::ZERO, text, style)).glyphs.iter().map(|g| (g.byte, g.advance)).collect::<Vec<_>>()
|
||||
};
|
||||
let plain = shaped("بب", false);
|
||||
let plain_zero = shaped("بب", true);
|
||||
let marked = shaped("بَب", false);
|
||||
let marked_zero = shaped("بَب", true);
|
||||
let marked_cluster = marked.iter().filter(|g| g.0 == 0).collect::<Vec<_>>();
|
||||
assert_eq!(marked_cluster.len(), plain.iter().filter(|g| g.0 == 0).count() + 1, "the mark has its own glyph in the base cluster");
|
||||
assert_eq!(marked_cluster.first().unwrap().1, 0.0, "the cluster starts with a nonspacing glyph");
|
||||
assert!(marked_cluster.last().unwrap().1 > 0.0, "the base follows the mark");
|
||||
let correction = marked.iter().filter(|g| g.0 == 4).map(|g| g.1).sum::<f64>() - marked_zero.iter().filter(|g| g.0 == 4).map(|g| g.1).sum::<f64>();
|
||||
let plain_correction =
|
||||
plain.iter().filter(|g| g.0 == 2).map(|g| g.1).sum::<f64>() - plain_zero.iter().filter(|g| g.0 == 2).map(|g| g.1).sum::<f64>();
|
||||
assert!(plain_correction.abs() > 0.1, "Optical adjusts the unmarked pair");
|
||||
assert!((correction - plain_correction).abs() < 0.01, "the marked cluster uses the base outline");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recurring_optical_pairs_have_equal_corrections() {
|
||||
let st = CharStyle { font_family: "JetBrains Mono".into(), size: 100.0, optical_kerning: true, ..CharStyle::default() };
|
||||
let text = "II".repeat(50);
|
||||
let optical = layout(db(), &TextObject::point(Point::ZERO, &text, st.clone())).glyphs.iter().map(|g| g.advance).collect::<Vec<_>>();
|
||||
let zero = layout(db(), &TextObject::point(Point::ZERO, &text, CharStyle { kerning: Some(0.0), ..st }))
|
||||
.glyphs
|
||||
.iter()
|
||||
.map(|g| g.advance)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(optical.len(), zero.len());
|
||||
let correction = optical[0] - zero[0];
|
||||
assert!(correction.abs() > 0.1);
|
||||
for i in 1..optical.len() - 1 {
|
||||
assert_eq!(optical[i] - zero[i], correction, "pair {i} has the same correction");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metrics_pair_survives_transition_to_optical() {
|
||||
let st = CharStyle { font_family: "Source Sans 3".into(), size: 100.0, ..CharStyle::default() };
|
||||
let mut text = TextObject::point(Point::ZERO, "AVV", st);
|
||||
let advances = |text: &TextObject| layout(db(), text).glyphs.iter().map(|g| g.advance).collect::<Vec<_>>();
|
||||
let metrics = advances(&text);
|
||||
let mut unkerned = text.clone();
|
||||
unkerned.runs[0].style.kerning = Some(0.0);
|
||||
assert!((metrics[0] - advances(&unkerned)[0]).abs() > 0.1, "the font supplies an AV pair");
|
||||
edit::style_range(&mut text.runs, 1, 3, |s| s.optical_kerning = true);
|
||||
let mixed = advances(&text);
|
||||
assert!((mixed[0] - metrics[0]).abs() < 0.01, "the metrics AV pair crosses the mode change");
|
||||
let mut optical = TextObject::point(Point::ZERO, "VV", text.runs[1].style.clone());
|
||||
let optical_pair = advances(&optical)[0];
|
||||
optical.runs[0].style.kerning = Some(0.0);
|
||||
assert!((mixed[1] - optical_pair).abs() < 0.01, "the first Optical V keeps its next pair");
|
||||
assert!((mixed[1] - advances(&optical)[0]).abs() > 0.1, "Optical corrects the next pair");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manual_pair_before_optical_does_not_gain_font_kerning() {
|
||||
let st = CharStyle { font_family: "Source Sans 3".into(), size: 100.0, ..CharStyle::default() };
|
||||
let mut text = TextObject::point(Point::ZERO, "AVA", st);
|
||||
edit::style_range(&mut text.runs, 1, 2, |s| s.kerning = Some(0.0));
|
||||
edit::style_range(&mut text.runs, 2, 3, |s| s.optical_kerning = true);
|
||||
let mixed = layout(db(), &text).glyphs[1].advance;
|
||||
let mut unkerned = TextObject::point(Point::ZERO, "VA", text.runs[1].style.clone());
|
||||
unkerned.runs[0].style.kerning = Some(0.0);
|
||||
let manual = layout(db(), &unkerned).glyphs[0].advance;
|
||||
assert!((mixed - manual).abs() < 0.01, "manual V keeps its unkerned pair with Optical A");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optical_pair_uses_drawn_baseline() {
|
||||
let st = CharStyle { font_family: "JetBrains Mono".into(), size: 100.0, optical_kerning: true, ..CharStyle::default() };
|
||||
let mut text = TextObject::point(Point::ZERO, "II", st);
|
||||
let first = |text: &TextObject| layout(db(), text).glyphs[0].advance;
|
||||
let normal = first(&text);
|
||||
edit::style_range(&mut text.runs, 1, 2, |s| s.baseline_shift = 80.0);
|
||||
let shifted = first(&text);
|
||||
assert!((shifted - normal).abs() > 0.1, "a displaced outline changes the Optical correction");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optical_pair_skips_rotated_glyphs() {
|
||||
let st = CharStyle { font_family: "JetBrains Mono".into(), size: 100.0, optical_kerning: true, ..CharStyle::default() };
|
||||
let mut text = TextObject::point(Point::ZERO, "II", st);
|
||||
let first = |text: &TextObject| layout(db(), text).glyphs[0].advance;
|
||||
let unrotated = first(&text);
|
||||
let mut plain = text.clone();
|
||||
plain.runs[0].style.kerning = Some(0.0);
|
||||
assert!((unrotated - first(&plain)).abs() > 0.1, "the unrotated pair has an Optical correction");
|
||||
edit::style_range(&mut text.runs, 1, 2, |s| s.rotation = 20.0);
|
||||
let rotated = first(&text);
|
||||
let mut manual_zero = text.clone();
|
||||
edit::style_range(&mut manual_zero.runs, 0, 1, |s| s.kerning = Some(0.0));
|
||||
assert!((rotated - first(&manual_zero)).abs() < 0.01, "rotated pairs keep their uncorrected advance");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optical_shaping_keeps_manual_value_on_first_run() {
|
||||
let st = CharStyle { font_family: "Source Sans 3".into(), size: 100.0, optical_kerning: true, kerning: Some(0.0), ..CharStyle::default() };
|
||||
let mut text = TextObject::point(Point::ZERO, "AVA", st);
|
||||
let zero = layout(db(), &text).glyphs.iter().map(|g| g.advance).collect::<Vec<_>>();
|
||||
edit::style_range(&mut text.runs, 0, 1, |s| s.kerning = Some(120.0));
|
||||
let manual = layout(db(), &text).glyphs.iter().map(|g| g.advance).collect::<Vec<_>>();
|
||||
assert!((manual[0] - zero[0] - 12.0).abs() < 0.01, "the first run adds its manual value");
|
||||
assert!((manual[1] - zero[1]).abs() < 0.01, "the following run keeps its advance");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_and_vertical_segments_keep_their_spacing() {
|
||||
let st = CharStyle { font_family: "Source Sans 3".into(), size: 100.0, kerning: Some(100.0), ..CharStyle::default() };
|
||||
let plain = TextObject::point(Point::ZERO, "AVA", st);
|
||||
let advances = |text: &TextObject| layout(db(), text).glyphs.iter().map(|g| g.advance).collect::<Vec<_>>();
|
||||
let expected = advances(&plain);
|
||||
let mut split = plain.clone();
|
||||
edit::style_range(&mut split.runs, 1, 2, |s| s.fill = vectorcraft_doc::color::Paint::solid(vectorcraft_doc::color::Color::rgb(1.0, 0.0, 0.0)));
|
||||
assert_eq!(advances(&split), expected, "a non-Optical segment keeps the same spacing across runs");
|
||||
|
||||
let mut vertical = plain;
|
||||
vertical.vertical = true;
|
||||
let expected = advances(&vertical);
|
||||
edit::style_range(&mut vertical.runs, 0, 1, |s| s.kerning = Some(120.0));
|
||||
let manual = advances(&vertical);
|
||||
assert!((manual[0] - expected[0] - 2.0).abs() < 0.01, "vertical manual spacing changes only its own advance");
|
||||
edit::style_range(&mut vertical.runs, 0, 3, |s| s.optical_kerning = true);
|
||||
assert_eq!(advances(&vertical), manual, "vertical type keeps its spacing with Optical selected");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optical_spacing_works_without_font_pair_tables() {
|
||||
use crate::test_fonts::{NO_PAIR_KERNING_FAMILY, no_pair_kerning_font};
|
||||
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
assert_eq!(db.add_font(no_pair_kerning_font().unwrap()), 1);
|
||||
let st = CharStyle { font_family: NO_PAIR_KERNING_FAMILY.into(), size: 100.0, ..CharStyle::default() };
|
||||
let mut text = TextObject::point(Point::ZERO, "AV", st);
|
||||
let metrics = layout(&db, &text).glyphs[0].advance;
|
||||
text.runs[0].style.kerning = Some(0.0);
|
||||
let manual_zero = layout(&db, &text).glyphs[0].advance;
|
||||
assert!((metrics - manual_zero).abs() < 0.01, "the font has no automatic pair correction");
|
||||
text.runs[0].style.kerning = None;
|
||||
text.runs[0].style.optical_kerning = true;
|
||||
let optical = layout(&db, &text).glyphs[0].advance;
|
||||
assert!((optical - manual_zero).abs() > 0.1, "Optical uses outlines without a pair table");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manual_pair_positions_match_under_auto_and_optical_in_both_directions() {
|
||||
use crate::test_fonts::{RIGHT_TO_LEFT_OPTICAL_FAMILY, right_to_left_optical_font};
|
||||
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
assert_eq!(db.add_font(right_to_left_optical_font().unwrap()), 1);
|
||||
for (letters, family) in [("AV", "Source Sans 3"), ("אב", RIGHT_TO_LEFT_OPTICAL_FAMILY)] {
|
||||
let mut text = TextObject::point(Point::ZERO, letters, CharStyle { font_family: family.into(), size: 100.0, ..CharStyle::default() });
|
||||
edit::style_range(&mut text.runs, 0, letters.chars().next().unwrap().len_utf8(), |s| s.kerning = Some(200.0));
|
||||
let positions = |text: &TextObject| layout(&db, text).glyphs.iter().map(|g| (g.byte, g.origin, g.advance)).collect::<Vec<_>>();
|
||||
let auto = positions(&text);
|
||||
edit::style_range(&mut text.runs, 0, letters.len(), |s| s.optical_kerning = true);
|
||||
assert_eq!(positions(&text), auto, "{letters}: manual pair has the same positions in both modes");
|
||||
}
|
||||
}
|
||||
|
||||
fn area_path(text: &str, st: CharStyle, frame: &BezPath, justify: Justify) -> TextObject {
|
||||
let mut t = TextObject::point(Point::ZERO, text, st);
|
||||
t.kind = TextKind::Area { frame: PathData::from_bezpath(frame) };
|
||||
|
||||
@@ -719,5 +719,29 @@ fn proportional_metrics_kern_the_glyphs_they_respace() {
|
||||
};
|
||||
let (auto, manual) = (lay(true, None), lay(true, Some(0.0)));
|
||||
assert!(auto.iter().zip(&manual).any(|(a, b)| (a - b).abs() > 0.01), "kerned: {auto:?} {manual:?}");
|
||||
let optical = {
|
||||
let s = CharStyle { font_family: "Noto Sans CJK SC".into(), optical_kerning: true, ..st(true, None) };
|
||||
layout(&db, &vertical_styled("「カタカナ」とトップ、ヤッホー。", s)).glyphs.iter().map(|g| g.advance).collect::<Vec<_>>()
|
||||
};
|
||||
assert!(auto.iter().zip(&optical).all(|(a, b)| (a - b).abs() < 0.01), "vertical Optical uses font metrics: {auto:?} {optical:?}");
|
||||
assert!(lay(false, None).iter().all(|a| (a - 20.0).abs() < 0.01), "off: an em each, as before");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertical_optical_to_auto_keeps_the_fonts_pair() {
|
||||
use crate::test_fonts::{PAIR_KERNING_FAMILY, pair_kerning_font};
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
db.add_font(pair_kerning_font().unwrap());
|
||||
let style = CharStyle { font_family: PAIR_KERNING_FAMILY.into(), size: 100.0, ..CharStyle::default() };
|
||||
let positions =
|
||||
|t: &TextObject| layout(&db, t).glyphs.iter().map(|g| (g.advance, g.origin, g.xf.as_coeffs()[4], g.xf.as_coeffs()[5])).collect::<Vec<_>>();
|
||||
let auto = vertical_styled("AV", style.clone());
|
||||
let mut mixed = auto.clone();
|
||||
edit::style_range(&mut mixed.runs, 0, 1, |s| s.optical_kerning = true);
|
||||
let mut unkerned = auto.clone();
|
||||
edit::style_range(&mut unkerned.runs, 0, 1, |s| s.kerning = Some(0.0));
|
||||
let expected = positions(&auto);
|
||||
assert_eq!(expected.len(), 2);
|
||||
assert_ne!(expected, positions(&unkerned), "the font's A-V pair changes vertical positions");
|
||||
assert_eq!(positions(&mixed), expected, "vertical Optical keeps the A-V font pair across the style boundary");
|
||||
}
|
||||
|
||||
@@ -1587,7 +1587,7 @@ font-technology Variable Променливи
|
||||
Keep the pasted color as a new swatch with a numbered name. Запазване на поставения цвят като нов образец с номерирано име.
|
||||
Keep this row's colors (exclude) Запазване на цветовете на този ред (изключване)
|
||||
Keep: Запазване:
|
||||
Kerning (0 = Auto) Кернинг (0 = Автоматично)
|
||||
Kerning Кернинг
|
||||
Keyboard Increment Стъпка с клавиатурата
|
||||
Keyboard Shortcut Conflicts Конфликти на клавишни комбинации
|
||||
Keyboard Shortcut Set Набор клавишни комбинации
|
||||
@@ -2159,6 +2159,7 @@ font-technology Variable Променливи
|
||||
Open the URL in the web browser Отваряне на URL адреса в уеб браузъра
|
||||
Opened with {} note(s): {} Отворено с {} бележка(и): {}
|
||||
Open… Отваряне…
|
||||
Optical Оптично
|
||||
Optical Margin Alignment Подравняване по оптично поле
|
||||
Optimize for fast web view Оптимизиране за бърз уеб преглед
|
||||
Optimized Оптимизирано
|
||||
@@ -3761,6 +3762,7 @@ axis Both И двете
|
||||
Float Tool Group Плаваща група инструменти
|
||||
@msg no library `{id}` няма библиотека `{id}`
|
||||
@msg select some type first първо изберете текст
|
||||
@msg kerning must be a number, "auto", or "optical" Кернингът трябва да е число, „auto“ или „optical“
|
||||
@msg {_1} has no {_2} style {_1} няма стил {_2}
|
||||
@msg a layer couldn't be read слой не можа да бъде прочетен
|
||||
@msg it has more than one artboard има повече от една работна област
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1411,7 +1411,8 @@ lighting Light Svjetlo
|
||||
Fractional Widths Djelimične širine
|
||||
Hide Options Sakrij opcije
|
||||
Horizontal Scale % Vodoravna skala %
|
||||
Kerning (0 = Auto) Podrezivanje (0 = automatski)
|
||||
Kerning Podrezivanje
|
||||
Optical Optičko
|
||||
Leading Prored
|
||||
No Break Bez preloma
|
||||
Proportional Metrics Proporcionalna metrika
|
||||
@@ -4114,6 +4115,7 @@ imageTrace Outline Obris
|
||||
@plural {n} stroke|{n} strokes {n} potez|{n} poteza|{n} poteza
|
||||
@msg no library `{id}` nema biblioteke `{id}`
|
||||
@msg select some type first prvo odaberite tekst
|
||||
@msg kerning must be a number, "auto", or "optical" Podrezivanje mora biti broj, „auto” ili „optical”
|
||||
@msg the selection has no gradient fill odabir nema ispunu gradijentom
|
||||
@msg the selection has no gradient stroke odabir nema potez gradijentom
|
||||
@msg the selection has no pattern fill odabir nema ispunu uzorkom
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2046,8 +2046,8 @@
|
||||
Keep visible surfaces only when expanding Conserva només les superfícies visibles en expandir
|
||||
Keep: Conserva:
|
||||
Keeps the document editable: VectorCraft reopens the PDF with nothing lost. For files you'll edit again. Manté el document editable: VectorCraft reobre el PDF sense perdre res. Per a fitxers que editaràs de nou.
|
||||
Kerning (0 = Auto) Kerning (0 = automàtic)
|
||||
Key Color: Color clau:
|
||||
Kerning Kerning
|
||||
Keyboard Increment Increment de teclat
|
||||
Keyboard Shortcut Conflicts Conflictes de dreceres de teclat
|
||||
Keyboard Shortcut Set Conjunt de dreceres de teclat
|
||||
@@ -2740,6 +2740,7 @@
|
||||
Opens files in other apps Obre fitxers en altres aplicacions
|
||||
Opens network connections Obre connexions de xarxa
|
||||
Open… Obre…
|
||||
Optical Òptic
|
||||
Optical Margin Alignment Alineació òptica de marges
|
||||
Optimize for fast web view Optimitza per a la visualització web ràpida
|
||||
Optimized Optimitzat
|
||||
@@ -5354,6 +5355,7 @@
|
||||
@msg select paths or anchor points selecciona traçats o punts d’ancoratge
|
||||
@msg select paths to join selecciona traçats per unir
|
||||
@msg select some type first selecciona text primer
|
||||
@msg kerning must be a number, "auto", or "optical" El kerning ha de ser un nombre, «auto» o «optical»
|
||||
@msg Select text or click into text with the Type tool to insert glyphs Selecciona text o fes clic dins del text amb l’eina de text per inserir glifs
|
||||
@msg select the art to export first selecciona primer la il·lustració que vols exportar
|
||||
@msg select the art to repeat selecciona la il·lustració que vols repetir
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1436,7 +1436,6 @@ lighting Light Světlo
|
||||
Fractional Widths Zlomkové šířky
|
||||
Hide Options Skrýt možnosti
|
||||
Horizontal Scale % Vodorovná změna velikosti %
|
||||
Kerning (0 = Auto) Vyrovnání párů (0 = automaticky)
|
||||
Leading Proklad
|
||||
No Break Nezalamovat
|
||||
Proportional Metrics Proporcionální metrika
|
||||
@@ -3514,6 +3513,7 @@ theme Appearance Motiv
|
||||
Identify Objects By Identifikovat objekty podle
|
||||
Include SVG Code Zahrnout kód SVG
|
||||
Italian Italština
|
||||
Kerning Vyrovnání párů
|
||||
Keyboard Increment Krok klávesnice
|
||||
Language Options Možnosti jazyka
|
||||
Large Velké
|
||||
@@ -3556,6 +3556,7 @@ theme Appearance Motiv
|
||||
On Paste Při vložení
|
||||
On Screen Na obrazovce
|
||||
Open Documents As Tabs Otevírat dokumenty jako karty
|
||||
Optical Optické
|
||||
Options Možnosti
|
||||
Output All Blacks Accurately Ve výstupu všechny černé přesně
|
||||
Output All Blacks as Rich Black Ve výstupu všechny černé jako sytou černou
|
||||
@@ -4151,6 +4152,7 @@ imageTrace Outline Obrys
|
||||
# Status and error messages
|
||||
@msg no library `{id}` knihovna `{id}` neexistuje
|
||||
@msg select some type first nejprve vyberte text
|
||||
@msg kerning must be a number, "auto", or "optical" Vyrovnání párů musí být číslo, „auto“ nebo „optical“
|
||||
@msg the selection has no gradient fill výběr nemá přechodovou výplň
|
||||
@msg the selection has no gradient stroke výběr nemá přechodový tah
|
||||
@msg the selection has no pattern fill výběr nemá vzorkovou výplň
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1351,7 +1351,6 @@ lighting Light Lys
|
||||
Fractional Widths Fraktionerede bredder
|
||||
Hide Options Skjul indstillinger
|
||||
Horizontal Scale % Vandret skalering %
|
||||
Kerning (0 = Auto) Kerning (0 = automatisk)
|
||||
Leading Linjeafstand
|
||||
No Break Ingen ombrydning
|
||||
Proportional Metrics Proportionale metrikker
|
||||
@@ -3271,6 +3270,7 @@ theme Appearance Udseende
|
||||
Identify Objects By Identificér objekter ud fra
|
||||
Include SVG Code Medtag SVG-kode
|
||||
Italian Italiensk
|
||||
Kerning Kerning
|
||||
Keyboard Increment Tastaturtrin
|
||||
Language Options Sprogindstillinger
|
||||
Large Stor
|
||||
@@ -3299,6 +3299,7 @@ theme Appearance Udseende
|
||||
On Paste Ved indsættelse
|
||||
On Screen På skærmen
|
||||
Open Documents As Tabs Åbn dokumenter som faner
|
||||
Optical Optisk
|
||||
Options Indstillinger
|
||||
Output All Blacks Accurately Udskriv alle sorte nøjagtigt
|
||||
Output All Blacks as Rich Black Udskriv alle sorte som rich black
|
||||
@@ -3881,6 +3882,7 @@ imageTrace Outline Kontur
|
||||
@plural {n} stroke|{n} strokes {n} kontur|{n} konturer
|
||||
@msg no library `{id}` intet bibliotek `{id}`
|
||||
@msg select some type first markér først noget tekst
|
||||
@msg kerning must be a number, "auto", or "optical" Kerning skal være et tal, „auto“ eller „optical“
|
||||
@msg {_1} has no {_2} style {_1} har ingen {_2}-stil
|
||||
@msg a layer couldn't be read et lag kunne ikke læses
|
||||
@msg it has more than one artboard den har mere end én tegneflade
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1625,7 +1625,7 @@ file-filter Templates Vorlagen
|
||||
Keep the pasted color as a new swatch with a numbered name. Die eingefügte Farbe als neues Farbfeld mit nummeriertem Namen behalten.
|
||||
Keep this row's colors (exclude) Farben dieser Zeile beibehalten (ausschließen)
|
||||
Keep: Beibehalten:
|
||||
Kerning (0 = Auto) Kerning (0 = Auto)
|
||||
Kerning Kerning
|
||||
Keyboard Increment Schritte per Tastatur
|
||||
Keyboard Shortcut Conflicts Konflikte bei Tastaturbefehlen
|
||||
Keyboard Shortcut Set Tastaturbefehlssatz
|
||||
@@ -2172,6 +2172,7 @@ file-filter Templates Vorlagen
|
||||
Open the URL in the web browser Die URL im Webbrowser öffnen
|
||||
Opened with {} note(s): {} Mit {} Hinweis(en) geöffnet: {}
|
||||
Open… Öffnen…
|
||||
Optical Optisch
|
||||
Optical Margin Alignment Optischer Randausgleich
|
||||
Optimize for fast web view Für schnelle Webanzeige optimieren
|
||||
Optimized Optimiert
|
||||
@@ -3763,6 +3764,7 @@ theme Appearance Erscheinungsbild
|
||||
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
|
||||
@msg no library `{id}` keine Bibliothek `{id}`
|
||||
@msg select some type first wählen Sie zuerst Text aus
|
||||
@msg kerning must be a number, "auto", or "optical" Kerning muss eine Zahl, „auto“ oder „optical“ sein
|
||||
@msg the selection has no gradient fill die Auswahl hat keinen Flächenverlauf
|
||||
@msg the selection has no gradient stroke die Auswahl hat keinen Konturverlauf
|
||||
@msg the selection has no pattern fill die Auswahl hat kein Flächenmuster
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1543,7 +1543,7 @@ font-technology Variable Μεταβλητές
|
||||
Keep Plain Text Διατήρηση απλού κειμένου
|
||||
Keep this row's colors (exclude) Διατήρηση των χρωμάτων αυτής της γραμμής (εξαίρεση)
|
||||
Keep: Διατήρηση:
|
||||
Kerning (0 = Auto) Διαγραμμάτωση (0 = Αυτόματο)
|
||||
Kerning Διαγραμμάτωση
|
||||
Keyboard Increment Βήμα πληκτρολογίου
|
||||
Keyboard Shortcut Conflicts Διενέξεις συντομεύσεων πληκτρολογίου
|
||||
Keyboard Shortcut Set Σύνολο συντομεύσεων πληκτρολογίου
|
||||
@@ -2080,6 +2080,7 @@ axis Both Και τα δύο
|
||||
Dock Panel Αγκύρωση πάνελ
|
||||
@msg no library `{id}` Δεν υπάρχει βιβλιοθήκη με αναγνωριστικό `{id}`
|
||||
@msg select some type first Επιλέξτε πρώτα κάποιο κείμενο
|
||||
@msg kerning must be a number, "auto", or "optical" Η διαγραμμάτωση πρέπει να είναι αριθμός, «auto» ή «optical»
|
||||
@msg the selection has no gradient fill η επιλογή δεν έχει ντεγκραντέ γεμίσματος
|
||||
@msg the selection has no gradient stroke η επιλογή δεν έχει ντεγκραντέ περιγράμματος
|
||||
@msg the selection has no pattern fill η επιλογή δεν έχει μοτίβο γεμίσματος
|
||||
@@ -2886,6 +2887,7 @@ theme Appearance Εμφάνιση
|
||||
Open Swatch Library Άνοιγμα βιβλιοθήκης δειγμάτων χρωμάτων
|
||||
Open the URL in the web browser Άνοιγμα της διεύθυνσης στο πρόγραμμα περιήγησης
|
||||
Opened with {} note(s): {} Άνοιξε με {} σημειώσεις: {}
|
||||
Optical Οπτική
|
||||
Optical Margin Alignment Οπτική στοίχιση περιθωρίου
|
||||
Optimize for fast web view Βελτιστοποίηση για γρήγορη προβολή στον ιστό
|
||||
Options for Selection… Επιλογές για την επιλογή…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 164 and column 5.
|
@@ -1625,7 +1625,7 @@ font-technology Variable Variables
|
||||
Keep the pasted color as a new swatch with a numbered name. Conservar el color pegado como una muestra nueva con nombre numerado.
|
||||
Keep this row's colors (exclude) Conservar los colores de esta fila (excluir)
|
||||
Keep: Conservar:
|
||||
Kerning (0 = Auto) Interletraje (0 = auto)
|
||||
Kerning Interletraje
|
||||
Keyboard Increment Incremento de teclado
|
||||
Keyboard Shortcut Conflicts Conflictos de métodos abreviados de teclado
|
||||
Keyboard Shortcut Set Conjunto de métodos abreviados de teclado
|
||||
@@ -2214,6 +2214,7 @@ font-technology Variable Variables
|
||||
Open the URL in the web browser Abrir la URL en el navegador web
|
||||
Opened with {} note(s): {} Abierto con {} nota(s): {}
|
||||
Open… Abrir…
|
||||
Optical Óptico
|
||||
Optical Margin Alignment Alineación óptica de márgenes
|
||||
Optimize for fast web view Optimizar para vista web rápida
|
||||
Optimized Optimizado
|
||||
@@ -3843,6 +3844,7 @@ axis Both Ambos
|
||||
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
|
||||
@msg no library `{id}` no hay ninguna biblioteca `{id}`
|
||||
@msg select some type first selecciona primero algún texto
|
||||
@msg kerning must be a number, "auto", or "optical" El interletraje debe ser un número, «auto» u «optical»
|
||||
@msg the selection has no gradient fill la selección no tiene un degradado de relleno
|
||||
@msg the selection has no gradient stroke la selección no tiene un degradado de trazo
|
||||
@msg the selection has no pattern fill la selección no tiene un motivo de relleno
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1411,7 +1411,6 @@ lighting Light Valo
|
||||
Fractional Widths Murtolukuleveydet
|
||||
Hide Options Piilota asetukset
|
||||
Horizontal Scale % Vaakaskaalaus %
|
||||
Kerning (0 = Auto) Parivälistys (0 = automaattinen)
|
||||
Leading Riviväli
|
||||
No Break Ei katkoa
|
||||
Proportional Metrics Suhteelliset metriikat
|
||||
@@ -3453,6 +3452,7 @@ theme Appearance Ulkoasu
|
||||
Identify Objects By Tunnista objektit
|
||||
Include SVG Code Sisällytä SVG-koodi
|
||||
Italian Italia
|
||||
Kerning Parivälistys
|
||||
Keyboard Increment Näppäimistön lisäys
|
||||
Language Options Kieliasetukset
|
||||
Large Suuri
|
||||
@@ -3481,6 +3481,7 @@ theme Appearance Ulkoasu
|
||||
On Paste Liitettäessä
|
||||
On Screen Näytöllä
|
||||
Open Documents As Tabs Avaa asiakirjat välilehtinä
|
||||
Optical Optinen
|
||||
Options Asetukset
|
||||
Output All Blacks Accurately Tulosta kaikki mustat tarkasti
|
||||
Output All Blacks as Rich Black Tulosta kaikki mustat syvämustina
|
||||
@@ -4067,6 +4068,7 @@ imageTrace Outline Ääriviiva
|
||||
@plural {n} stroke|{n} strokes {n} viiva|{n} viivaa
|
||||
@msg no library `{id}` ei kirjastoa `{id}`
|
||||
@msg select some type first valitse ensin tekstiä
|
||||
@msg kerning must be a number, "auto", or "optical" Parivälistyksen on oltava luku, ”auto” tai ”optical”
|
||||
@msg the selection has no gradient fill valinnalla ei ole liukuväritäyttöä
|
||||
@msg the selection has no gradient stroke valinnalla ei ole liukuväriviivaa
|
||||
@msg the selection has no pattern fill valinnalla ei ole kuviotäyttöä
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1625,7 +1625,7 @@ font-technology Variable Variables
|
||||
Keep the pasted color as a new swatch with a numbered name. Conserver la couleur collée comme nouvelle nuance avec un nom numéroté.
|
||||
Keep this row's colors (exclude) Conserver les couleurs de cette ligne (exclure)
|
||||
Keep: Conserver :
|
||||
Kerning (0 = Auto) Crénage (0 = auto)
|
||||
Kerning Crénage
|
||||
Keyboard Increment Incrément clavier
|
||||
Keyboard Shortcut Conflicts Conflits de raccourcis clavier
|
||||
Keyboard Shortcut Set Jeu de raccourcis clavier
|
||||
@@ -2212,6 +2212,7 @@ font-technology Variable Variables
|
||||
Open the URL in the web browser Ouvrir l’URL dans le navigateur web
|
||||
Opened with {} note(s): {} Ouvert avec {} remarque(s) : {}
|
||||
Open… Ouvrir…
|
||||
Optical Optique
|
||||
Optical Margin Alignment Alignement optique des marges
|
||||
Optimize for fast web view Optimiser pour un affichage web rapide
|
||||
Optimized Optimisé
|
||||
@@ -3839,6 +3840,7 @@ axis Both Les deux
|
||||
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
|
||||
@msg no library `{id}` aucune bibliothèque `{id}`
|
||||
@msg select some type first sélectionnez d’abord du texte
|
||||
@msg kerning must be a number, "auto", or "optical" Le crénage doit être un nombre, « auto » ou « optical »
|
||||
@msg the selection has no gradient fill la sélection n’a pas de dégradé de fond
|
||||
@msg the selection has no gradient stroke la sélection n’a pas de dégradé de contour
|
||||
@msg the selection has no pattern fill la sélection n’a pas de motif de fond
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2044,8 +2044,8 @@ file-filter Templates Sablonok
|
||||
Keep visible surfaces only when expanding Kibontáskor csak a látható felületek megtartása
|
||||
Keep: Megtartás:
|
||||
Keeps the document editable: VectorCraft reopens the PDF with nothing lost. For files you'll edit again. Szerkeszthető marad a dokumentum: a VectorCraft veszteség nélkül nyitja meg újra a PDF-et. Azokhoz a fájlokhoz, amelyeket még szerkesztesz.
|
||||
Kerning (0 = Auto) Alávágás (0 = automatikus)
|
||||
Key Color: Kulcsszín:
|
||||
Kerning Alávágás
|
||||
Keyboard Increment Billentyűzetes lépésköz
|
||||
Keyboard Shortcut Conflicts Billentyűparancs-ütközések
|
||||
Keyboard Shortcut Set Billentyűparancs-készlet
|
||||
@@ -2688,6 +2688,7 @@ file-filter Templates Sablonok
|
||||
“{name}” already exists. Replace it? „{name}” már létezik. Felülírod?
|
||||
The name in the save panel was “{typed}”; VectorCraft added the file type's extension. Replacing the file overwrites its contents. A mentési panelen megadott név ez volt: „{typed}”, a VectorCraft pedig hozzáadta a fájltípus kiterjesztését. A fájl felülírásakor a tartalma elvész.
|
||||
Open… Megnyitás…
|
||||
Optical Optikai
|
||||
Optical Margin Alignment Optikai margóigazítás
|
||||
Optimize for fast web view Optimalizálás gyors webes megtekintéshez
|
||||
Optimized Optimalizált
|
||||
@@ -5044,6 +5045,7 @@ imageTrace Outline Körvonal
|
||||
@msg select paths or anchor points jelölj ki útvonalakat vagy horgonypontokat
|
||||
@msg select paths to join jelöld ki az összekapcsolandó útvonalakat
|
||||
@msg select some type first előbb jelölj ki szöveget
|
||||
@msg kerning must be a number, "auto", or "optical" Az alávágás szám, „auto” vagy „optical” lehet
|
||||
@msg the selection has no gradient fill a kijelölésnek nincs színátmenetes kitöltése
|
||||
@msg the selection has no gradient stroke a kijelölésnek nincs színátmenetes körvonala
|
||||
@msg the selection has no pattern fill a kijelölésnek nincs mintázatos kitöltése
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1351,7 +1351,6 @@ lighting Light Cahaya
|
||||
Fractional Widths Lebar fraksional
|
||||
Hide Options Sembunyikan opsi
|
||||
Horizontal Scale % Skala horizontal %
|
||||
Kerning (0 = Auto) Kerning (0 = Otomatis)
|
||||
Leading Leading
|
||||
No Break Tanpa pemenggalan
|
||||
Proportional Metrics Metrik proporsional
|
||||
@@ -3271,6 +3270,7 @@ theme Appearance Tampilan
|
||||
Identify Objects By Identifikasi objek berdasarkan
|
||||
Include SVG Code Sertakan kode SVG
|
||||
Italian Italia
|
||||
Kerning Kerning
|
||||
Keyboard Increment Inkremen papan ketik
|
||||
Language Options Opsi bahasa
|
||||
Large Besar
|
||||
@@ -3299,6 +3299,7 @@ theme Appearance Tampilan
|
||||
On Paste Saat menempel
|
||||
On Screen Di layar
|
||||
Open Documents As Tabs Buka dokumen sebagai tab
|
||||
Optical Optis
|
||||
Options Opsi
|
||||
Output All Blacks Accurately Keluarkan semua warna hitam secara akurat
|
||||
Output All Blacks as Rich Black Keluarkan semua warna hitam sebagai rich black
|
||||
@@ -3881,6 +3882,7 @@ imageTrace Outline Kontur
|
||||
@plural {n} stroke|{n} strokes {n} goresan
|
||||
@msg no library `{id}` tidak ada pustaka `{id}`
|
||||
@msg select some type first pilih beberapa teks terlebih dahulu
|
||||
@msg kerning must be a number, "auto", or "optical" Kerning harus berupa angka, “auto”, atau “optical”
|
||||
@msg {_1} has no {_2} style {_1} tidak memiliki gaya {_2}
|
||||
@msg a layer couldn't be read satu lapisan tidak dapat dibaca
|
||||
@msg it has more than one artboard memiliki lebih dari satu artboard
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2175 and column 5.
|
@@ -1650,7 +1650,7 @@ file-filter Templates Modelli
|
||||
Keep the pasted color as a new swatch with a numbered name. Mantieni il colore incollato come nuovo campione con un nome numerato.
|
||||
Keep this row's colors (exclude) Mantieni i colori di questa riga (escludi)
|
||||
Keep: Mantieni:
|
||||
Kerning (0 = Auto) Crenatura (0 = auto)
|
||||
Kerning Crenatura
|
||||
Keyboard Increment Incremento da tastiera
|
||||
Keyboard Shortcut Conflicts Conflitti tra scelte rapide da tastiera
|
||||
Keyboard Shortcut Set Set di scelte rapide da tastiera
|
||||
@@ -2241,6 +2241,7 @@ file-filter Templates Modelli
|
||||
Open the URL in the web browser Apri l'URL nel browser web
|
||||
Opened with {} note(s): {} Aperto con note ({}): {}
|
||||
Open… Apri…
|
||||
Optical Ottica
|
||||
Optical Margin Alignment Allineamento ottico dei margini
|
||||
Optimize for fast web view Ottimizza per visualizzazione web veloce
|
||||
Optimized Ottimizzato
|
||||
@@ -3883,6 +3884,7 @@ axis Both Entrambi
|
||||
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
|
||||
@msg no library `{id}` nessuna libreria `{id}`
|
||||
@msg select some type first seleziona prima del testo
|
||||
@msg kerning must be a number, "auto", or "optical" La crenatura deve essere un numero, «auto» o «optical»
|
||||
@msg the selection has no gradient fill la selezione non ha una sfumatura di riempimento
|
||||
@msg the selection has no gradient stroke la selezione non ha una sfumatura di traccia
|
||||
@msg the selection has no pattern fill la selezione non ha un motivo di riempimento
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1880,7 +1880,7 @@ font-technology Variable バリアブル
|
||||
Keep Plain Text テキストのみを保持
|
||||
Keep both sides the same width 両側を同じ幅に保つ
|
||||
Keep this row's colors (exclude) この行のカラーを保持 (除外)
|
||||
Kerning (0 = Auto) カーニング (0 = 自動)
|
||||
Kerning カーニング
|
||||
Keyboard Increment キー入力
|
||||
Keyboard Shortcut Conflicts キーボードショートカットの競合
|
||||
Keyboard Shortcut Set キーボードショートカットセット
|
||||
@@ -2346,6 +2346,7 @@ font-technology Variable バリアブル
|
||||
Open the URL in the web browser URL を Web ブラウザーで開く
|
||||
Optimize for fast web view Web ビュー用に最適化
|
||||
Optimized 最適化
|
||||
Optical オプティカル
|
||||
Options オプション
|
||||
Options for Selection… 選択項目のオプション…
|
||||
Options: オプション:
|
||||
@@ -4573,6 +4574,7 @@ swatch Bright Violet ビビッドバイオレット
|
||||
@msg choose a folder to export to 書き出し先のフォルダーを選択してください
|
||||
@msg the selection has no URL (or they differ) 選択したオブジェクトに URL がありません (または URL がそれぞれ異なります)
|
||||
@msg select some type first 先にテキストを選択してください
|
||||
@msg kerning must be a number, "auto", or "optical" カーニングは数値、「auto」または「optical」にしてください
|
||||
@msg the selection has no gradient fill 選択したオブジェクトに塗りのグラデーションがありません
|
||||
@msg the selection has no gradient stroke 選択したオブジェクトに線のグラデーションがありません
|
||||
@msg the selection has no pattern fill 選択したオブジェクトに塗りのパターンがありません
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1268,7 +1268,6 @@ lighting Light სინათლე
|
||||
Fractional Widths წილადი სიგანეები
|
||||
Hide Options პარამეტრების დამალვა
|
||||
Horizontal Scale % ჰორიზონტალური მასშტაბი %
|
||||
Kerning (0 = Auto) კერნინგი (0 = ავტო)
|
||||
Leading სტრიქონთა დაშორება
|
||||
No Break გადატანის გარეშე
|
||||
Proportional Metrics პროპორციული მეტრიკა
|
||||
@@ -3082,6 +3081,7 @@ theme Appearance იერსახე
|
||||
Identify Objects By ობიექტების იდენტიფიკაცია:
|
||||
Include SVG Code SVG კოდის ჩართვა
|
||||
Italian იტალიური
|
||||
Kerning კერნინგი
|
||||
Keyboard Increment კლავიატურის ბიჯი
|
||||
Language Options ენის პარამეტრები
|
||||
Large დიდი
|
||||
@@ -3110,6 +3110,7 @@ theme Appearance იერსახე
|
||||
On Paste ჩასმისას
|
||||
On Screen ეკრანზე
|
||||
Open Documents As Tabs დოკუმენტების ჩანართებად გახსნა
|
||||
Optical ოპტიკური
|
||||
Options პარამეტრები
|
||||
Output All Blacks Accurately ყველა შავის ზუსტად გამოტანა
|
||||
Output All Blacks as Rich Black ყველა შავის მდიდარ შავად გამოტანა
|
||||
@@ -3647,6 +3648,7 @@ font-technology Variable ცვლადი
|
||||
@plural {n} stroke|{n} strokes კონტური: {n}
|
||||
@msg no library `{id}` ბიბლიოთეკა `{id}` არ არსებობს
|
||||
@msg select some type first ჯერ მონიშნეთ ტექსტი
|
||||
@msg kerning must be a number, "auto", or "optical" კერნინგი უნდა იყოს რიცხვი, „auto“ ან „optical“
|
||||
@msg the selection has no gradient fill მონიშნულს შევსების გრადიენტი არ აქვს
|
||||
@msg the selection has no gradient stroke მონიშნულს კონტურის გრადიენტი არ აქვს
|
||||
@msg the selection has no pattern fill მონიშნულს შევსების პატერნი არ აქვს
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -1909,7 +1909,7 @@ font-technology Variable 가변
|
||||
Keep Plain Text 일반 텍스트 유지
|
||||
Keep both sides the same width 양쪽 너비를 같게 유지
|
||||
Keep this row's colors (exclude) 이 행의 색상 유지 (제외)
|
||||
Kerning (0 = Auto) 커닝 (0 = 자동)
|
||||
Kerning 커닝
|
||||
Keyboard Increment 키보드 증분
|
||||
Keyboard Shortcut Conflicts 단축키 충돌
|
||||
Keyboard Shortcut Set 단축키 세트
|
||||
@@ -2337,6 +2337,7 @@ font-technology Variable 가변
|
||||
Open the URL in the web browser 웹 브라우저에서 URL 열기
|
||||
Optimize for fast web view 빠른 웹 보기용으로 최적화
|
||||
Optimized 최적화됨
|
||||
Optical 광학
|
||||
Options 옵션
|
||||
Options for Selection… 선택 항목 옵션…
|
||||
Options: 옵션:
|
||||
@@ -4501,6 +4502,7 @@ theme Appearance 외관
|
||||
@msg choose a folder to export to 내보낼 폴더를 선택하세요
|
||||
@msg the selection has no URL (or they differ) 선택 항목에 URL이 없거나 서로 다릅니다
|
||||
@msg select some type first 먼저 문자를 선택하세요
|
||||
@msg kerning must be a number, "auto", or "optical" 커닝은 숫자, “auto” 또는 “optical”이어야 합니다
|
||||
@msg the selection has no gradient fill 선택 항목에 칠 그레이디언트가 없습니다
|
||||
@msg the selection has no gradient stroke 선택 항목에 선 그레이디언트가 없습니다
|
||||
@msg the selection has no pattern fill 선택 항목에 칠 패턴이 없습니다
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1365,7 +1365,6 @@ lighting Light Licht
|
||||
Fractional Widths Fractionele breedtes
|
||||
Hide Options Opties verbergen
|
||||
Horizontal Scale % Horizontale schaal %
|
||||
Kerning (0 = Auto) Kerning (0 = Auto)
|
||||
Leading Regelafstand
|
||||
No Break Niet afbreken
|
||||
Proportional Metrics Proportionele metingen
|
||||
@@ -3285,6 +3284,7 @@ theme Appearance Vormgeving
|
||||
Identify Objects By Objecten identificeren op
|
||||
Include SVG Code SVG-code opnemen
|
||||
Italian Italiaans
|
||||
Kerning Kerning
|
||||
Keyboard Increment Toetsenbordincrement
|
||||
Language Options Taalopties
|
||||
Large Groot
|
||||
@@ -3313,6 +3313,7 @@ theme Appearance Vormgeving
|
||||
On Paste Bij plakken
|
||||
On Screen Op scherm
|
||||
Open Documents As Tabs Documenten openen als tabbladen
|
||||
Optical Optisch
|
||||
Options Opties
|
||||
Output All Blacks Accurately Alle zwarttinten nauwkeurig uitvoeren
|
||||
Output All Blacks as Rich Black Alle zwarttinten uitvoeren als rijk zwart
|
||||
@@ -3895,6 +3896,7 @@ imageTrace Outline Contour
|
||||
@plural {n} stroke|{n} strokes {n} lijn|{n} lijnen
|
||||
@msg no library `{id}` geen bibliotheek `{id}`
|
||||
@msg select some type first selecteer eerst wat tekst
|
||||
@msg kerning must be a number, "auto", or "optical" Kerning moet een getal, ‘auto’ of ‘optical’ zijn
|
||||
@msg {_1} has no {_2} style {_1} heeft geen {_2}stijl
|
||||
@msg a layer couldn't be read een laag kon niet worden gelezen
|
||||
@msg it has more than one artboard het heeft meer dan één tekengebied
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1350,7 +1350,6 @@ lighting Light Światło
|
||||
Fractional Widths Szerokości ułamkowe
|
||||
Hide Options Ukryj opcje
|
||||
Horizontal Scale % Skala pozioma %
|
||||
Kerning (0 = Auto) Kerning (0 = auto)
|
||||
Leading Interlinia
|
||||
No Break Bez podziału
|
||||
Proportional Metrics Metryka proporcjonalna
|
||||
@@ -3392,6 +3391,7 @@ theme Appearance Wygląd
|
||||
Identify Objects By Identyfikuj obiekty według
|
||||
Include SVG Code Dołącz kod SVG
|
||||
Italian Włoski
|
||||
Kerning Kerning
|
||||
Keyboard Increment Przyrost klawiatury
|
||||
Language Options Opcje języka
|
||||
Large Duże
|
||||
@@ -3420,6 +3420,7 @@ theme Appearance Wygląd
|
||||
On Paste Przy wklejaniu
|
||||
On Screen Na ekranie
|
||||
Open Documents As Tabs Otwieraj dokumenty jako karty
|
||||
Optical Optyczny
|
||||
Options Opcje
|
||||
Output All Blacks Accurately Wyprowadzaj wszystkie czernie dokładnie
|
||||
Output All Blacks as Rich Black Wyprowadzaj wszystkie czernie jako czerń głęboką
|
||||
@@ -3975,6 +3976,7 @@ imageTrace Outline Kontur
|
||||
@plural {n} stroke|{n} strokes {n} obrys|{n} obrysy|{n} obrysów
|
||||
@msg no library `{id}` brak biblioteki `{id}`
|
||||
@msg select some type first najpierw zaznacz tekst
|
||||
@msg kerning must be a number, "auto", or "optical" Kerning musi być liczbą, „auto” lub „optical”
|
||||
@msg the selection has no gradient fill zaznaczenie nie ma gradientu wypełnienia
|
||||
@msg the selection has no gradient stroke zaznaczenie nie ma gradientu obrysu
|
||||
@msg the selection has no pattern fill zaznaczenie nie ma wzorka wypełnienia
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1400,7 +1400,7 @@
|
||||
Keep Plain Text Manter Texto Simples
|
||||
Keep both sides the same width Manter os dois lados com a mesma largura
|
||||
Keep this row's colors (exclude) Manter as cores desta linha (excluir)
|
||||
Kerning (0 = Auto) Kerning (0 = Automático)
|
||||
Kerning Kerning
|
||||
Keyboard Increment Incremento do Teclado
|
||||
Keyboard Shortcut Conflicts Conflitos de Atalhos de Teclado
|
||||
Keyboard Shortcut Set Conjunto de Atalhos de Teclado
|
||||
@@ -1810,6 +1810,7 @@
|
||||
Open password: Senha de abertura:
|
||||
Open the URL in the web browser Abra a URL no navegador da web
|
||||
Open… Abrir…
|
||||
Optical Óptico
|
||||
Optical Margin Alignment Alinhamento de Margem Óptica
|
||||
Optimize for fast web view Otimizar para visualização rápida na web
|
||||
Optimized Otimizado
|
||||
@@ -3430,6 +3431,7 @@ axis Both Ambos
|
||||
Drag to the window's left edge to dock Arraste até a borda esquerda da janela para encaixar
|
||||
@msg no library `{id}` nenhuma biblioteca `{id}`
|
||||
@msg select some type first selecione algum texto primeiro
|
||||
@msg kerning must be a number, "auto", or "optical" O kerning deve ser um número, “auto” ou “optical”
|
||||
@msg the selection has no gradient fill a seleção não tem gradiente de preenchimento
|
||||
@msg the selection has no gradient stroke a seleção não tem gradiente de traçado
|
||||
@msg the selection has no pattern fill a seleção não tem padrão de preenchimento
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1648,7 +1648,7 @@ file-filter Templates Шаблоны
|
||||
Keep the pasted color as a new swatch with a numbered name. Сохранять вставленный цвет как новый образец с номерным именем.
|
||||
Keep this row's colors (exclude) Сохранить цвета этой строки (исключить)
|
||||
Keep: Сохранять:
|
||||
Kerning (0 = Auto) Кернинг (0 = авто)
|
||||
Kerning Кернинг
|
||||
Keyboard Increment Приращение с клавиатуры
|
||||
Keyboard Shortcut Conflicts Конфликты сочетаний клавиш
|
||||
Keyboard Shortcut Set Набор сочетаний клавиш
|
||||
@@ -2237,6 +2237,7 @@ file-filter Templates Шаблоны
|
||||
Open the URL in the web browser Открыть URL в веб-браузере
|
||||
Opened with {} note(s): {} Открыто с примечаниями ({}) : {}
|
||||
Open… Открыть…
|
||||
Optical Оптический
|
||||
Optical Margin Alignment Оптическое выравнивание полей
|
||||
Optimize for fast web view Оптимизировать для быстрого просмотра в web
|
||||
Optimized Оптимизировано
|
||||
@@ -3879,6 +3880,7 @@ axis Both Оба
|
||||
# names and values in backticks or code (`colorModel rgb`, `document.export`) stay as they are.
|
||||
@msg no library `{id}` нет библиотеки `{id}`
|
||||
@msg select some type first сначала выделите текст
|
||||
@msg kerning must be a number, "auto", or "optical" Кернинг должен быть числом, «auto» или «optical»
|
||||
@msg the selection has no gradient fill у выделения нет градиентной заливки
|
||||
@msg the selection has no gradient stroke у выделения нет градиентной обводки
|
||||
@msg the selection has no pattern fill у выделения нет узорной заливки
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1587,7 +1587,7 @@ font-technology Variable Variabla
|
||||
Keep the pasted color as a new swatch with a numbered name. Behåll den inklistrade färgen som en ny färgruta med numrerat namn.
|
||||
Keep this row's colors (exclude) Behåll radens färger (undanta)
|
||||
Keep: Behåll:
|
||||
Kerning (0 = Auto) Kerning (0 = auto)
|
||||
Kerning Kerning
|
||||
Keyboard Increment Tangentbordssteg
|
||||
Keyboard Shortcut Conflicts Konflikter mellan kortkommandon
|
||||
Keyboard Shortcut Set Uppsättning kortkommandon
|
||||
@@ -2159,6 +2159,7 @@ font-technology Variable Variabla
|
||||
Open the URL in the web browser Öppna URL:en i webbläsaren
|
||||
Opened with {} note(s): {} Öppnad med {} anteckning(ar): {}
|
||||
Open… Öppna…
|
||||
Optical Optisk
|
||||
Optical Margin Alignment Optisk marginaljustering
|
||||
Optimize for fast web view Optimera för snabb webbvisning
|
||||
Optimized Optimerad
|
||||
@@ -3761,6 +3762,7 @@ axis Both Båda
|
||||
Float Tool Group Gör verktygsgruppen flytande
|
||||
@msg no library `{id}` inget bibliotek `{id}`
|
||||
@msg select some type first markera någon text först
|
||||
@msg kerning must be a number, "auto", or "optical" Kerning måste vara ett tal, ”auto” eller ”optical”
|
||||
@msg the selection has no gradient fill markeringen har ingen fyllnadsgradient
|
||||
@msg the selection has no gradient stroke markeringen har ingen konturgradient
|
||||
@msg the selection has no pattern fill markeringen har inget fyllnadsmönster
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -576,7 +576,7 @@ fn character_panel_terms_agree_within_each_catalog() {
|
||||
for l in LANGUAGES.iter().filter(|l| !l.source.is_empty()) {
|
||||
let (entries, _) = parse_entries(l.source);
|
||||
let row = |src: &str| entries.iter().find(|(ctx, s, _)| ctx.is_empty() && s == src).map(|(_, _, tr)| tr.to_lowercase());
|
||||
let kerning = row("Kerning (0 = Auto)").map(|tr| tr.split(" (").next().unwrap_or_default().trim().to_string());
|
||||
let kerning = row("Kerning");
|
||||
let leading = row("Leading");
|
||||
let tracking = row("Tracking");
|
||||
for (a, b) in [(&kerning, &leading), (&kerning, &tracking), (&leading, &tracking)] {
|
||||
|
||||
@@ -1474,7 +1474,7 @@ font-technology Variable Değişken
|
||||
Keep the pasted color as a new swatch with a numbered name. Yapıştırılan rengi numaralı bir adla yeni bir renk örneği olarak koruyun.
|
||||
Keep this row's colors (exclude) Bu satırın renklerini koru (hariç tut)
|
||||
Keep: Sakla:
|
||||
Kerning (0 = Auto) Karakter Aralığı (0 = Otomatik)
|
||||
Kerning Karakter aralığı
|
||||
Keyboard Increment Klavye Artışı
|
||||
Keyboard Shortcut Conflicts Klavye Kısayolu Çakışmaları
|
||||
Keyboard Shortcut Set Klavye Kısayol Seti
|
||||
@@ -2038,6 +2038,7 @@ shape Open Açık
|
||||
Open the URL in the web browser URL'yi web tarayıcısında açın
|
||||
Opened with {} note(s): {} {} notla açıldı: {}
|
||||
Open… Aç…
|
||||
Optical Optik
|
||||
Optical Margin Alignment Optik Kenar Boşluğu Hizalaması
|
||||
Optimize for fast web view Hızlı web görünümü için optimize edin
|
||||
Optimized Optimize edilmiş
|
||||
@@ -3655,6 +3656,7 @@ axis Both Her İkisi
|
||||
Float Tool Group Araç Grubunu Yüzer Yap
|
||||
@msg no library `{id}` `{id}` kütüphanesi yok
|
||||
@msg select some type first önce bir tür seçin
|
||||
@msg kerning must be a number, "auto", or "optical" Karakter aralığı sayı, “auto” veya “optical” olmalıdır
|
||||
@msg the selection has no gradient fill seçimin degrade dolgusu yok
|
||||
@msg the selection has no gradient stroke seçimin degrade konturu yok
|
||||
@msg the selection has no pattern fill seçimin desen dolgusu yok
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -1644,7 +1644,7 @@ file-filter Templates Шаблони
|
||||
Keep the pasted color as a new swatch with a numbered name. Зберігати вставлений колір як новий зразок із номерним ім’ям.
|
||||
Keep this row's colors (exclude) Зберегти кольори цього рядка (виключити)
|
||||
Keep: Зберігати:
|
||||
Kerning (0 = Auto) Кернінг (0 = авто)
|
||||
Kerning Кернінг
|
||||
Keyboard Increment Крок клавіатури
|
||||
Keyboard Shortcut Conflicts Конфлікти поєднань клавіш
|
||||
Keyboard Shortcut Set Набір клавіш
|
||||
@@ -2233,6 +2233,7 @@ file-filter Templates Шаблони
|
||||
Open the URL in the web browser Відкрити URL у веб-браузері
|
||||
Opened with {} note(s): {} Відкрито із примітками ({}) : {}
|
||||
Open… Відкрити…
|
||||
Optical Оптичний
|
||||
Optical Margin Alignment Оптичне вирівнювання полів
|
||||
Optimize for fast web view Оптимізувати для швидкого перегляду web
|
||||
Optimized Оптимізовано
|
||||
@@ -3867,6 +3868,7 @@ axis Both Обидві
|
||||
Float Tool Group Від’єднати групу інструментів
|
||||
@msg no library `{id}` немає бібліотеки `{id}`
|
||||
@msg select some type first спочатку виділіть текст
|
||||
@msg kerning must be a number, "auto", or "optical" Кернінг має бути числом, «auto» або «optical»
|
||||
@msg the selection has no gradient fill виділення не має градієнтної заливки
|
||||
@msg the selection has no gradient stroke виділення не має градієнтного обведення
|
||||
@msg the selection has no pattern fill виділення не має візерункової заливки
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 178 and column 5.
|
@@ -1922,8 +1922,8 @@ file-filter Templates Mẫu
|
||||
Keep this row's colors (exclude) Giữ màu của hàng này (loại trừ)
|
||||
Keep visible surfaces only when expanding Chỉ giữ bề mặt hiển thị khi mở rộng
|
||||
Keep: Giữ:
|
||||
Kerning (0 = Auto) Kerning (0 = Tự động)
|
||||
Key Color: Màu chủ:
|
||||
Kerning Kerning
|
||||
Keyboard Increment Bước nhảy bàn phím
|
||||
Keyboard Shortcut Conflicts Xung đột phím tắt
|
||||
Keyboard Shortcut Set Bộ phím tắt
|
||||
@@ -2499,6 +2499,7 @@ file-filter Templates Mẫu
|
||||
“{name}” already exists. Replace it? “{name}” đã tồn tại. Thay thế tệp này?
|
||||
The name in the save panel was “{typed}”; VectorCraft added the file type's extension. Replacing the file overwrites its contents. Tên trong bảng lưu là “{typed}”, và VectorCraft đã thêm phần mở rộng của loại tệp. Thay thế tệp sẽ ghi đè nội dung của tệp.
|
||||
Open… Mở…
|
||||
Optical Quang học
|
||||
Optical Margin Alignment Căn lề quang học
|
||||
Optimize for fast web view Tối ưu để xem web nhanh
|
||||
Optimized Đã tối ưu
|
||||
@@ -4696,6 +4697,7 @@ theme Appearance Giao diện
|
||||
@msg select paths or anchor points chọn đường dẫn hoặc điểm neo
|
||||
@msg select paths to join hãy chọn đường dẫn để nối
|
||||
@msg select some type first hãy chọn chữ trước
|
||||
@msg kerning must be a number, "auto", or "optical" Kerning phải là số, “auto” hoặc “optical”
|
||||
@msg the selection has no gradient fill vùng chọn không có tô chuyển sắc
|
||||
@msg the selection has no gradient stroke vùng chọn không có nét chuyển sắc
|
||||
@msg the selection has no pattern fill vùng chọn không có tô họa tiết
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 200 and column 5.
|
@@ -1575,7 +1575,7 @@ file-filter Templates 模板
|
||||
Keep the pasted color as a new swatch with a numbered name. 将粘贴的颜色保留为新色板,并以编号命名。
|
||||
Keep this row's colors (exclude) 保留此行的颜色 (排除)
|
||||
Keep: 保留:
|
||||
Kerning (0 = Auto) 字偶距 (0 = 自动)
|
||||
Kerning 字偶距
|
||||
Keyboard Increment 键盘增量
|
||||
Keyboard Shortcut Conflicts 键盘快捷键冲突
|
||||
Keyboard Shortcut Set 键盘快捷键组
|
||||
@@ -2113,6 +2113,7 @@ file-filter Templates 模板
|
||||
Opened with {} note(s): {} 已打开,附 {} 条附注:{}
|
||||
OpenType OpenType
|
||||
Open… 打开…
|
||||
Optical 视觉
|
||||
Optical Margin Alignment 视觉边界对齐
|
||||
Optimize for fast web view 优化快速 Web 查看
|
||||
Optimized 优化
|
||||
@@ -4290,6 +4291,7 @@ imageTrace Outline 轮廓
|
||||
@msg choose a folder to export to 请选择要导出到的文件夹
|
||||
@msg the selection has no URL (or they differ) 所选对象没有 URL (或其 URL 各不相同)
|
||||
@msg select some type first 请先选择一些文字
|
||||
@msg kerning must be a number, "auto", or "optical" 字偶距必须是数字、“auto”或“optical”
|
||||
@msg the selection has no gradient fill 所选对象没有渐变填色
|
||||
@msg the selection has no gradient stroke 所选对象没有渐变描边
|
||||
@msg the selection has no pattern fill 所选对象没有图案填色
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1614,7 +1614,7 @@ file-filter Templates 範本
|
||||
Keep the pasted color as a new swatch with a numbered name. 將貼上的顏色保留為新色票,並以編號命名。
|
||||
Keep this row's colors (exclude) 保留此列的顏色 (排除)
|
||||
Keep: 保留:
|
||||
Kerning (0 = Auto) 字距微調 (0 = 自動)
|
||||
Kerning 字距微調
|
||||
Keyboard Increment 鍵盤增量
|
||||
Keyboard Shortcut Conflicts 鍵盤快速鍵衝突
|
||||
Keyboard Shortcut Set 鍵盤快速鍵組
|
||||
@@ -2228,6 +2228,7 @@ file-filter Templates 範本
|
||||
@msg choose a folder to export to 請選擇要匯出至的資料夾
|
||||
@msg the selection has no URL (or they differ) 選取範圍沒有 URL (或其 URL 各不相同)
|
||||
@msg select some type first 請先選取一些文字
|
||||
@msg kerning must be a number, "auto", or "optical" 字距微調必須是數字、「auto」或「optical」
|
||||
@msg the selection has no gradient fill 選取範圍沒有漸層填色
|
||||
@msg the selection has no gradient stroke 選取範圍沒有漸層筆畫
|
||||
@msg the selection has no pattern fill 選取範圍沒有圖樣填色
|
||||
@@ -3047,6 +3048,7 @@ file-filter Templates 範本
|
||||
Opened with {} note(s): {} 已開啟,附 {} 則附註:{}
|
||||
OpenType OpenType
|
||||
Open… 開啟…
|
||||
Optical 視覺
|
||||
Optical Margin Alignment 視覺邊界對齊
|
||||
Optimize for fast web view 最佳化以便快速網頁檢視
|
||||
Optimized 最佳化
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -222,7 +222,7 @@ fn char_cmd(app: &mut VectorcraftApp, cmd: &str, p: Value) {
|
||||
range_style(app, p);
|
||||
}
|
||||
|
||||
/// Set the Kerning field's `value` (a number or "auto"). At a caret in text the Type tool edits
|
||||
/// Set the Kerning field's `value` (a number, "auto", or "optical"). At a caret in text the Type tool edits
|
||||
/// it is the pair kerning after the character before the caret, as Alt+←/→ steps it
|
||||
/// (`type.step`); nowhere to set at the start of the text. Else it is a character attribute like
|
||||
/// the others: the selected range, or all the text.
|
||||
@@ -244,6 +244,37 @@ fn set_kerning(app: &mut VectorcraftApp, value: Value) {
|
||||
app.run("text.setRangeStyle", json!({"id": id.0, "start": start, "end": a, "kerning": value})).ok();
|
||||
}
|
||||
|
||||
/// The pair before the caret, in 1/1000 em, using the layout the canvas draws.
|
||||
fn caret_kerning(app: &VectorcraftApp, ui: &Ui) -> Option<(f64, bool)> {
|
||||
let (id, a, b) = text_editing(app)?;
|
||||
if a != b {
|
||||
return None;
|
||||
}
|
||||
let NodeKind::Text(t) = &app.session.active()?.doc.node(id)?.kind else { return None };
|
||||
let text = t.plain_text();
|
||||
let start = vectorcraft_text::edit::prev_char(&text, a);
|
||||
if start >= a {
|
||||
return None;
|
||||
}
|
||||
let style = vectorcraft_text::edit::style_at(&t.runs, start + 1);
|
||||
if let Some(v) = style.kerning {
|
||||
return Some((v, false));
|
||||
}
|
||||
let state = app.session.active()?;
|
||||
let key = (state.uid, state.revision, id, a);
|
||||
let slot = ui.scope_id().with("caret-kerning");
|
||||
if let Some((cached_key, value)) = ui.ctx().data(|d| d.get_temp::<((u64, u64, NodeId, usize), f64)>(slot))
|
||||
&& cached_key == key
|
||||
{
|
||||
return Some((value, true));
|
||||
}
|
||||
let original = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t);
|
||||
let value = vectorcraft_engine::cmd::effective_kerning(vectorcraft_text::FontDb::global(), t, &original, start, a);
|
||||
let value = value.round();
|
||||
ui.ctx().data_mut(|d| d.insert_temp(slot, (key, value)));
|
||||
Some((value, true))
|
||||
}
|
||||
|
||||
/// The faces Type › Bold and Type › Italic switch to.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Face {
|
||||
@@ -637,14 +668,33 @@ pub(crate) fn metrics_grid(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, s: &
|
||||
match widgets::spin_field_auto(ui, "ch-lead", leading, Some(auto), unit, fw, 1.0, 0.1, &SIZE_PRESETS) {
|
||||
Some(SpinPick::Value(v)) => style(app, json!({"leading": v})),
|
||||
Some(SpinPick::Auto) => style(app, json!({"leading": "auto"})),
|
||||
Some(SpinPick::Optical) => {}
|
||||
None => {}
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
cell(ui, "VA", tl!("Kerning (0 = Auto)"), |ui| {
|
||||
let kerning = shared(app, |s| s.kerning.unwrap_or(0.0));
|
||||
if let Some(v) = widgets::spin_plain(ui, "ch-kern", kerning, "", 0, fw, 10.0, -1000.0, &TRACKING_PRESETS) {
|
||||
set_kerning(app, if v == 0.0 { json!("auto") } else { json!(v) });
|
||||
cell(ui, "VA", tl!("Kerning"), |ui| {
|
||||
let mode = shared(app, |s| (s.optical_kerning, s.kerning));
|
||||
let caret = text_editing(app).is_some_and(|(_, a, b)| a == b);
|
||||
let at_caret = if caret { caret_kerning(app, ui) } else { None };
|
||||
let kerning = at_caret.map(|(v, _)| v).or_else(|| mode.map(|(_, manual)| manual.unwrap_or(0.0)));
|
||||
let automatic = mode.map(|(optical, manual)| !optical && manual.is_none());
|
||||
let optical = mode.map(|(optical, _)| optical);
|
||||
match widgets::spin_kerning(
|
||||
ui,
|
||||
"ch-kern",
|
||||
kerning,
|
||||
automatic,
|
||||
optical,
|
||||
tl!("Optical"),
|
||||
at_caret.is_some_and(|(_, auto)| auto),
|
||||
fw,
|
||||
&TRACKING_PRESETS,
|
||||
) {
|
||||
Some(SpinPick::Value(v)) => set_kerning(app, json!(v)),
|
||||
Some(SpinPick::Auto) => set_kerning(app, json!("auto")),
|
||||
Some(SpinPick::Optical) => set_kerning(app, json!("optical")),
|
||||
None => {}
|
||||
}
|
||||
});
|
||||
cell(ui, "VA↔", tl!("Tracking"), |ui| {
|
||||
|
||||
@@ -130,6 +130,38 @@ fn kerning_at_a_caret_sets_the_pair_not_the_whole_text() {
|
||||
assert_eq!((kerning(&app, 1), kerning(&app, 2), kerning(&app, 3)), (Some(-50.0), Some(30.0), Some(30.0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_kerning_setter_and_automatic_pair_value() {
|
||||
let german = crate::i18n::Lang::from_code("de").unwrap();
|
||||
assert!(crate::i18n::has(german, "Optical"));
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
|
||||
app.session.select_tool("type", Default::default()).unwrap();
|
||||
app.run("text.create", json!({"x": 20, "y": 50, "text": "III", "size": 100, "font": "JetBrains Mono"})).unwrap();
|
||||
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
|
||||
app.session.set_tool_option("select", &json!({"start": 1, "end": 1}));
|
||||
set_kerning(&mut app, json!("optical"));
|
||||
assert!(text_style(&app).unwrap().0.optical_kerning);
|
||||
let ctx = egui::Context::default();
|
||||
let mut shown = None;
|
||||
ctx.run_ui(Default::default(), |ui| {
|
||||
shown = caret_kerning(&app, ui);
|
||||
})
|
||||
.textures_delta
|
||||
.clear();
|
||||
let (value, automatic) = shown.unwrap();
|
||||
assert!(automatic && value != 0.0);
|
||||
set_kerning(&mut app, json!(50));
|
||||
ctx.run_ui(Default::default(), |ui| {
|
||||
shown = caret_kerning(&app, ui);
|
||||
})
|
||||
.textures_delta
|
||||
.clear();
|
||||
assert_eq!(shown, Some((50.0, false)));
|
||||
app.session.set_tool_option("select", &json!({"start": 0, "end": 2}));
|
||||
assert_eq!(shared(&app, |s| (s.optical_kerning, s.kerning)), None, "a mixed range shows a blank Kerning field");
|
||||
}
|
||||
|
||||
/// Highlight Kerning and Highlight Tracking show the Character panel and give its field the
|
||||
/// keyboard focus with the value selected: what is typed replaces it, and Enter applies it.
|
||||
/// Highlight Font Name opens the font family menu with its search field focused. Without
|
||||
|
||||
@@ -51,7 +51,7 @@ pub(crate) use numeric::{ANY, round_to, set_bare_numbers_are_points, typed_unit}
|
||||
pub use numeric::{
|
||||
SpinPick, mixed_field, mixed_field_named, num_field, num_field_named, num_field_within, opt_field, opt_field_named, range_field,
|
||||
range_field_named, range_field_stepped, size_field, size_field_named, slider, spin_field, spin_field_auto, spin_field_auto_named,
|
||||
spin_field_named, spin_field_stops_named, spin_plain, spin_plain_named,
|
||||
spin_field_named, spin_field_stops_named, spin_kerning, spin_plain, spin_plain_named,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use numeric::{decimal_point, share_of, spinner_step, step_through, step_to_multiple, within_a_millionth};
|
||||
|
||||
@@ -346,7 +346,7 @@ pub fn range_field_named(
|
||||
let places = decimals.min(6) as i32;
|
||||
let value = value.into().map(|v| within(v, &range));
|
||||
let label = take_previous_label(ui);
|
||||
bounded_field(ui, id, label, name, value, suffix, decimals, width, &range, None).map(|v| within(round_to(v, places), &range))
|
||||
bounded_field(ui, id, label, name, value, suffix, decimals, width, &range, None, false).map(|v| within(round_to(v, places), &range))
|
||||
}
|
||||
|
||||
/// [`range_field`] whose ↑/↓ and wheel take `step`'s steps instead of whole numbers (see
|
||||
@@ -366,7 +366,7 @@ pub fn range_field_stepped(
|
||||
let places = decimals.min(6) as i32;
|
||||
let value = value.into().map(|v| within(v, &range));
|
||||
let label = take_previous_label(ui);
|
||||
bounded_field(ui, id, label, None, value, suffix, decimals, width, &range, Some(step)).map(|v| within(round_to(v, places), &range))
|
||||
bounded_field(ui, id, label, None, value, suffix, decimals, width, &range, Some(step), false).map(|v| within(round_to(v, places), &range))
|
||||
}
|
||||
|
||||
/// [`plain_field`] for a value the selected objects may not share: `None` shows a blank field.
|
||||
@@ -385,7 +385,7 @@ pub fn mixed_field_named(
|
||||
width: f32,
|
||||
) -> Option<f64> {
|
||||
let label = take_previous_label(ui);
|
||||
bounded_field(ui, id, label, name, value, suffix, decimals, width, &ANY, None)
|
||||
bounded_field(ui, id, label, name, value, suffix, decimals, width, &ANY, None, false)
|
||||
}
|
||||
|
||||
/// [`mixed_field`] whose ↑/↓, wheel and scrub stop at `bounds`; ↑/↓ and the wheel take `step`'s steps
|
||||
@@ -403,6 +403,7 @@ fn bounded_field(
|
||||
width: f32,
|
||||
bounds: &RangeInclusive<f64>,
|
||||
step: Option<fn(f64, f64) -> f64>,
|
||||
parentheses: bool,
|
||||
) -> Option<f64> {
|
||||
let armed = take_armed_focus(ui, &id);
|
||||
let id = id_with(ui.scope_id(), id);
|
||||
@@ -412,9 +413,9 @@ fn bounded_field(
|
||||
let suffix = suffix.trim();
|
||||
let buf = decimal_point(buf);
|
||||
let bare = if suffix.is_empty() { buf.to_string() } else { buf.replace(suffix, "") };
|
||||
vectorcraft_doc::parse_number(&bare.replace(['%', '°'], ""))
|
||||
vectorcraft_doc::parse_number(&bare.replace(['%', '°', '(', ')'], ""))
|
||||
};
|
||||
let shown = value.map(show).unwrap_or_default();
|
||||
let shown = value.map(|v| if parentheses { format!("({})", show(v)) } else { show(v) }).unwrap_or_default();
|
||||
take_dialog_focus(ui, id, &shown);
|
||||
if armed {
|
||||
focus_field(ui, id, &shown);
|
||||
@@ -508,7 +509,7 @@ pub fn spin_field_stops_named(
|
||||
presets: &[f64],
|
||||
) -> Option<f64> {
|
||||
let label = take_previous_label(ui);
|
||||
spin_generic(ui, label, value, None, width, step, min, presets, presets, &|v| unit.format(v), &mut |ui, fw, label| {
|
||||
spin_generic(ui, label, value, None, None, None, width, step, min, presets, presets, &|v| unit.format(v), &mut |ui, fw, label| {
|
||||
unit_field(ui, id, label, name, value, unit, fw, false, min..=f64::INFINITY, presets)
|
||||
})
|
||||
.and_then(SpinPick::value)
|
||||
@@ -519,6 +520,7 @@ pub fn spin_field_stops_named(
|
||||
pub enum SpinPick {
|
||||
Value(f64),
|
||||
Auto,
|
||||
Optical,
|
||||
}
|
||||
|
||||
impl SpinPick {
|
||||
@@ -527,6 +529,7 @@ impl SpinPick {
|
||||
match self {
|
||||
SpinPick::Value(v) => Some(v),
|
||||
SpinPick::Auto => None,
|
||||
SpinPick::Optical => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -563,7 +566,7 @@ pub fn spin_field_auto_named(
|
||||
presets: &[f64],
|
||||
) -> Option<SpinPick> {
|
||||
let label = take_previous_label(ui);
|
||||
spin_generic(ui, label, value, auto, width, step, min, presets, &[], &|v| unit.format(v), &mut |ui, fw, label| {
|
||||
spin_generic(ui, label, value, auto, None, None, width, step, min, presets, &[], &|v| unit.format(v), &mut |ui, fw, label| {
|
||||
unit_field(ui, id, label, name, value, unit, fw, false, min..=f64::INFINITY, &[])
|
||||
})
|
||||
}
|
||||
@@ -602,12 +605,43 @@ pub fn spin_plain_named(
|
||||
let label = take_previous_label(ui);
|
||||
let value = value.into();
|
||||
let fmt = |v: f64| format!("{v}{suffix}");
|
||||
spin_generic(ui, label, value, None, width, step, min, presets, &[], &fmt, &mut |ui, fw, label| {
|
||||
bounded_field(ui, id, label, name, value, suffix, decimals, fw, &(min..=f64::INFINITY), None)
|
||||
spin_generic(ui, label, value, None, None, None, width, step, min, presets, &[], &fmt, &mut |ui, fw, label| {
|
||||
bounded_field(ui, id, label, name, value, suffix, decimals, fw, &(min..=f64::INFINITY), None, false)
|
||||
})
|
||||
.and_then(SpinPick::value)
|
||||
}
|
||||
|
||||
/// Kerning's numeric field and automatic method choices.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn spin_kerning(
|
||||
ui: &mut Ui,
|
||||
id: impl std::hash::Hash + Copy,
|
||||
value: Option<f64>,
|
||||
automatic: Option<bool>,
|
||||
optical: Option<bool>,
|
||||
optical_label: &str,
|
||||
parentheses: bool,
|
||||
width: f32,
|
||||
presets: &[f64],
|
||||
) -> Option<SpinPick> {
|
||||
let label = take_previous_label(ui);
|
||||
spin_generic(
|
||||
ui,
|
||||
label,
|
||||
value,
|
||||
automatic,
|
||||
optical,
|
||||
Some(optical_label),
|
||||
width,
|
||||
10.0,
|
||||
-1000.0,
|
||||
presets,
|
||||
&[],
|
||||
&|v| format!("{v}"),
|
||||
&mut |ui, fw, label| bounded_field(ui, id, label, None, value, "", 0, fw, &(-1000.0..=f64::INFINITY), None, parentheses),
|
||||
)
|
||||
}
|
||||
|
||||
type SpinField<'a> = dyn FnMut(&mut Ui, f32, Option<PreviousLabel>) -> Option<f64> + 'a;
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -616,6 +650,8 @@ fn spin_generic(
|
||||
label: Option<PreviousLabel>,
|
||||
value: Option<f64>,
|
||||
auto: Option<bool>,
|
||||
optical: Option<bool>,
|
||||
optical_label: Option<&str>,
|
||||
width: f32,
|
||||
step: f64,
|
||||
min: f64,
|
||||
@@ -674,6 +710,11 @@ fn spin_generic(
|
||||
{
|
||||
out = Some(SpinPick::Auto);
|
||||
}
|
||||
if let Some(on) = optical
|
||||
&& ui.selectable_label(on, optical_label.unwrap_or_default()).clicked()
|
||||
{
|
||||
out = Some(SpinPick::Optical);
|
||||
}
|
||||
let current = value.filter(|_| auto != Some(true));
|
||||
for p in presets {
|
||||
if ui.selectable_label(current.is_some_and(|v| (v - p).abs() < 1e-6), fmt(*p)).clicked() {
|
||||
|
||||
+12
-1
@@ -3217,7 +3217,8 @@ Pen's second anchor at (150, 431), in line with the square's centre.
|
||||
leading (↑/↓ in horizontal type, ←/→ in vertical type), and baseline shift (Shift+↑/↓ in horizontal type,
|
||||
Shift+→/← in vertical type). Mixed selected text objects use the horizontal mapping; Cmd/Ctrl makes five steps.
|
||||
`type.resetTracking` (Cmd+Alt+Q, `press_key {key: "Q", mods: {cmd: true, alt: true}}`) sets tracking to 0 and
|
||||
kerning to Auto in the type objects in `ids`, the Type tool's selected text or the selected type objects; at a caret
|
||||
clears manual kerning in the type objects in `ids`, the Type tool's selected text or the selected type objects,
|
||||
keeping Optical; at a caret
|
||||
it resets only the kerning of the character before it. `type.resetHorizontalScale` (Cmd+Shift+X,
|
||||
`press_key {key: "X", mods: {cmd: true, shift: true}}`) sets the horizontal scale to 100%, as the Character panel
|
||||
shows it, in the same text; at a caret it leaves the text as it is. In the app, `type.highlightKerning` (Cmd+Alt+K),
|
||||
@@ -4918,6 +4919,16 @@ the paragraph's text now starts; when the text before `at` starts no list, nothi
|
||||
{"name":"run_command","arguments":{"command":"text.convertListToText","params":{}}}
|
||||
```
|
||||
|
||||
## Kerning
|
||||
|
||||
`text.setRangeStyle {id, start, end, kerning}` (a range) and `text.setFormat {ids?, kerning}`
|
||||
(the selected type) accept `kerning: "auto" | "optical" | number` (1/1000 em). Auto uses the font's
|
||||
pair metrics; Optical spaces horizontal pairs from their glyph shapes. Vertical text uses font
|
||||
metrics in either mode. A number sets a manual pair value,
|
||||
replacing that pair's automatic correction while tracking remains additive. Choosing Optical keeps
|
||||
existing manual pair values; choosing `"auto"` clears them. New text and files without an Optical setting use Auto. The Character
|
||||
panel offers both automatic modes.
|
||||
|
||||
## Small Caps, Superscript and Subscript
|
||||
`text.setFormat {smallCaps, position}` (the selected type, or `ids`) and `text.setRangeStyle {id, start, end, smallCaps,
|
||||
position}` (a range) set what the Character panel's Small Caps, Superscript and Subscript buttons and menu items set.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -24,7 +24,7 @@ can use installed system fonts, while the web app uses the fonts supplied by its
|
||||
6. Adjust **Font Size** (the field beside **T**) and **Tracking** (beside **VA↔**) in the Character
|
||||
panel. Hover a field's label to see its name. For example, enter `72 pt` for a large heading and
|
||||
try tracking `50` to spread its letters slightly. Positive tracking adds space; negative tracking
|
||||
tightens it. **Leading** controls the spacing between lines; **Kerning (0 = Auto)** adjusts pairs.
|
||||
tightens it. **Leading** controls the spacing between lines; **Kerning** adjusts pairs.
|
||||
7. To change only part of the text, select **Type Tool**, click into the text and drag across the
|
||||
characters before using the Character panel. Press **Escape** when finished. Keep the text
|
||||
editable until its wording, font and spacing are settled.
|
||||
|
||||
Reference in New Issue
Block a user