mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Type: Optical Margin Alignment is a setting of the whole thread
Optical Margin Alignment was stored and set per text object, so the frames of one thread could differ. Turning it on in the first frame of a thread left the lines in the other frames flush with their margins. Type › Optical Margin Alignment and type.opticalMarginAlignment now set every frame of a selected frame's thread, in one undo step, and the reply's ids list them all. After any edit to a thread, its frames take the setting of the frame that the edit turned on or off, else the first frame's. Threading frames gives the thread its first frame's setting, and frames taken out of a thread keep it. A file whose frames of one thread differ opens with the first frame's setting in all of them. This holds for native files and for documents restored from the editing copy that an EPS, SVG, PDF or .ai file carries. Threading text from an out port into existing area type (text.thread.add with to) now flows the story into that frame at once. Before, the story reached it only at the next edit of one of the thread's frames. Docs: docs/mcp.md, docs/target-app-parity.md, docs/type-parity.md.
This commit is contained in:
@@ -270,9 +270,13 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
|
||||
{
|
||||
super::super::fileinfo::read_pdf(&mut doc.metadata, info);
|
||||
}
|
||||
// Threaded type read from a file flows through its frames.
|
||||
// Threaded type read from a file flows through its frames (the threaded type of a restored
|
||||
// document keeps its text as saved), and every frame of a thread takes its first frame's
|
||||
// Optical Margin Alignment setting.
|
||||
if !doc.text_threads.is_empty() && !restored {
|
||||
crate::cmd::threads::reflow(&Document::new(1.0, 1.0), &mut doc);
|
||||
} else if !doc.text_threads.is_empty() {
|
||||
crate::cmd::threads::share_optical_margin(&mut doc);
|
||||
}
|
||||
if let Some(mode) = opts.color_mode.filter(|m| *m != doc.color_mode) {
|
||||
super::super::colormgmt::set_color_mode(&mut doc, mode, true, None, opts.grays);
|
||||
|
||||
@@ -137,6 +137,7 @@ pub(crate) fn reflow(before: &Document, doc: &mut Document) {
|
||||
}
|
||||
}
|
||||
}
|
||||
let optical = if live.len() >= 2 { optical_margin(before, doc, &live) } else { None };
|
||||
let frames: Vec<TextObject> = live.iter().filter_map(|id| text_of(doc, *id).cloned()).collect();
|
||||
let refs: Vec<&TextObject> = frames.iter().collect();
|
||||
let parts = vectorcraft_text::thread::distribute(vectorcraft_text::FontDb::global(), &refs, &story, ¶s);
|
||||
@@ -145,10 +146,12 @@ pub(crate) fn reflow(before: &Document, doc: &mut Document) {
|
||||
let mut next = (**t).clone();
|
||||
next.runs = part.runs;
|
||||
next.set_paragraph_styles(part.paras);
|
||||
if next.runs != t.runs || next.para != t.para || next.paras != t.paras {
|
||||
let optical = optical.unwrap_or(t.optical_margin);
|
||||
if next.runs != t.runs || next.para != t.para || next.paras != t.paras || optical != t.optical_margin {
|
||||
t.runs = next.runs;
|
||||
t.para = next.para;
|
||||
t.paras = next.paras;
|
||||
t.optical_margin = optical;
|
||||
refresh_bounds(t);
|
||||
}
|
||||
}
|
||||
@@ -160,6 +163,44 @@ pub(crate) fn reflow(before: &Document, doc: &mut Document) {
|
||||
doc.text_threads = kept;
|
||||
}
|
||||
|
||||
/// The Optical Margin Alignment setting that every frame of a thread with the frames `live` takes:
|
||||
/// that of the first frame whose setting changed between `before` and `doc`, else the first frame's.
|
||||
fn optical_margin(before: &Document, doc: &Document, live: &[NodeId]) -> Option<bool> {
|
||||
let now = |id: &NodeId| text_of(doc, *id).map(|t| t.optical_margin);
|
||||
let changed = live.iter().find(|id| text_of(before, **id).is_some_and(|t| Some(t.optical_margin) != now(id)));
|
||||
changed.or(live.first()).and_then(now)
|
||||
}
|
||||
|
||||
/// Give every frame of each thread the first frame's Optical Margin Alignment setting, without
|
||||
/// re-flowing the story: for a document restored from a file as it was saved.
|
||||
pub(crate) fn share_optical_margin(doc: &mut Document) {
|
||||
for thread in doc.text_threads.clone() {
|
||||
let live: Vec<NodeId> = thread.into_iter().filter(|id| text_of(doc, *id).is_some()).collect();
|
||||
let Some(on) = optical_margin(doc, doc, &live) else { continue };
|
||||
for id in &live {
|
||||
if let Some(NodeKind::Text(t)) = doc.node_mut(*id).map(|n| &mut n.kind)
|
||||
&& t.optical_margin != on
|
||||
{
|
||||
t.optical_margin = on;
|
||||
refresh_bounds(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `ids` followed by the other frames of the threads they are in, each once, in thread order.
|
||||
pub(crate) fn with_threads(d: &Document, ids: &[NodeId]) -> Vec<NodeId> {
|
||||
let mut out = ids.to_vec();
|
||||
for thread in d.text_threads.iter().filter(|t| t.iter().any(|id| ids.contains(id))) {
|
||||
for id in thread {
|
||||
if !out.contains(id) && text_of(d, *id).is_some() {
|
||||
out.push(*id);
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A closed path converted to an empty area-type frame with `style`.
|
||||
fn frame_from_path(n: &Node, style: vectorcraft_doc::CharStyle) -> Option<Node> {
|
||||
let NodeKind::Path { path, .. } = &n.kind else { return None };
|
||||
@@ -280,7 +321,9 @@ fn add(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
None => d.text_threads.push(vec![from, to]),
|
||||
}
|
||||
// `to` is new or changed, so [`reflow`] flows the story into it after this edit.
|
||||
// Force a re-flow of the thread on this edit: the edit leaves existing area type `to`
|
||||
// unchanged.
|
||||
touch(d, to);
|
||||
sel.set([from, to]);
|
||||
Ok(to)
|
||||
})?;
|
||||
@@ -550,6 +593,17 @@ mod tests {
|
||||
assert_eq!(threads(&s), [[a, c, d]]);
|
||||
}
|
||||
|
||||
/// Area type threaded into from an out port takes the overflow at once, before its own text.
|
||||
#[test]
|
||||
fn add_into_area_type_flows_the_story_into_it() {
|
||||
let (mut s, a, _) = setup();
|
||||
let c = new_id(s.execute("text.create", &json!({"x": 10, "y": 100, "text": "Own text.", "area": {"width": 150, "height": 190}})).unwrap());
|
||||
s.execute("text.thread.add", &json!({"from": a, "to": c})).unwrap();
|
||||
assert_eq!(threads(&s), [[a, c]]);
|
||||
assert_eq!(format!("{}{}", text(&s, a), text(&s, c)), format!("{}Own text.", TEXT.repeat(4)));
|
||||
assert!(text(&s, c).len() > "Own text.".len(), "the overflow flowed into the frame: {:?}", text(&s, c));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_refuses_bad_params_and_leaves_the_document_alone() {
|
||||
let (mut s, a, b) = setup();
|
||||
@@ -595,4 +649,113 @@ mod tests {
|
||||
assert_eq!(text(&s, b), "");
|
||||
assert_eq!(text(&s, a), TEXT.repeat(4), "the story returns to the remaining frame");
|
||||
}
|
||||
|
||||
/// A story of one-line paragraphs that each start with a quotation mark, threaded from area
|
||||
/// type `a` into the rectangle `b`, which shows the paragraphs that `a` has no room for.
|
||||
fn quoted_thread() -> (Session, u64, u64) {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 600, "height": 300})).unwrap();
|
||||
let story = "“Quoted.”\n".repeat(20);
|
||||
let a = new_id(s.execute("text.create", &json!({"x": 10, "y": 10, "text": story, "area": {"width": 150, "height": 60}})).unwrap());
|
||||
let b = new_id(s.execute("shape.rectangle", &json!({"x": 200, "y": 10, "width": 150, "height": 200})).unwrap());
|
||||
s.execute("select.set", &json!({"ids": [a, b]})).unwrap();
|
||||
s.execute("text.thread.create", &json!({})).unwrap();
|
||||
assert!(text(&s, b).starts_with('“'));
|
||||
(s, a, b)
|
||||
}
|
||||
|
||||
/// Frame `id`'s Optical Margin Alignment setting, and whether its glyphs start left of its frame
|
||||
/// (its lines' opening quotation marks hang).
|
||||
fn optical(s: &Session, id: u64) -> (bool, bool) {
|
||||
let Some(t) = text_of(&s.doc().unwrap().doc, NodeId(id)) else { panic!("not area type") };
|
||||
let TextKind::Area { frame } = &t.kind else { panic!("not area type") };
|
||||
(t.optical_margin, t.cached_bounds.unwrap().x0 < frame.bounds().unwrap().x0 - 1.0)
|
||||
}
|
||||
|
||||
/// Optical Margin Alignment is a setting of the whole thread: Type › Optical Margin Alignment
|
||||
/// in any frame sets it in every frame of the thread, in one undo step, and the lines in every
|
||||
/// frame hang. Typing keeps it, and frames taken out of the thread keep it.
|
||||
#[test]
|
||||
fn optical_margin_alignment_is_set_for_every_frame_of_a_thread() {
|
||||
let (mut s, a, b) = quoted_thread();
|
||||
assert_eq!((optical(&s, a), optical(&s, b)), ((false, false), (false, false)));
|
||||
s.execute("select.set", &json!({"ids": [a]})).unwrap();
|
||||
let r = s.execute("type.opticalMarginAlignment", &json!({})).unwrap();
|
||||
assert_eq!(r, json!({"on": true, "ids": [a, b]}));
|
||||
assert_eq!((optical(&s, a), optical(&s, b)), ((true, true), (true, true)), "the second frame's lines hang too");
|
||||
// Turned off from the second frame, it is off in both, and one undo turns it back on in both.
|
||||
s.execute("select.set", &json!({"ids": [b]})).unwrap();
|
||||
assert_eq!(s.execute("type.opticalMarginAlignment", &json!({})).unwrap()["on"], false);
|
||||
assert_eq!((optical(&s, a), optical(&s, b)), ((false, false), (false, false)));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!((optical(&s, a), optical(&s, b)), ((true, true), (true, true)));
|
||||
// Typing in the first frame re-flows the story and keeps the setting.
|
||||
s.execute("text.editRange", &json!({"id": a, "start": 0, "end": 0, "insert": "“New.”\n"})).unwrap();
|
||||
assert_eq!((optical(&s, a), optical(&s, b)), ((true, true), (true, true)));
|
||||
// Any edit that sets one frame sets the thread.
|
||||
s.edit("Set", |d, _| {
|
||||
let Some(NodeKind::Text(t)) = d.node_mut(NodeId(b)).map(|n| &mut n.kind) else { panic!("not text") };
|
||||
t.optical_margin = false;
|
||||
refresh_bounds(t);
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!((optical(&s, a), optical(&s, b)), ((false, false), (false, false)));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
// Released or unthreaded, each frame keeps it.
|
||||
s.execute("select.set", &json!({"ids": [b]})).unwrap();
|
||||
s.execute("text.thread.releaseSelection", &json!({})).unwrap();
|
||||
assert_eq!((optical(&s, a).0, optical(&s, b).0), (true, true));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("text.thread.remove", &json!({})).unwrap();
|
||||
assert!(threads(&s).is_empty());
|
||||
assert_eq!((optical(&s, a), optical(&s, b)), ((true, true), (true, true)));
|
||||
}
|
||||
|
||||
/// Threading type into a thread gives the joined thread the setting of its first frame.
|
||||
#[test]
|
||||
fn a_thread_takes_its_first_frames_optical_margin_alignment() {
|
||||
let (mut s, a, b) = quoted_thread();
|
||||
s.execute("text.thread.remove", &json!({})).unwrap();
|
||||
s.execute("type.opticalMarginAlignment", &json!({"ids": [a], "on": true})).unwrap();
|
||||
assert_eq!((optical(&s, a).0, optical(&s, b).0), (true, false));
|
||||
// Threaded again in stacking order: the second frame takes the first one's setting.
|
||||
s.execute("select.set", &json!({"ids": [a, b]})).unwrap();
|
||||
s.execute("text.thread.create", &json!({})).unwrap();
|
||||
assert_eq!((optical(&s, a), optical(&s, b)), ((true, true), (true, true)));
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!((optical(&s, a).0, optical(&s, b).0), (true, false));
|
||||
// From `b`'s out port into `a`: `a` joins `b`'s thread after it and takes its setting.
|
||||
s.execute("text.thread.add", &json!({"from": b, "to": a})).unwrap();
|
||||
assert_eq!(threads(&s), [[b, a]]);
|
||||
assert_eq!((optical(&s, a).0, optical(&s, b).0), (false, false));
|
||||
// A new frame threaded from a frame takes the thread's setting.
|
||||
s.execute("type.opticalMarginAlignment", &json!({"ids": [a], "on": true})).unwrap();
|
||||
let c = new_id(s.execute("text.thread.add", &json!({"from": a, "at": [400, 10]})).unwrap());
|
||||
assert_eq!([a, b, c].map(|id| optical(&s, id).0), [true; 3]);
|
||||
}
|
||||
|
||||
/// A file saved with the frames of one thread set differently opens with the first frame's
|
||||
/// setting in every frame: a native file, and an EPS file restored from the document it carries.
|
||||
#[test]
|
||||
fn a_thread_saved_with_differing_frames_opens_with_the_first_frames_setting() {
|
||||
let (mut s, a, b) = quoted_thread();
|
||||
for (first, name) in [(true, "old.vectorcraft"), (false, "old.vectorcraft"), (true, "old.eps"), (false, "old.eps")] {
|
||||
for (id, on) in [(a, first), (b, !first)] {
|
||||
let doc = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
let Some(NodeKind::Text(t)) = doc.node_mut(NodeId(id)).map(|n| &mut n.kind) else { panic!("not text") };
|
||||
t.optical_margin = on;
|
||||
}
|
||||
let bytes = if name.ends_with(".eps") {
|
||||
let r = s.execute("document.exportEps", &json!({})).unwrap();
|
||||
vectorcraft_format::base64_decode(r["dataBase64"].as_str().unwrap()).unwrap()
|
||||
} else {
|
||||
vectorcraft_format::save(&s.doc().unwrap().doc, false)
|
||||
};
|
||||
let r = s.execute("document.open", &json!({"name": name, "dataBase64": vectorcraft_format::base64_encode(&bytes)})).unwrap();
|
||||
assert_eq!(r["restored"], name.ends_with(".eps"), "{r}");
|
||||
assert_eq!(threads(&s), [[a, b]], "{name}");
|
||||
assert_eq!((optical(&s, a), optical(&s, b)), ((first, first), (first, first)), "{name}, first frame {first}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Optical Margin Alignment",
|
||||
["Type"],
|
||||
None,
|
||||
"{ids?|id?, on?: bool} turn Optical Margin Alignment on or off for the selected type (on omitted: the opposite of the first object's setting): in every left-to-right paragraph of point and area type, quotation marks, hyphens, full stops and commas at a line's start or end hang wholly outside the margin there; asterisks, tildes, ellipses, en and em dashes, colons and semicolons hang by half; and A, V, W, Y, v, w and y hang by a tenth of their advance; a left-aligned line hangs at its left margin, a right-aligned line hangs at its right margin, and centered and justified lines hang at both → {on, ids}",
|
||||
"{ids?|id?, on?: bool} turn Optical Margin Alignment on or off for the selected type and every frame of its threads (on omitted: the opposite of the first object's setting): in every left-to-right paragraph of point and area type, quotation marks, hyphens, full stops and commas at a line's start or end hang wholly outside the margin there; asterisks, tildes, ellipses, en and em dashes, colons and semicolons hang by half; and A, V, W, Y, v, w and y hang by a tenth of their advance; a left-aligned line hangs at its left margin, a right-aligned line hangs at its right margin, and centered and justified lines hang at both → {on, ids}",
|
||||
has_selection,
|
||||
optical_margin_alignment
|
||||
),
|
||||
@@ -107,9 +107,9 @@ fn orientation(s: &mut Session, p: &Value, vertical: bool) -> Result<Value> {
|
||||
}
|
||||
|
||||
/// Type › Optical Margin Alignment: set `on` (without it, the opposite of the first object's
|
||||
/// setting) on every selected text object, in one undo step.
|
||||
/// setting) on every selected text object and every frame of their threads, in one undo step.
|
||||
fn optical_margin_alignment(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let ids = text_targets(s, p, "Optical Margin Alignment")?;
|
||||
let ids = super::threads::with_threads(&s.doc()?.doc, &text_targets(s, p, "Optical Margin Alignment")?);
|
||||
let first = ids.first().and_then(|id| match s.doc().ok()?.doc.node(*id).map(|n| &n.kind) {
|
||||
Some(NodeKind::Text(t)) => Some(t.optical_margin),
|
||||
_ => None,
|
||||
|
||||
+4
-1
@@ -3523,7 +3523,10 @@ undo step.
|
||||
`type.opticalMarginAlignment {on?: bool}` (Type › Optical Margin Alignment) turns Optical Margin Alignment on or off
|
||||
for the selected type objects (or `ids`); without `on` it sets the opposite of the first object's setting. It replies
|
||||
`{on, ids}`, and the menu item is checked while the first type object it acts on (inside a selected group too) has it
|
||||
on. In every left-to-right paragraph of point and area type, these characters hang outside the margin when they
|
||||
on. Optical Margin Alignment is a setting of the whole thread: the command sets every frame of a selected frame's
|
||||
thread, and `ids` lists them all. Threading frames gives the thread its first frame's setting, frames taken out of
|
||||
a thread keep it, and a file whose frames of one thread differ opens with the first frame's setting in all of them.
|
||||
In every left-to-right paragraph of point and area type, these characters hang outside the margin when they
|
||||
start or end a line: quotation marks, hyphens (a hyphenated line's hyphen too), full stops and commas hang wholly;
|
||||
asterisks, tildes, ellipses, en and em dashes, colons and semicolons hang by half their width; and A, V, W, Y, v, w
|
||||
and y hang by a tenth of theirs. A punctuation mark followed by a quotation mark hangs with it.
|
||||
|
||||
@@ -605,6 +605,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
For Auto Size area type in columns, the bisection for the frame's height runs on the lines of the one-column layout, and its result is checked with at most two layouts of the columns. When the text wraps around objects or does not fit the one-column layout, or when the check fails, the columns are laid out at every step of the bisection, as before. Typing at the start of a story of 50,000 characters in 2 columns took 1,670 ms a keystroke and now takes 292 ms (release build, `auto_height_in_columns_typing_timing`).
|
||||
Exports to PDF, EPS, SVG, EMF, WMF and the raster formats warn about the area type and type on a path that they draw without the text that doesn't fit, and give their ids, as Print and Print Preview do.
|
||||
An edit copies a thread's story only when it changes one of the thread's frames: with a story of 50,000 characters threaded through 8 frames with a style change at every word, moving another object took 7.8 ms and now takes 0.013 ms, and a frame of a drag took 7.2 ms and now takes 0.012 ms (release build, `threaded_story_edit_timing`).
|
||||
The frames of threaded text share one Optical Margin Alignment setting: turning it on or off in one frame sets it in every frame, threading frames gives the thread its first frame's setting, and frames taken out of a thread keep it.
|
||||
Type › Optical Margin Alignment (`type.opticalMarginAlignment`) hangs the punctuation and letter edges at the ends of a type object's lines outside its margins: at the left of left-aligned lines, at the right of right-aligned lines, and at both ends of centered and justified lines, in point and area type of left-to-right paragraphs. Quotation marks, hyphens, full stops and commas hang wholly; asterisks, tildes, ellipses, en and em dashes, colons and semicolons hang by half; and A, V, W, Y, v, w and y hang by a tenth of their width. It is off by default and saved with the type object.
|
||||
The Paragraph panel menu's Hyphenation… (`ui.hyphenation`, `text.setFormat {wordsLongerThan, afterFirst, beforeLast, hyphenLimit, hyphenationZone, hyphenateCapitalized}`) sets each paragraph's hyphenation limits: the shortest word that breaks, the fewest letters on each side of the hyphen, the most hyphenated lines in a row, a zone at the right edge where the Single-line Composer breaks before a word instead of hyphenating it, and whether capitalized words break. The defaults are words longer than 5 letters, 3 letters on each side, no limit or zone, and capitalized words hyphenated; paragraph styles and the native file keep the options.
|
||||
- **Interface languages, vertical type:** Preferences ▸ User Interface (or VectorCraft ▸ Language on macOS) picks Automatic (the system locale) or a registered language, persisted as `interfaceLanguage`. Translations come from per-language catalogs (`i18n/*.tsv`): Traditional and Simplified Chinese, Japanese, German (every `de-*` locale), Spanish (neutral international Spanish, for every `es-*` locale), French (every `fr-*` locale), Italian (every `it-*` locale) Russian (every `ru-*` locale) and Ukrainian (every `uk-*` locale) cover the whole interface, Czech every menu label; Czech, German, Greek, Spanish, French, Hungarian, Italian, Japanese, Korean, Vietnamese, Polish, Brazilian Portuguese, Russian, Simplified Chinese, Swedish, Ukrainian and Finnish also translate status-bar and error messages as they are shown (`@msg` templates, every message literal tested; the messages themselves stay English for agents); untranslated text stays English. Japanese and Chinese glyphs come from the craft-fonts build input (release builds) or the installed fonts, Czech, German, Spanish, French, Italian, Russian and Ukrainian ones from the bundled UI fonts. The web app follows the browser's languages (or `?lang=`). The Basic toolbar's Type button holds Vertical Type in its flyout. Vertical Type, Vertical Area Type and Vertical Type on a Path create vertical text (columns right to left, upright CJK glyphs and marks with `vert` alternates, Latin and longer numbers on their side, two- and three-digit numbers set as tate-chu-yoko in one em of the column (kept whole when justified), upright glyphs centred on the column (tracked or justified too), point type's anchor on the first column's centre line); kinsoku applies to horizontal and vertical Japanese (Paragraph panel › Kinsoku Set, `text.setFormat {kinsoku}`: Hard, the default, keeps closing marks, iteration marks, the prolonged sound mark and small kana off the line start and opening brackets off its end; Soft lets 々, ー and small kana start a line as JLREQ's level 3 rules do; None turns it off), and justified Japanese lines spread their room between characters (not inside Latin words); Paragraph panel › Mojikumi Set (shown with Preferences › Type › Show East Asian Options, `text.setFormat {mojikumi}`): None or Line-end Punctuation Half Width (new type's default, as in Illustrator; older documents and imported text keep None) shares one half-em space between consecutive punctuation, sets a closing mark ending a line half width and an opening bracket starting a line flush with its start (a paragraph's first line at its first-line indent, JIS X 4051's principle in JLREQ 3.1.5), and puts a quarter em between Japanese and Latin letters or digits; Paragraph panel menu › Top-to-Top / Bottom-to-Bottom Leading measures leading from em box top to top (area type's first line touches the frame) or from baseline to baseline (new type in a Japanese interface starts top to top, with em box centre character alignment, #432); Paragraph panel menu › Burasagari None / Regular / Force (`text.setFormat {burasagari: "none"|"standard"|"forced"}`) hangs an East Asian comma or full stop (、。,.、。) ending an area type line outside it, Regular (new type's default) when it doesn't fit and Force always (the rest of the line then fills the measure; closing brackets and Latin punctuation don't hang); Character panel menu › Character Alignment (with the East Asian options; `text.setFormat` or `text.setRangeStyle {charAlign}`) lines characters smaller than the largest on their line up on the Roman baseline, the em box top/right, centre or bottom/left, or the ICF (ideographic character face) top/right or bottom/left, from the font's BASE table or, without one, the average ink box of some ideographs and kana; Proportional Metrics (the same menu; `{proportionalMetrics}`) sets full-width glyphs on the font's proportional widths (OpenType `palt`) in horizontal type and upright glyphs on its proportional heights (`vpal`, shaped top to bottom; fonts without vertical metrics keep their cells) in vertical type, and Line-end Punctuation Half Width takes nothing more off the punctuation they re-spaced; Type ▸ Type Orientation switches existing text; caret, selection, hit testing and arrow keys follow the writing direction. Vertical text exports to PDF as real text and to SVG as outlines. The interface is available in Polish (`pl`): menus, panels, dialogs and messages, with one/few/many plurals; AI-drafted, native review pending (#1498).
|
||||
|
||||
@@ -35,6 +35,7 @@ against Illustrator's public documentation; **estimated** unless noted.
|
||||
| Fallback glyphs keep the text's weight and slant | done | #2491: a character from a fallback face is set in that family's face nearest the run's weight and slant; Bold Italic of a missing family is drawn Bold (Source Sans 3 has no Bold Italic) rather than Italic |
|
||||
| Colour fonts (OpenType SVG / COLR) | partial | |
|
||||
| Every-line and Single-line composers | done | tuned by eye, never compared side by side |
|
||||
| Optical Margin Alignment | done | Type › Optical Margin Alignment, `type.opticalMarginAlignment`: per type object, with one setting for all the frames of a thread, in point and area type of left-to-right paragraphs; type on a path and right-to-left paragraphs do not hang |
|
||||
| Optical Margin Alignment | done | Type › Optical Margin Alignment, `type.opticalMarginAlignment`: per type object, in point and area type of left-to-right paragraphs; type on a path and right-to-left paragraphs do not hang |
|
||||
| Roman Hanging Punctuation | done | Paragraph panel menu › Roman Hanging Punctuation, `text.setFormat {romanHangingPunctuation}` (#2756): per paragraph, Optical Margin Alignment's punctuation and amounts without the letters; paragraph styles carry it. Evidence: `tests::roman_hanging_punctuation_hangs_punctuation_but_not_letters`, `typecmd::area_tests::roman_hanging_punctuation_is_set_per_paragraph_and_saved_only_when_on` |
|
||||
| Hyphenation… and Justification… options | partial | Hyphenation… (`ui.hyphenation`, `text.setFormat`) sets each paragraph's Words Longer Than, After First, Before Last, Hyphen Limit, Hyphenation Zone (Single-line Composer only) and Hyphenate Capitalized Words, and paragraph styles carry them (#2310); Justification… missing (menu stub) |
|
||||
|
||||
Reference in New Issue
Block a user