Merge pull request #6761 from dotCooCoo/feat/dynamic-text-path-presets

Type: Dynamic Text Presets put type on a circle, arch or bow
This commit is contained in:
BFlat
2026-11-12 12:40:55 +07:00
committed by GitHub
33 changed files with 539 additions and 1 deletions
+199
View File
@@ -69,6 +69,15 @@ pub fn specs() -> Vec<CommandSpec> {
has_dynamic_type,
dynamic_text
),
cmd!(
"type.dynamicTextPreset",
"Dynamic Text Presets",
["Type", "Dynamic Text Presets"],
None,
"{preset: \"circle\"|\"arch\"|\"bow\", position?: \"above\"|\"center\"|\"below\", flip?: bool, ids?|id?} put the selected point type and type on a path on a new path of the preset's shape with Dynamic Text on, so the text is sized to fill it (see type.dynamicText). Circle: a circle as wide as the text, the text running clockwise round all of it from its bottom; Arch: an arc of a circle bulging upward over 120°, as wide as the text; Bow: the same arc bulging downward. The width is, for point type, the length on the page of the longer side of the text's bounds, and for type on a path the longer side of its path's bounds on the page (so the same preset again leaves it as it is); the new path is centered where they were, and the shape is upright on the page when the object is rotated or scaled. position (omitted: above) sets Type on a Path Options › Align to Path: above stands the type on the path (descender), center centers it on the path (center), below hangs it from the path (ascender); flip true then turns the text to the other side of its path, running the other way. Type on a Path Options › effect and spacing stay. Area type in the selection is left as it is; a selection holding type on a path in a thread is refused. When nothing changes, no undo step is recorded → {ids, fitScale (the first object's factor)}",
has_preset_type,
dynamic_text_preset
),
cmd!(
"type.fillPlaceholder",
"Fill with Placeholder Text",
@@ -648,6 +657,196 @@ pub(crate) fn bake_dynamic_text(t: &mut TextObject) -> bool {
true
}
/// The shapes of `type.dynamicTextPreset`.
const PRESETS: [&str; 3] = ["circle", "arch", "bow"];
/// Point type and type on a path among `ids` and their descendants: the type
/// `type.dynamicTextPreset` puts on a path.
fn preset_ids(d: &Document, ids: &[NodeId]) -> Vec<NodeId> {
text_ids(d, ids)
.into_iter()
.filter(|id| matches!(d.node(*id).map(|n| &n.kind), Some(NodeKind::Text(t)) if matches!(t.kind, TextKind::Point | TextKind::OnPath { .. })))
.collect()
}
/// `type.dynamicTextPreset` is enabled for a selection holding point type or type on a path, and
/// no type on a path in a thread.
fn has_preset_type(s: &Session) -> std::result::Result<(), String> {
has_selection(s)?;
let st = s.active().ok_or("no document open")?;
let d = &st.doc;
let ids = preset_ids(d, &st.selection.objects);
if ids.is_empty() {
return Err("select point type or type on a path".into());
}
let frames = threaded_ids(d, &ids);
if !frames.is_empty() {
return Err(format!("type on a path in a thread cannot use Dynamic Text: {frames}"));
}
Ok(())
}
/// The path of Dynamic Text preset `preset` (one of [`PRESETS`]) `w` wide, centered on `c`, with
/// the start bracket the text starts at. Circle: a circle `w` across, run clockwise from its
/// bottom; Arch and Bow: a 120° arc of a circle whose chord is `w`, bulging up or down, run from
/// left to right.
fn preset_path(preset: &str, c: vectorcraft_geom::Point, w: f64) -> (vectorcraft_geom::PathData, f64) {
let h = w / 2.0;
if preset == "circle" {
// `shapes::ellipse` runs clockwise on screen from its left: its bottom is 3/4 of the way.
return (shapes::ellipse(Rect::new(c.x - h, c.y - h, c.x + h, c.y + h)), 0.75);
}
// A chord of `w` under 120°: radius w/√3, and the arc rises a half radius above its chord.
let r = w / 3f64.sqrt();
let arch = preset == "arch";
let cy = if arch { c.y - r / 4.0 + r } else { c.y + r / 4.0 - r };
let circle = Rect::new(c.x - r, cy - r, c.x + r, cy + r);
// `shapes::ellipse_arc` runs counterclockwise on screen: right to left over the top.
let mut path = if arch {
shapes::ellipse_arc(circle, 30.0, 150.0, shapes::EllipseMode::Arc)
} else {
shapes::ellipse_arc(circle, 210.0, 330.0, shapes::EllipseMode::Arc)
};
if arch {
path.reverse();
}
(path, 0.0)
}
/// `t` (point type or type on a path) on the path of `preset` with Dynamic Text on, Align to Path
/// `align`, and flipped when `flip`. The shape is made upright on the page and mapped back through
/// the object's transform, so a rotated or scaled object still shows a circle, an arch or a bow.
/// Point type: the path is as wide as the longer side of the text's layout bounds is long on the
/// page, centered on them; with no layout area (empty text), four ems of its first character
/// wide, centered on its estimated bounds. Type on a path: as wide as the longer side of its
/// path's bounds on the page, centered on them, so the same preset again gives the same path.
/// When the object's transform can't be undone, or undoing it gives a path that isn't finite, the
/// shape is made in the text's own space.
fn with_preset(t: &TextObject, preset: &str, align: vectorcraft_doc::PathAlign, flip: bool) -> TextObject {
let invertible = t.xf.is_finite() && t.xf.determinant() != 0.0 && t.xf.inverse().is_finite();
// The transform the shape was made upright through, with the shape.
let ((path, start, w), xf) = invertible
.then(|| preset_shape(t, preset, t.xf).map(|s| (s, t.xf)))
.flatten()
.or_else(|| preset_shape(t, preset, Affine::IDENTITY).map(|s| (s, Affine::IDENTITY)))
.unwrap_or_else(|| {
let (path, start) = preset_path(preset, vectorcraft_geom::Point::ZERO, 48.0);
((path, start, 48.0), Affine::IDENTITY)
});
let mut out = t.clone();
out.kind = TextKind::OnPath { path, start, end: None };
out.path_align = align;
out.dynamic_text = true;
if flip {
out.flip_on_path();
}
// The same preset again measures the path it made, which can differ in the last bits: a path
// within a billionth of its width of the old one on the page keeps the old one, so nothing
// changes.
if let (TextKind::OnPath { path: old, .. }, TextKind::OnPath { path: new, .. }) = (&t.kind, &mut out.kind)
&& same_path(&old.transformed(xf), &new.transformed(xf), w * 1e-9)
{
*new = old.clone();
}
refresh_bounds(&mut out);
out
}
/// The path of `preset` for `t` made upright through transform `xf` (see [`with_preset`]), in the
/// text's space, with its start bracket and its width on the page; None when the path isn't
/// finite.
fn preset_shape(t: &TextObject, preset: &str, xf: Affine) -> Option<(vectorcraft_geom::PathData, f64, f64)> {
// How long `(x, y)` in the text's space is on the page.
let on_page = |x: f64, y: f64| (xf * vectorcraft_geom::Point::new(x, y) - xf * vectorcraft_geom::Point::ZERO).hypot();
// The center on the page, and the width measured (0 when nothing can be measured).
let (c, longer) = match &t.kind {
TextKind::OnPath { path, .. } => path
.transformed(xf)
.bounds()
.filter(|b| b.is_finite())
.map_or((xf * vectorcraft_geom::Point::ZERO, 0.0), |b| (b.center(), b.width().max(b.height()))),
_ => {
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t).bounds;
match Some(lay).filter(|b| b.area() > 0.0 && b.is_finite()) {
Some(b) => (xf * b.center(), on_page(b.width(), 0.0).max(on_page(0.0, b.height()))),
None => (xf * Some(t.estimate_bounds()).filter(|b| b.is_finite()).map_or(vectorcraft_geom::Point::ZERO, |b| b.center()), 0.0),
}
}
};
// Without a measured width: four ems of the first character, else 48 pt.
let size = t.first_style().size;
let w = if longer.is_finite() && longer > 1e-6 {
longer
} else if size.is_finite() && size > 0.0 && on_page(size * 4.0, 0.0) > 1e-6 {
on_page(size * 4.0, 0.0)
} else {
48.0
};
let (path, start) = preset_path(preset, c, w);
let path = path.transformed(xf.inverse());
let finite = path.subpaths.iter().flat_map(|s| &s.anchors).all(|a| a.p.is_finite() && a.h_in.is_finite() && a.h_out.is_finite());
(finite && w.is_finite()).then_some((path, start, w))
}
/// Whether paths `a` and `b` have the same subpaths, closed alike, with every anchor and handle
/// within `eps` of each other.
fn same_path(a: &vectorcraft_geom::PathData, b: &vectorcraft_geom::PathData, eps: f64) -> bool {
let near = |p: vectorcraft_geom::Point, q: vectorcraft_geom::Point| (p - q).hypot() <= eps;
a.subpaths.len() == b.subpaths.len()
&& a.subpaths.iter().zip(&b.subpaths).all(|(s, t)| {
s.closed == t.closed
&& s.anchors.len() == t.anchors.len()
&& s.anchors.iter().zip(&t.anchors).all(|(x, y)| x.kind == y.kind && near(x.p, y.p) && near(x.h_in, y.h_in) && near(x.h_out, y.h_out))
})
}
/// Type › Dynamic Text Presets: put the selected point type and type on a path on a new path of
/// the preset's shape with Dynamic Text on, in one undo step, and none when nothing changes.
fn dynamic_text_preset(s: &mut Session, p: &Value) -> Result<Value> {
const C: &str = "type.dynamicTextPreset";
let key = "preset";
let preset = choice(p, C, key, &PRESETS)?.ok_or_else(|| bad(C, format!("`{key}` must be one of {}", PRESETS.join(", "))))?;
let align = match choice(p, C, "position", &["above", "center", "below"])? {
Some("center") => vectorcraft_doc::PathAlign::Center,
Some("below") => vectorcraft_doc::PathAlign::Ascender,
_ => vectorcraft_doc::PathAlign::Descender,
};
let flip = switch_or(p, "flip", false, C)?;
let (ids, changed) = {
let d = &s.doc()?.doc;
let ids = preset_ids(d, &targets(s, p)?);
if ids.is_empty() {
return Err(bad(C, "select point type or type on a path"));
}
let frames = threaded_ids(d, &ids);
if !frames.is_empty() {
return Err(bad(C, format!("type on a path in a thread cannot use Dynamic Text: {frames}")));
}
let changed: Vec<(NodeId, TextObject)> = ids
.iter()
.filter_map(|id| match d.node(*id).map(|n| &n.kind) {
Some(NodeKind::Text(t)) => Some((*id, with_preset(t, preset, align, flip))).filter(|(_, new)| new != &**t),
_ => None,
})
.collect();
(ids, changed)
};
if !changed.is_empty() {
s.edit("Dynamic Text", |d, _| {
for (id, new) in &changed {
if let Some(NodeKind::Text(t)) = d.node_mut(*id).map(|n| &mut n.kind) {
**t = new.clone();
}
}
Ok(())
})?;
}
let d = &s.doc()?.doc;
let fit =
ids.first().and_then(|id| path_text(d.node(*id))).map_or(1.0, |t| vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t).fit_scale);
Ok(json!({"ids": ids.iter().map(|i| i.0).collect::<Vec<_>>(), "fitScale": fit}))
}
// ---------- placeholder / insert ----------
/// VectorCraft's own placeholder copy (not the classical Latin text).
+194
View File
@@ -234,6 +234,200 @@ fn svg_export_writes_the_drawn_size() {
assert!(!sizes.iter().any(|v| (v - 12.0).abs() < 1e-3), "the stored size is not written: {sizes:?}");
}
/// 12 pt point type at (100, 100), selected, with its layout bounds.
fn point(s: &mut Session, content: &str) -> (NodeId, vectorcraft_geom::Rect) {
let r = s.execute("text.create", &json!({"x": 100, "y": 100, "text": content, "size": 12})).unwrap();
let id = NodeId(r["id"].as_u64().unwrap());
s.execute("select.set", &json!({"ids": [id.0]})).unwrap();
let b = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &text(s, id)).bounds;
(id, b)
}
/// Type on a path's path, start bracket and end bracket.
fn on_path(t: &TextObject) -> (vectorcraft_geom::PathData, f64, Option<f64>) {
match &t.kind {
vectorcraft_doc::TextKind::OnPath { path, start, end } => (path.clone(), *start, *end),
k => panic!("not type on a path: {k:?}"),
}
}
fn close(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-6
}
#[test]
fn presets_put_point_type_on_a_fitted_circle_arch_or_bow() {
let mut s = session();
let (id, b) = point(&mut s, "Round and round");
let w = b.width().max(b.height());
let before = undo_len(&s);
let r = s.execute("type.dynamicTextPreset", &json!({"preset": "circle"})).unwrap();
assert_eq!(r["ids"], json!([id.0]));
assert_eq!(undo_len(&s), before + 1);
assert_eq!(s.doc().unwrap().history.undo.last().unwrap().label, "Dynamic Text");
let t = text(&s, id);
assert!(t.dynamic_text);
assert_eq!(t.path_align, vectorcraft_doc::PathAlign::Descender, "above by default");
let (path, start, end) = on_path(&t);
assert!(path.is_closed());
assert_eq!((start, end), (0.75, None));
let pb = path.bounds().unwrap();
assert!(close(pb.width(), w) && close(pb.height(), w), "{pb:?} vs {w}");
assert!(close(pb.center().x, b.center().x) && close(pb.center().y, b.center().y), "{pb:?} vs {b:?}");
// The start bracket, 3/4 of the way round, is the circle's bottom.
let bottom = path.subpaths[0].anchors[3].p;
assert!(close(bottom.y, pb.y1) && close(bottom.x, pb.center().x), "{bottom:?}");
// Round a circle as wide as the text, the text grows to fill it and does not overflow.
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t);
assert!(lay.fit_scale > 2.0 && !lay.overflow, "{} {}", lay.fit_scale, lay.overflow);
assert_eq!(r["fitScale"], json!(lay.fit_scale));
assert_eq!(t.cached_bounds, Some(lay.bounds));
// Undo gives the point type back.
s.execute("edit.undo", &json!({})).unwrap();
assert!(matches!(text(&s, id).kind, vectorcraft_doc::TextKind::Point) && !text(&s, id).dynamic_text);
for (preset, up) in [("arch", true), ("bow", false)] {
s.execute("type.dynamicTextPreset", &json!({"preset": preset, "ids": [id.0]})).unwrap();
let t = text(&s, id);
let (path, start, end) = on_path(&t);
assert!(!path.is_closed() && t.dynamic_text);
assert_eq!((start, end), (0.0, None));
let a = &path.subpaths[0].anchors;
let (first, middle, last) = (a[0].p, a[1].p, a[a.len() - 1].p);
// Left to right, the chord as wide as the text, the arc rising (or falling) half its radius.
assert!(first.x < last.x && close(last.x - first.x, w) && close(first.y, last.y), "{preset}: {first:?} {last:?}");
let pb = path.bounds().unwrap();
assert!(close(pb.height(), w / 3f64.sqrt() / 2.0), "{preset}: {pb:?}");
assert!(close(pb.center().y, b.center().y), "{preset}: {pb:?} vs {b:?}");
assert_eq!(middle.y < first.y, up, "{preset}: {middle:?} {first:?}");
let lay = vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t);
assert!(lay.fit_scale > 1.0 && !lay.overflow, "{preset}: {}", lay.fit_scale);
s.execute("edit.undo", &json!({})).unwrap();
}
}
#[test]
fn preset_position_and_flip_set_align_to_path_and_direction() {
let mut s = session();
let (id, _) = point(&mut s, "Over the hill");
for (position, align) in [
("above", vectorcraft_doc::PathAlign::Descender),
("center", vectorcraft_doc::PathAlign::Center),
("below", vectorcraft_doc::PathAlign::Ascender),
] {
s.execute("type.dynamicTextPreset", &json!({"preset": "arch", "position": position})).unwrap();
assert_eq!(text(&s, id).path_align, align, "{position}");
}
// Flipped, the arch runs right to left and the text stands on its other side.
s.execute("type.dynamicTextPreset", &json!({"preset": "arch", "flip": true})).unwrap();
let t = text(&s, id);
let (path, start, end) = on_path(&t);
let a = &path.subpaths[0].anchors;
assert!(a[0].p.x > a[a.len() - 1].p.x, "{path:?}");
assert_eq!((start, end), (0.0, None));
assert_eq!(t.path_align, vectorcraft_doc::PathAlign::Descender);
assert!(!vectorcraft_text::layout(vectorcraft_text::FontDb::global(), &t).overflow);
// A flipped circle runs the other way round from its bottom.
s.execute("type.dynamicTextPreset", &json!({"preset": "circle", "flip": true})).unwrap();
let (path, start, end) = on_path(&text(&s, id));
assert_eq!((start, end), (0.25, None));
let at = path.subpaths[0].anchors[1].p;
assert!(close(at.y, path.bounds().unwrap().y1), "reversed, the bottom is a quarter of the way: {at:?}");
// The effect and Spacing of Type on a Path Options stay.
s.execute("type.pathOptions", &json!({"effect": "skew", "spacing": 5})).unwrap();
s.execute("type.dynamicTextPreset", &json!({"preset": "bow"})).unwrap();
let t = text(&s, id);
assert_eq!((t.path_effect.id(), t.path_spacing), ("skew", 5.0));
}
#[test]
fn presets_fit_type_on_a_path_to_its_path_and_record_nothing_when_nothing_changes() {
let mut s = session();
let id = on_line(&mut s, "Along the line");
s.execute("type.dynamicTextPreset", &json!({"preset": "bow"})).unwrap();
let (path, ..) = on_path(&text(&s, id));
let a = &path.subpaths[0].anchors;
// The bow is as wide as the 400 pt line, centered where it was.
assert!(close(a[0].p.x, 100.0) && close(a[a.len() - 1].p.x, 500.0), "{a:?}");
assert!(close(path.bounds().unwrap().center().y, 300.0));
// The same preset again: nothing changes and no undo step is recorded.
let before = undo_len(&s);
let r = s.execute("type.dynamicTextPreset", &json!({"preset": "bow"})).unwrap();
assert_eq!(r["ids"], json!([id.0]));
assert_eq!(undo_len(&s), before);
// Another position is a change.
s.execute("type.dynamicTextPreset", &json!({"preset": "bow", "position": "below"})).unwrap();
assert_eq!(undo_len(&s), before + 1);
// The fit follows later edits to the text.
let f = fit(&s, id);
s.execute("text.setText", &json!({"ids": [id.0], "text": "Along the line and on past it"})).unwrap();
assert!(fit(&s, id) < f * 0.8, "{} vs {f}", fit(&s, id));
// On a rotated object the arch still bulges up the page, and on a squashed one the circle is
// round on the page.
let (turned, _) = point(&mut s, "Turned");
s.execute("object.rotate", &json!({"ids": [turned.0], "angle": 45})).unwrap();
s.execute("type.dynamicTextPreset", &json!({"preset": "arch", "ids": [turned.0]})).unwrap();
let t = text(&s, turned);
let page = on_path(&t).0.transformed(t.xf);
let a = &page.subpaths[0].anchors;
assert!(close(a[0].p.y, a[2].p.y) && a[1].p.y < a[0].p.y && a[0].p.x < a[2].p.x, "{a:?}");
// The same preset again leaves the rotated arch as it is.
let before = undo_len(&s);
s.execute("type.dynamicTextPreset", &json!({"preset": "arch", "ids": [turned.0]})).unwrap();
assert_eq!(undo_len(&s), before);
let (squashed, _) = point(&mut s, "Squashed");
s.execute("object.scale", &json!({"ids": [squashed.0], "sx": 200, "sy": 50})).unwrap();
s.execute("type.dynamicTextPreset", &json!({"preset": "circle", "ids": [squashed.0]})).unwrap();
let t = text(&s, squashed);
let pb = on_path(&t).0.transformed(t.xf).bounds().unwrap();
assert!(close(pb.width(), pb.height()), "{pb:?}");
// Empty 12 pt point type gets a circle four ems (48 pt) across.
let (empty, _) = point(&mut s, "");
s.execute("type.dynamicTextPreset", &json!({"preset": "circle"})).unwrap();
let (path, ..) = on_path(&text(&s, empty));
let pb = path.bounds().unwrap();
assert!(close(pb.width(), 48.0) && close(pb.height(), 48.0), "{pb:?}");
}
#[test]
fn presets_refuse_bad_params_area_type_and_threads() {
let mut s = session();
let (id, _) = point(&mut s, "Point");
let before = undo_len(&s);
for p in [
json!({}),
json!({"preset": "square"}),
json!({"preset": 1}),
json!({"preset": "arch", "position": "top"}),
json!({"preset": "arch", "position": true}),
json!({"preset": "arch", "flip": "yes"}),
] {
let e = err(s.execute("type.dynamicTextPreset", &p));
let key = ["position", "flip"].into_iter().find(|k| p.get(k).is_some()).unwrap_or("preset");
assert!(e.contains(key), "{p}: {e}");
}
assert_eq!(undo_len(&s), before);
assert!(matches!(text(&s, id).kind, vectorcraft_doc::TextKind::Point));
// Area type alone is refused; beside point type it is left as it is.
let area =
s.execute("text.create", &json!({"x": 10, "y": 300, "text": "Area", "area": {"width": 100, "height": 50}})).unwrap()["id"].as_u64().unwrap();
s.execute("select.set", &json!({"ids": [area]})).unwrap();
assert!(err(s.execute("type.dynamicTextPreset", &json!({"preset": "circle"}))).contains("select point type or type on a path"));
s.execute("select.set", &json!({"ids": [area, id.0]})).unwrap();
let r = s.execute("type.dynamicTextPreset", &json!({"preset": "circle"})).unwrap();
assert_eq!(r["ids"], json!([id.0]));
assert!(matches!(text(&s, NodeId(area)).kind, vectorcraft_doc::TextKind::Area { .. }) && !text(&s, NodeId(area)).dynamic_text);
// Type on a path in a thread is refused by id.
let threaded = on_line(&mut s, "Threaded story");
let frame = s.execute("shape.rectangle", &json!({"x": 100, "y": 400, "width": 200, "height": 100})).unwrap()["id"].as_u64().unwrap();
s.execute("select.set", &json!({"ids": [threaded.0, frame]})).unwrap();
s.execute("text.thread.create", &json!({})).unwrap();
let before = undo_len(&s);
let e = err(s.execute("type.dynamicTextPreset", &json!({"preset": "arch", "ids": [threaded.0, id.0]})));
assert!(e.contains("in a thread") && e.contains(&threaded.0.to_string()), "{e}");
assert_eq!(undo_len(&s), before);
}
/// The id of the first text object in `s`'s document.
fn first_text(s: &Session) -> NodeId {
let mut found = None;
+2
View File
@@ -2274,6 +2274,8 @@ font-technology Variable Променливи
Optical Оптично
Optical Margin Alignment Подравняване по оптично поле
Dynamic Text Динамичен текст
Dynamic Text Presets Шаблони за динамичен текст
Arch Арка
Optimize for fast web view Оптимизиране за бърз уеб преглед
Optimized Оптимизирано
Options Опции
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -410,6 +410,8 @@
Create Outlines Kreiraj obrise
Optical Margin Alignment Optičko poravnanje margina
Dynamic Text Dinamički tekst
Dynamic Text Presets Predlošci dinamičkog teksta
Arch Svod
Fill with Placeholder Text Ispuni tekstom čuvara mjesta
Insert Inline Symbol Umetni simbol u redu
Show Hidden Characters Prikaži skrivene znakove
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -2907,6 +2907,9 @@ digits Malayalam Malaiàlam
Optical Òptic
Optical Margin Alignment Alineació òptica de marges
Dynamic Text Text dinàmic
Dynamic Text Presets Valors predefinits de text dinàmic
Arch Volta
@msg select point type or type on a path seleccioneu text de punt o text sobre un traçat
@msg type on a path in a thread cannot use Dynamic Text: {frames} el text sobre un traçat d’un text enllaçat no pot fer servir el text dinàmic: {frames}
Optimize for fast web view Optimitza per a la visualització web ràpida
Optimized Optimitzat
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -417,6 +417,9 @@
Create Outlines Převést na křivky
Optical Margin Alignment Optické zarovnání okrajů
Dynamic Text Dynamický text
Dynamic Text Presets Předvolby dynamického textu
Arch Klenba
@msg select point type or type on a path vyberte bodový text nebo text na cestě
@msg type on a path in a thread cannot use Dynamic Text: {frames} text na cestě ve zřetězeném textu nemůže používat dynamický text: {frames}
Fill with Placeholder Text Vyplnit zástupným textem
Insert Inline Symbol Vložit symbol do textu
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -378,6 +378,8 @@
Create Outlines Opret konturer
Optical Margin Alignment Optisk marginjustering
Dynamic Text Dynamisk tekst
Dynamic Text Presets Forudindstillinger for dynamisk tekst
Arch Hvælving
Fill with Placeholder Text Udfyld med pladsholdertekst
Insert Inline Symbol Indsæt indlejret symbol
Show Hidden Characters Vis skjulte tegn
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -2300,6 +2300,9 @@ file-filter Templates Vorlagen
Optical Optisch
Optical Margin Alignment Optischer Randausgleich
Dynamic Text Dynamischer Text
Dynamic Text Presets Vorgaben für dynamischen Text
Arch Rundbogen
@msg select point type or type on a path wählen Sie Punkttext oder Pfadtext aus
@msg type on a path in a thread cannot use Dynamic Text: {frames} Pfadtext in verkettetem Text kann keinen dynamischen Text verwenden: {frames}
Optimize for fast web view Für schnelle Webanzeige optimieren
Optimized Optimiert
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -3074,6 +3074,9 @@ theme Appearance Εμφάνιση
Optical Οπτική
Optical Margin Alignment Οπτική στοίχιση περιθωρίου
Dynamic Text Δυναμικό κείμενο
Dynamic Text Presets Προεπιλογές δυναμικού κειμένου
Arch Αψίδα
@msg select point type or type on a path επιλέξτε κείμενο σημείου ή κείμενο σε διαδρομή
@msg type on a path in a thread cannot use Dynamic Text: {frames} το κείμενο σε διαδρομή μέσα σε συνδεδεμένο κείμενο δεν μπορεί να χρησιμοποιήσει δυναμικό κείμενο: {frames}
Optimize for fast web view Βελτιστοποίηση για γρήγορη προβολή στον ιστό
Options for Selection… Επιλογές για την επιλογή…
Can't render this file because it is too large.
+3
View File
@@ -2346,6 +2346,9 @@ font-technology Variable Variables
Optical Óptico
Optical Margin Alignment Alineación óptica de márgenes
Dynamic Text Texto dinámico
Dynamic Text Presets Ajustes preestablecidos de texto dinámico
Arch Bóveda
@msg select point type or type on a path seleccione texto de punto o texto en trazado
@msg type on a path in a thread cannot use Dynamic Text: {frames} el texto en trazado de un texto enlazado no puede usar texto dinámico: {frames}
Optimize for fast web view Optimizar para vista web rápida
Optimized Optimizado
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -414,6 +414,9 @@
Create Outlines Muunna ääriviivoiksi
Optical Margin Alignment Optinen reunustasaus
Dynamic Text Dynaaminen teksti
Dynamic Text Presets Dynaamisen tekstin esiasetukset
Arch Holvi
@msg select point type or type on a path valitse pisteteksti tai polkuteksti
@msg type on a path in a thread cannot use Dynamic Text: {frames} ketjutetun tekstin polkuteksti ei voi käyttää dynaamista tekstiä: {frames}
Fill with Placeholder Text Täytä paikkamerkkitekstillä
Insert Inline Symbol Lisää tekstinsisäinen symboli
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -2338,6 +2338,9 @@ font-technology Variable Variables
Optical Optique
Optical Margin Alignment Alignement optique des marges
Dynamic Text Texte dynamique
Dynamic Text Presets Paramètres prédéfinis du texte dynamique
Arch Voûte
@msg select point type or type on a path sélectionnez du texte de point ou du texte curviligne
@msg type on a path in a thread cannot use Dynamic Text: {frames} le texte curviligne d’un texte lié ne peut pas utiliser le texte dynamique : {frames}
Optimize for fast web view Optimiser pour un affichage web rapide
Optimized Optimisé
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -3163,6 +3163,9 @@ antiAlias Sharp शार्प
Optical ऑप्टिकल
Optical Margin Alignment ऑप्टिकल मार्जिन संरेखण
Dynamic Text डायनैमिक टेक्स्ट
Dynamic Text Presets डायनैमिक टेक्स्ट प्रीसेट
Arch मेहराब
@msg select point type or type on a path पॉइंट टाइप या पाथ पर टाइप चुनें
@msg type on a path in a thread cannot use Dynamic Text: {frames} थ्रेडेड टेक्स्ट में पाथ पर टाइप डायनैमिक टेक्स्ट का उपयोग नहीं कर सकता: {frames}
Optimize for fast web view तेजी से वेब दृश्य के लिए ऑप्टिमाइज़
Optimized ऑप्टिमाइज़्ड
Can't render this file because it is too large.
+3
View File
@@ -2840,6 +2840,9 @@ file-filter Templates Sablonok
Optical Optikai
Optical Margin Alignment Optikai margóigazítás
Dynamic Text Dinamikus szöveg
Dynamic Text Presets Dinamikus szöveg előbeállításai
Arch Boltív
@msg select point type or type on a path jelöljön ki pontszöveget vagy útvonalszöveget
@msg type on a path in a thread cannot use Dynamic Text: {frames} a láncolt szövegben lévő útvonalszöveg nem használhat dinamikus szöveget: {frames}
Optimize for fast web view Optimalizálás gyors webes megtekintéshez
Optimized Optimalizált
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -378,6 +378,8 @@
Create Outlines Buat garis luar
Optical Margin Alignment Perataan margin optik
Dynamic Text Teks dinamis
Dynamic Text Presets Preset teks dinamis
Arch Pelengkung
Fill with Placeholder Text Isi dengan teks placeholder
Insert Inline Symbol Sisipkan simbol sebaris
Show Hidden Characters Tampilkan karakter tersembunyi
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
378
379
380
381
382
383
384
385
+3
View File
@@ -2376,6 +2376,9 @@ file-filter Templates Modelli
Optical Ottica
Optical Margin Alignment Allineamento ottico dei margini
Dynamic Text Testo dinamico
Dynamic Text Presets Predefiniti testo dinamico
Arch Volta
@msg select point type or type on a path seleziona testo per punti o testo su tracciato
@msg type on a path in a thread cannot use Dynamic Text: {frames} il testo su tracciato in un testo concatenato non può usare il testo dinamico: {frames}
Optimize for fast web view Ottimizza per visualizzazione web veloce
Optimized Ottimizzato
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -445,6 +445,9 @@ digits Malayalam マラヤーラム数字
Create Outlines アウトラインを作成
Optical Margin Alignment 最適なマージン揃え
Dynamic Text ダイナミックテキスト
Dynamic Text Presets ダイナミックテキストプリセット
Arch アーチ
@msg select point type or type on a path ポイント文字またはパス上文字を選択してください
@msg type on a path in a thread cannot use Dynamic Text: {frames} スレッドテキスト内のパス上文字にはダイナミックテキストを使用できません: {frames}
Fill with Placeholder Text サンプルテキストの割り付け
Insert Inline Symbol インラインシンボルを挿入
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -343,6 +343,9 @@
Create Outlines კონტურებად გარდაქმნა
Optical Margin Alignment ოპტიკური არშიის სწორება
Dynamic Text დინამიკური ტექსტი
Dynamic Text Presets დინამიკური ტექსტის წინასწარი პარამეტრები
Arch თაღი
@msg select point type or type on a path აირჩიეთ წერტილოვანი ტექსტი ან ტრაექტორიაზე ტექსტი
@msg type on a path in a thread cannot use Dynamic Text: {frames} დაკავშირებულ ტექსტში ტრაექტორიაზე ტექსტი ვერ გამოიყენებს დინამიკურ ტექსტს: {frames}
Fill with Placeholder Text შემავსებელი ტექსტით შევსება
Insert Inline Symbol ჩადგმული სიმბოლოს ჩასმა
Can't render this file because it is too large.
+3
View File
@@ -438,6 +438,9 @@ digits Malayalam 말라얄람어
Create Outlines 윤곽선 만들기
Optical Margin Alignment 시각적 여백 정렬
Dynamic Text 동적 텍스트
Dynamic Text Presets 동적 텍스트 사전 설정
Arch 아치
@msg select point type or type on a path 포인트 문자 또는 패스 위 문자를 선택하십시오
@msg type on a path in a thread cannot use Dynamic Text: {frames} 연결된 문자의 패스 위 문자에는 동적 텍스트를 사용할 수 없습니다: {frames}
Fill with Placeholder Text 자리 표시 문자로 채우기
Insert Inline Symbol 인라인 심볼 삽입
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -392,6 +392,8 @@
Create Outlines Omzetten in curven
Optical Margin Alignment Optische marge-uitlijning
Dynamic Text Dynamische tekst
Dynamic Text Presets Voorinstellingen voor dynamische tekst
Arch Gewelf
Fill with Placeholder Text Vullen met plaatshoudertekst
Insert Inline Symbol Inline-symbool invoegen
Show Hidden Characters Verborgen tekens tonen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -376,6 +376,9 @@
Create Outlines Utwórz kontury
Optical Margin Alignment Optyczne wyrównanie marginesów
Dynamic Text Tekst dynamiczny
Dynamic Text Presets Ustawienia predefiniowane tekstu dynamicznego
Arch Sklepienie
@msg select point type or type on a path zaznacz tekst punktowy lub tekst na ścieżce
@msg type on a path in a thread cannot use Dynamic Text: {frames} tekst na ścieżce w tekście wątkowym nie może używać tekstu dynamicznego: {frames}
Fill with Placeholder Text Wypełnij tekstem zastępczym
Insert Inline Symbol Wstaw symbol w tekście
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1942,6 +1942,9 @@ antiAlias Sharp Nítido
Optical Óptico
Optical Margin Alignment Alinhamento de Margem Óptica
Dynamic Text Texto Dinâmico
Dynamic Text Presets Predefinições de Texto Dinâmico
Arch Abóbada
@msg select point type or type on a path selecione texto de ponto ou texto em um caminho
@msg type on a path in a thread cannot use Dynamic Text: {frames} o texto em um caminho de um texto encadeado não pode usar Texto Dinâmico: {frames}
Optimize for fast web view Otimizar para visualização rápida na web
Optimized Otimizado
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3
View File
@@ -2371,6 +2371,9 @@ file-filter Templates Шаблоны
Optical Оптический
Optical Margin Alignment Оптическое выравнивание полей
Dynamic Text Динамический текст
Dynamic Text Presets Наборы параметров динамического текста
Arch Арка
@msg select point type or type on a path выберите точечный текст или текст по контуру
@msg type on a path in a thread cannot use Dynamic Text: {frames} текст по контуру в связанном тексте не может использовать динамический текст: {frames}
Optimize for fast web view Оптимизировать для быстрого просмотра в web
Optimized Оптимизировано
Can't render this file because it is too large.
+3
View File
@@ -2285,6 +2285,9 @@ font-technology Variable Variabla
Optical Optisk
Optical Margin Alignment Optisk marginaljustering
Dynamic Text Dynamisk text
Dynamic Text Presets Förinställningar för dynamisk text
Arch Valv
@msg select point type or type on a path markera punkttext eller text på bana
@msg type on a path in a thread cannot use Dynamic Text: {frames} text på bana i länkad text kan inte använda dynamisk text: {frames}
Optimize for fast web view Optimera för snabb webbvisning
Optimized Optimerad
Can't render this file because it contains an unexpected character in line 5 and column 25.
+3
View File
@@ -2157,6 +2157,9 @@ shape Open Açık
Optical Optik
Optical Margin Alignment Optik Kenar Boşluğu Hizalaması
Dynamic Text Dinamik Metin
Dynamic Text Presets Dinamik Metin Hazır Ayarları
Arch Kemer
@msg select point type or type on a path nokta metni veya yol üzerindeki yazıyı seçin
@msg type on a path in a thread cannot use Dynamic Text: {frames} zincirli metindeki yol üzerindeki yazı dinamik metin kullanamaz: {frames}
Optimize for fast web view Hızlı web görünümü için optimize edin
Optimized Optimize edilmiş
Can't render this file because it contains an unexpected character in line 5 and column 25.
+3
View File
@@ -2368,6 +2368,9 @@ file-filter Templates Шаблони
Optical Оптичний
Optical Margin Alignment Оптичне вирівнювання полів
Dynamic Text Динамічний текст
Dynamic Text Presets Набори параметрів динамічного тексту
Arch Арка
@msg select point type or type on a path виберіть точковий текст або текст за контуром
@msg type on a path in a thread cannot use Dynamic Text: {frames} текст за контуром у пов’язаному тексті не може використовувати динамічний текст: {frames}
Optimize for fast web view Оптимізувати для швидкого перегляду web
Optimized Оптимізовано
Can't render this file because it contains an unexpected character in line 182 and column 5.
+3
View File
@@ -2653,6 +2653,9 @@ digits Hindi Hindi
Optical Quang học
Optical Margin Alignment Căn lề quang học
Dynamic Text Văn bản động
Dynamic Text Presets Thiết lập sẵn văn bản động
Arch Vòm
@msg select point type or type on a path chọn chữ điểm hoặc chữ trên đường dẫn
@msg type on a path in a thread cannot use Dynamic Text: {frames} chữ trên đường dẫn trong văn bản chuỗi không thể dùng văn bản động: {frames}
Optimize for fast web view Tối ưu để xem web nhanh
Optimized Đã tối ưu
Can't render this file because it contains an unexpected character in line 204 and column 5.
+3
View File
@@ -2244,6 +2244,9 @@ file-filter Templates 模板
Optical 视觉
Optical Margin Alignment 视觉边界对齐
Dynamic Text 动态文本
Dynamic Text Presets 动态文本预设
Arch 拱形
@msg select point type or type on a path 请选择点文字或路径文字
@msg type on a path in a thread cannot use Dynamic Text: {frames} 串接文本中的路径文字不能使用动态文本:{frames}
Optimize for fast web view 优化快速 Web 查看
Optimized 优化
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -3335,6 +3335,9 @@ file-filter Templates 範本
Optical 視覺
Optical Margin Alignment 視覺邊界對齊
Dynamic Text 動態文字
Dynamic Text Presets 動態文字預設集
Arch 拱形
@msg select point type or type on a path 請選取點狀文字或路徑文字
@msg type on a path in a thread cannot use Dynamic Text: {frames} 串連文字中的路徑文字無法使用動態文字:{frames}
Optimize for fast web view 最佳化以便快速網頁檢視
Optimized 最佳化
Can't render this file because it contains an unexpected character in line 6 and column 47.
+35
View File
@@ -358,6 +358,41 @@ fn dynamic_text_is_a_checked_type_menu_item() {
assert_eq!(checked(&app, id, &Value::Null), Some(true));
}
/// Type › Dynamic Text Presets › Circle, Arch and Bow run `type.dynamicTextPreset` with their
/// preset, enabled for point type and type on a path but not for area type alone.
#[test]
fn dynamic_text_presets_are_a_type_submenu() {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({})).unwrap();
let items: Vec<(&str, &str, Value)> = menu_tree()
.into_iter()
.filter(|(menu, _)| *menu == "Type")
.flat_map(|(_, items)| items)
.filter_map(|item| match item {
Item::Sub("Dynamic Text Presets", children) => Some(children),
_ => None,
})
.flatten()
.filter_map(|item| match item {
Item::Cmd(label, id, p) => Some((label, id, p)),
_ => None,
})
.collect();
let id = "type.dynamicTextPreset";
assert_eq!(items, [("Circle", id, json!({"preset": "circle"})), ("Arch", id, json!({"preset": "arch"})), ("Bow", id, json!({"preset": "bow"}))]);
assert!(!enabled(&app, id));
let area = app.run("text.create", json!({"x": 10, "y": 200, "text": "Area", "area": {"width": 100, "height": 50}})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [area]})).unwrap();
assert!(!enabled(&app, id), "area type");
let point = app.run("text.create", json!({"x": 10, "y": 10, "text": "Point"})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [point]})).unwrap();
assert!(enabled(&app, id));
app.run(id, json!({"preset": "arch"})).unwrap();
// Now type on a path with Dynamic Text on, so Type › Dynamic Text is checked.
assert!(enabled(&app, id));
assert_eq!(checked(&app, "type.dynamicText", &Value::Null), Some(true));
}
/// Type › Optical Margin Alignment menu check uses `shared_text_flag`; mixed values stay unchecked.
#[test]
fn optical_margin_alignment_menu_unchecked_when_shared_flag_none() {
+8
View File
@@ -400,6 +400,14 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec<Item>)> {
.collect(),
),
c("Dynamic Text", "type.dynamicText"),
sub(
"Dynamic Text Presets",
vec![
cp("Circle", "type.dynamicTextPreset", json!({"preset": "circle"})),
cp("Arch", "type.dynamicTextPreset", json!({"preset": "arch"})),
cp("Bow", "type.dynamicTextPreset", json!({"preset": "bow"})),
],
),
sub(
"Threaded Text",
vec![
+26 -1
View File
@@ -1480,7 +1480,7 @@ These commands refuse a string, a number or another value that is not true or fa
and `text.createInPath`; `underline`, `strikethrough`, `allCaps`, `proportionalMetrics`, `tateChuYoko`, `standardVerticalRoman` and `noBreak`
in `text.setRangeStyle` and `text.setFormat`, which also checks `hyphenate`, `hyphenateCapitalized` and
`romanHangingPunctuation`; `effective` and `story` in `text.getRange`; `story` in `text.setRangeStyle`; `on` in `type.opticalMarginAlignment`, `type.dynamicText`, `type.systemLayout`
and `type.fractionalWidths`; `remove` and `overhang` in `type.ruby`; `expandInstances` in `symbol.delete`; `alt` in
and `type.fractionalWidths`; `flip` in `type.dynamicTextPreset`; `remove` and `overhang` in `type.ruby`; `expandInstances` in `symbol.delete`; `alt` in
`symbol.spray` and `symbol.adjust`; `spot` and `global` in `swatch.new` and `swatch.edit`; `unlink` in
`swatch.delete`; `fromArtwork`, `toGlobal` and `includeTints` in `swatch.newGroup`; `global` and `selection` in
`swatch.addUsedColors`; `visible` in `appearance.setItem`; `copy` in `appearance.transfer`; `reverse` and `append` in
@@ -7721,6 +7721,31 @@ so in a warning.
{"name":"run_command","arguments":{"command":"type.dynamicText","params":{"on":true}}}
```
`type.dynamicTextPreset {preset: "circle"|"arch"|"bow", position?: "above"|"center"|"below", flip?: bool, ids?}`
(Type › Dynamic Text Presets › Circle, Arch and Bow) puts the selected point type and type on a path on a new path
of the preset's shape and turns Dynamic Text on, so the text is sized to fill it. Circle puts the text on a circular
path, Arch on an upward-curving path and Bow on a downward-curving one; `position` sets where the text sits against
the path (above, center or below) and `flip` turns its direction
([Dynamic Text overview](https://helpx.adobe.com/photoshop/desktop/text-typography/work-with-dynamic-text/dynamic-text-overview.html)).
The shapes and the rest of this entry are VectorCraft's own rules. The circle is as wide as the text, and the text
runs clockwise round all of it from its bottom. Arch and Bow are a 120° arc of a circle whose chord is as wide as the
text, bulging up or down, and the text runs along it from left to right. The width is, for point type, the length on the
page of the longer side of the text's bounds, and for type on a path the longer side of its path's bounds on the
page, so the same preset again leaves it as it is; the new path is centered where they were, and
the shape is upright on the page when the object is rotated or scaled. Empty point type gets a path four ems of its
first character wide. `position` (omitted:
above) sets Type on a Path Options › Align to Path: above is Descender, center is Center and below is Ascender.
`flip: true` then turns the text to the other side of its path, running the other way, as Type on a Path Options ›
Flip does. Type on a Path Options › effect and Spacing stay as they are, and Type on a Path Options, the brackets and
Type › Dynamic Text change the result afterwards. Area type in the selection is left as it is; with no point type
or type on a path selected the command refuses with `` select point type or type on a path ``, and type on a path in
a thread is refused as `type.dynamicText` refuses it. A preset that would leave every object as it is records no
undo step. It replies `{ids, fitScale}` (the first object's factor).
```json
{"name":"run_command","arguments":{"command":"type.dynamicTextPreset","params":{"preset":"arch","position":"center"}}}
```
## Paragraph attributes per paragraph
Each paragraph (text split at `\n`) has its own alignment, indents, space before and after, hyphenation,
+1
View File
@@ -1181,6 +1181,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
Type on a Path Options › Align to Path (option names as in [Create type on a path](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/create-type-on-a-path.html)) moves each character by its own font, size and vertical scale: Ascender runs the top of the font's em box along the path (the ideographic em box that its `BASE` table, its typographic or `hhea` box or its ideographs give, else an em box centered on the cap height), Descender runs the bottom of the font's bounding box along it, and Center runs a line at half the height of the top of the font's H above the baseline along it. In vertical type, Ascender and Descender move the characters half an em, and Center leaves them on the path. These distances are VectorCraft's own values (`tests::on_path_align_runs_each_glyph_by_its_own_metrics`, `tests::on_path_align_finds_the_em_box_of_each_kind_of_face`, `tests::on_path_align_in_vertical_type_moves_half_an_em`).
Type › Dynamic Text (`type.dynamicText`, also on the Contextual Task Bar for type on a path) sizes type on a path to fill the span between its start and end brackets and fits it again after each edit to the text, the path or a bracket ([Dynamic Text overview](https://helpx.adobe.com/photoshop/desktop/text-typography/work-with-dynamic-text/dynamic-text-overview.html), [start and end points](https://helpx.adobe.com/photoshop/desktop/text-typography/work-with-dynamic-text/reposition-start-and-end-points.html)). VectorCraft's own rules: one factor scales every size, leading and baseline shift; the first character's size moves in 0.1 pt steps within 0.1 to 1,296 pt; turning it off or threading the type keeps the drawn sizes; SVG, EPS and the editing data write the drawn sizes (`crates/text/src/tests/fit.rs`, `crates/engine/src/tests/dynamic_text.rs`).
Dynamic Text on area type is Block (Type › Dynamic Text, the Contextual Task Bar, Area Type Options › Block, the Properties panel's Dynamic Text checkbox with Word Gap and Line Gap; [Dynamic Text overview](https://helpx.adobe.com/photoshop/desktop/text-typography/work-with-dynamic-text/dynamic-text-overview.html)): each line as typed is set to the frame's width by its font size, again after every edit to the text or the frame. VectorCraft's own rules: lines are not broken again, the first character's size moves in 0.1 pt steps within 0.1 to 1,296 pt, Word Gap (0 to 1,000 %, default 100) is the Desired word spacing and Line Gap (-1,296 to 1,296 pt, default 0) is added to each size for the leading, the sizes are written into the characters, and threaded frames cannot use it (`crates/engine/src/cmd/typecmd/block_tests.rs`).
Type › Dynamic Text Presets › Circle, Arch and Bow (`type.dynamicTextPreset`) put point type or type on a path on a circular, upward-curving or downward-curving path with Dynamic Text on, with the text's position against the path (above, center, below) and its direction ([Dynamic Text overview](https://helpx.adobe.com/photoshop/desktop/text-typography/work-with-dynamic-text/dynamic-text-overview.html)). VectorCraft's own rules: the circle is as wide as the text and the text starts at its bottom; Arch and Bow are a 120° arc whose chord is as wide as the text; above, center and below are Align to Path's Descender, Center and Ascender (`crates/engine/src/tests/dynamic_text.rs`).
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.
The frames of threaded text share one writing direction: Type › Type Orientation (`type.orientation.vertical`, `type.orientation.horizontal`) in one frame sets every frame, threading frames gives the thread its first frame's direction, frames taken out of a thread keep theirs, and the story flows through every frame in the thread's direction.
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. Justification's Glyph Scaling leaves the glyphs that hang at their width, so they hang exactly their share, and Standard burasagari counts what hangs at a line's start as room before hanging a comma; `crates/text/src/tests/hang.rs` checks whole paragraphs with both composers, ragged, centred and justified, with Line-end Punctuation Half Width and bidirectional lines.