From 4bc50fa21f93d67f30822db796a18313171c5d82 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Wed, 11 Nov 2026 17:11:54 -0800 Subject: [PATCH] Type: Dynamic Text Presets put type on a circle, arch or bow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Type › Dynamic Text Presets › Circle, Arch and Bow (`type.dynamicTextPreset {preset, position?, flip?, ids?}`) put the selected point type and type on a path on a new path of that shape and turn Dynamic Text on, so the text is sized to fill it. - Circle is a circle as wide as the text, the text running clockwise from its bottom; Arch and Bow are a 120° arc whose chord is as wide as the text, bulging up or down. The shape is upright on the page when the object is rotated or scaled. - `position` above, center or below sets Align to Path to Descender, Center or Ascender; `flip` turns the text to the other side of its path. Effect and Spacing stay. - Area type is left as it is, threaded type on a path is refused, and a preset that changes nothing records no undo step. Docs: docs/mcp.md, docs/target-app-parity.md. Part of #4134. --- crates/engine/src/cmd/typemenu.rs | 199 ++++++++++++++++++++++++ crates/engine/src/tests/dynamic_text.rs | 194 +++++++++++++++++++++++ crates/ui-egui/src/i18n/bg.tsv | 2 + crates/ui-egui/src/i18n/bs.tsv | 2 + crates/ui-egui/src/i18n/ca.tsv | 3 + crates/ui-egui/src/i18n/cs.tsv | 3 + crates/ui-egui/src/i18n/da.tsv | 2 + crates/ui-egui/src/i18n/de.tsv | 3 + crates/ui-egui/src/i18n/el.tsv | 3 + crates/ui-egui/src/i18n/es.tsv | 3 + crates/ui-egui/src/i18n/fi.tsv | 3 + crates/ui-egui/src/i18n/fr.tsv | 3 + crates/ui-egui/src/i18n/hi.tsv | 3 + crates/ui-egui/src/i18n/hu.tsv | 3 + crates/ui-egui/src/i18n/id.tsv | 2 + crates/ui-egui/src/i18n/it.tsv | 3 + crates/ui-egui/src/i18n/ja.tsv | 3 + crates/ui-egui/src/i18n/ka.tsv | 3 + crates/ui-egui/src/i18n/ko.tsv | 3 + crates/ui-egui/src/i18n/nl.tsv | 2 + crates/ui-egui/src/i18n/pl.tsv | 3 + crates/ui-egui/src/i18n/pt-br.tsv | 3 + crates/ui-egui/src/i18n/ru.tsv | 3 + crates/ui-egui/src/i18n/sv.tsv | 3 + crates/ui-egui/src/i18n/tr.tsv | 3 + crates/ui-egui/src/i18n/uk.tsv | 3 + crates/ui-egui/src/i18n/vi.tsv | 3 + crates/ui-egui/src/i18n/zh-hans.tsv | 3 + crates/ui-egui/src/i18n/zh-hant.tsv | 3 + crates/ui-egui/src/menus/tests.rs | 35 +++++ crates/ui-egui/src/menus/tree.rs | 8 + docs/mcp.md | 27 +++- docs/target-app-parity.md | 1 + 33 files changed, 539 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/typemenu.rs b/crates/engine/src/cmd/typemenu.rs index 03681c556..faaaf2145 100644 --- a/crates/engine/src/cmd/typemenu.rs +++ b/crates/engine/src/cmd/typemenu.rs @@ -69,6 +69,15 @@ pub fn specs() -> Vec { 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 { + 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 { + 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::>(), "fitScale": fit})) +} + // ---------- placeholder / insert ---------- /// VectorCraft's own placeholder copy (not the classical Latin text). diff --git a/crates/engine/src/tests/dynamic_text.rs b/crates/engine/src/tests/dynamic_text.rs index 3512db9c5..ae1a72fa0 100644 --- a/crates/engine/src/tests/dynamic_text.rs +++ b/crates/engine/src/tests/dynamic_text.rs @@ -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) { + 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; diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 35765c6e7..b7d6648b6 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -2264,6 +2264,8 @@ font-technology Variable Променливи Optical Оптично Optical Margin Alignment Подравняване по оптично поле Dynamic Text Динамичен текст + Dynamic Text Presets Шаблони за динамичен текст + Arch Арка Optimize for fast web view Оптимизиране за бърз уеб преглед Optimized Оптимизирано Options Опции diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index cb0d927fd..5b9cd95da 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -400,6 +400,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 diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 3b186f866..3a3763e97 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -2892,6 +2892,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 diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index d0469b1f4..7eb7d8c3b 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -407,6 +407,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 diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index 239a5236a..1ff55af55 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -368,6 +368,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 diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 1a62bc991..362ed03b9 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2290,6 +2290,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 diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index dfc550e66..bdf0e9c5b 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -3062,6 +3062,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… Επιλογές για την επιλογή… diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 8644b8501..e0b4edb01 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -2336,6 +2336,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 diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index d0d870dce..5d2e8470b 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -404,6 +404,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 diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 42f8d664d..d03566bc2 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2328,6 +2328,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é diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index 55707d511..ddeb3355f 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -3147,6 +3147,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 ऑप्टिमाइज़्ड diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index acd57f1fe..adf79ad42 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -2830,6 +2830,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 diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 8a37686d3..53e04a5ea 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -368,6 +368,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 diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index a91895759..48dfe0c26 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2366,6 +2366,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 diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 5dee1a8f1..bee01610a 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -435,6 +435,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 インラインシンボルを挿入 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index c71c586f0..0f95f1ef0 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -333,6 +333,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 ჩადგმული სიმბოლოს ჩასმა diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 3e582ae89..82b7ecf74 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -428,6 +428,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 인라인 심볼 삽입 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index ec13492d2..33a7706b4 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -382,6 +382,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 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index c32c2e260..7ec4b3b40 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -366,6 +366,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 diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 487532692..2859a6735 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -1932,6 +1932,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 diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index ea69c1fca..4d7cd8888 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2361,6 +2361,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 Оптимизировано diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index eb40d3630..9798d3a05 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -2275,6 +2275,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 diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index adc8328bb..615a6309d 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -2147,6 +2147,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ş diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 86d6241c5..cfee441de 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2358,6 +2358,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 Оптимізовано diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 3d7277d62..f6433ad2d 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -2643,6 +2643,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 diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 68bb795a2..fbd806400 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2234,6 +2234,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 优化 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index b50bed47d..27fd3db16 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -3314,6 +3314,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 最佳化 diff --git a/crates/ui-egui/src/menus/tests.rs b/crates/ui-egui/src/menus/tests.rs index 8591c4d89..3d1577e1b 100644 --- a/crates/ui-egui/src/menus/tests.rs +++ b/crates/ui-egui/src/menus/tests.rs @@ -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() { diff --git a/crates/ui-egui/src/menus/tree.rs b/crates/ui-egui/src/menus/tree.rs index 20dc91566..916b8b311 100644 --- a/crates/ui-egui/src/menus/tree.rs +++ b/crates/ui-egui/src/menus/tree.rs @@ -400,6 +400,14 @@ pub fn menu_tree_named(english_names: bool) -> Vec<(&'static str, Vec)> { .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![ diff --git a/docs/mcp.md b/docs/mcp.md index ef2b8df02..817abfd6e 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1444,7 +1444,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 @@ -7592,6 +7592,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, diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 490dfb38d..faa328344 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1170,6 +1170,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.