Type: Standard Vertical Roman Alignment stands half-width characters upright in vertical type

Character panel menu > Standard Vertical Roman Alignment, with Show East
Asian Options, is now a check item. It sets a new character attribute,
`standardVerticalRoman` (text.setFormat, text.setRangeStyle), off by
default and saved only when on. In vertical type the marked Latin
letters, digits and other characters that would lie on their side stand
upright one below the other, each in its upright cell. Spaces keep their
width and tate-chu-yoko blocks stay across the column. Horizontal type
keeps the mark and does not change. A combining mark shaped as its own
glyph turns with its letter, in Latin and CJK upright type alike.

Character Style Options labels the Character Alignment row "Character
Alignment" and gives Standard Vertical Roman Alignment its own checkbox.
Reset Panel turns the attribute off. PDF and SVG export place the glyphs
from the layout and need no change.
This commit is contained in:
dotCooCoo
2026-11-01 08:32:55 +07:00
committed by bflatastic
parent ac896cfe3d
commit ece78f052f
17 changed files with 290 additions and 35 deletions
File diff suppressed because one or more lines are too long
+8 -2
View File
@@ -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\"|\"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\"|\"devanagari\"|\"bengali\"|\"gurmukhi\"|\"gujarati\"|\"odia\"|\"tamil\"|\"telugu\"|\"kannada\"|\"malayalam\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed: Western, Arabic-Indic (hindi), Eastern Arabic-Indic (farsi) or an Indic script's own digits), language?: name|tag (Character panel › Language: one of Document Setup's languages by name or BCP 47 tag, case-insensitive, such as \"Turkish\" or \"tr\"; the characters are shaped in it, so the font uses that language's forms (OpenType `locl`); saved as the tag, English: USA, the default, as none; new type takes the document's language), 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), standardVerticalRoman?: bool (Character panel menu › Standard Vertical Roman Alignment, with Show East Asian Options: in vertical type those half-width characters, such as Latin letters and digits, stand upright one below the other instead of being rotated; spaces and tate-chu-yoko blocks are not changed; kept on the characters and ignored in horizontal type; off by default), 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\"|\"devanagari\"|\"bengali\"|\"gurmukhi\"|\"gujarati\"|\"odia\"|\"tamil\"|\"telugu\"|\"kannada\"|\"malayalam\" (Character panel menu › Digits: the numeral system every digit is drawn in, whatever was typed: Western, Arabic-Indic (hindi), Eastern Arabic-Indic (farsi) or an Indic script's own digits), language?: name|tag (Character panel › Language: one of Document Setup's languages by name or BCP 47 tag, case-insensitive, such as \"Turkish\" or \"tr\"; the characters are shaped in it, so the font uses that language's forms (OpenType `locl`); saved as the tag, English: USA, the default, as none; new type takes the document's language), 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
),
@@ -319,6 +319,7 @@ pub(crate) struct CharChange {
char_direction: Option<Option<vectorcraft_doc::ParaDirection>>,
proportional_metrics: Option<bool>,
tate_chu_yoko: Option<bool>,
standard_vertical_roman: Option<bool>,
no_break: Option<bool>,
digits: Option<vectorcraft_doc::Digits>,
/// The language to set: a tag, or `None` for the default.
@@ -471,7 +472,7 @@ impl CharChange {
/// `setup` gives the superscript, subscript and small caps proportions.
pub(crate) fn parse(p: &Value, cmd: &str, setup: &vectorcraft_doc::DocSetup) -> Result<Self> {
check_numbers(p, &["size", "tracking", "baselineShift", "hScale", "vScale", "rotation", "strokeWidth"], cmd)?;
check_switches(p, &["underline", "strikethrough", "allCaps", "proportionalMetrics", "tateChuYoko", "noBreak"], cmd)?;
check_switches(p, &["underline", "strikethrough", "allCaps", "proportionalMetrics", "tateChuYoko", "standardVerticalRoman", "noBreak"], cmd)?;
let num = |k: &str| p.get(k).and_then(Value::as_f64);
let (position, small_caps) = super::docsetup::script_params(p, setup, cmd)?;
let flag = |k: &str| p.get(k).and_then(Value::as_bool);
@@ -517,6 +518,7 @@ impl CharChange {
char_direction: char_direction_param(p, cmd)?,
proportional_metrics: flag("proportionalMetrics"),
tate_chu_yoko: flag("tateChuYoko"),
standard_vertical_roman: flag("standardVerticalRoman"),
no_break: flag("noBreak"),
digits: digits_param(p, cmd)?,
language: language_param(p, cmd)?,
@@ -563,6 +565,7 @@ impl CharChange {
&& self.char_direction.is_none()
&& self.proportional_metrics.is_none()
&& self.tate_chu_yoko.is_none()
&& self.standard_vertical_roman.is_none()
&& self.no_break.is_none()
&& self.digits.is_none()
&& self.language.is_none()
@@ -668,6 +671,9 @@ impl CharChange {
if let Some(v) = self.tate_chu_yoko {
st.tate_chu_yoko = v;
}
if let Some(v) = self.standard_vertical_roman {
st.standard_vertical_roman = v;
}
if let Some(v) = self.no_break {
st.no_break = v;
}
+1
View File
@@ -175,6 +175,7 @@ pub fn char_attrs(v: &CharStyle) -> Map<String, Value> {
("charDirection", json!(v.char_direction)),
("proportionalMetrics", json!(v.proportional_metrics)),
("tateChuYoko", json!(v.tate_chu_yoko)),
("standardVerticalRoman", json!(v.standard_vertical_roman)),
("noBreak", json!(v.no_break)),
("digits", json!(v.digits)),
("language", json!(v.language)),
@@ -1206,6 +1206,44 @@ fn tate_chu_yoko_is_a_character_attribute_saved_only_when_on() {
assert!(runs(&s).iter().all(|r| !r.style.tate_chu_yoko));
}
/// Standard Vertical Roman Alignment (Character panel menu, East Asian options): a character
/// attribute, off by default and saved only when on. Documents written before it read as off; a
/// character style captured from the characters carries it.
#[test]
fn standard_vertical_roman_is_a_character_attribute_saved_only_when_on() {
let mut s = Session::new();
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
let id = s.execute("text.create", &json!({"x": 10, "y": 50, "text": "ABC年"})).unwrap()["id"].as_u64().unwrap();
let runs = |s: &Session| match &s.doc().unwrap().doc.node(NodeId(id)).unwrap().kind {
NodeKind::Text(t) => t.runs.clone(),
_ => panic!("text"),
};
assert!(!runs(&s)[0].style.standard_vertical_roman);
assert!(serde_json::to_value(&runs(&s)[0].style).unwrap().get("standardVerticalRoman").is_none());
s.execute("select.set", &json!({"ids": [id]})).unwrap();
s.execute("text.setFormat", &json!({"standardVerticalRoman": true})).unwrap();
assert!(runs(&s).iter().all(|r| r.style.standard_vertical_roman));
assert_eq!(serde_json::to_value(&runs(&s)[0].style).unwrap()["standardVerticalRoman"], true);
s.execute("edit.undo", &json!({})).unwrap();
assert!(!runs(&s)[0].style.standard_vertical_roman);
s.execute("text.setRangeStyle", &json!({"id": id, "start": 0, "end": 3, "standardVerticalRoman": true})).unwrap();
let on: Vec<_> = runs(&s).iter().map(|r| (r.text.clone(), r.style.standard_vertical_roman)).collect();
assert_eq!(on, [("ABC".to_string(), true), ("年".to_string(), false)]);
let old = serde_json::to_value(&runs(&s)[1].style).unwrap();
assert!(old.get("standardVerticalRoman").is_none());
assert!(!serde_json::from_value::<vectorcraft_doc::CharStyle>(old).unwrap().standard_vertical_roman);
// Saved and reopened as it was.
let back = vectorcraft_format::load(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap();
let NodeKind::Text(t) = &back.node(NodeId(id)).unwrap().kind else { panic!("text") };
let reopened: Vec<_> = t.runs.iter().map(|r| (r.text.clone(), r.style.standard_vertical_roman)).collect();
assert_eq!(reopened, on);
assert_eq!(super::super::textstyles::char_attrs(&runs(&s)[0].style)["standardVerticalRoman"], true);
assert_eq!(super::super::textstyles::char_attrs(&runs(&s)[1].style)["standardVerticalRoman"], false);
assert!(s.execute("text.setRangeStyle", &json!({"id": id, "standardVerticalRoman": "true"})).is_err());
s.execute("text.setFormat", &json!({"standardVerticalRoman": false})).unwrap();
assert!(runs(&s).iter().all(|r| !r.style.standard_vertical_roman));
}
/// No Break is a character attribute, saved only when on.
#[test]
fn no_break_is_a_character_attribute_saved_only_when_on() {