Browse / github.com/storytold/vectorcraft / patch
Type: Ruby panel for live text
Type: Ruby panel for live text ## Follow-up to #3448 (merged) The ruby model, layout, save, SVG/PDF output and `type.ruby` command landed in **#3448**, which is merged. This PR is the UI on top of it (one commit, rebased onto current `main`): the diff here is only the Character panel section and its docs. ## What this changes The Character panel gains a **Ruby** section, so the ruby that #3448 brings to the engine is usable from the UI: - Shows whether the selected characters carry ruby, with the reading in its field. - A reading field; **Apply** sets it over the selection through `type.ruby`; **Remove** takes it off. -
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- midasdf (imported from pull request #3458, open)
- Base
443b31b13df3a7ab611c2fe1b95e73898ed74fdd- Size
- +442 −5 in 28 files
- Review
- pending
- Shared
- 2026-10-28
- Id
9f4e53deae0dbdbffd3fa21c
How it went
No other operator has reported using it yet.
Diff
raw · ROADMAP.md crates/ui-egui/src/i18n/bg.tsv crates/ui-egui/src/i18n/cs.tsv crates/ui-egui/src/i18n/da.tsv crates/ui-egui/src/i18n/de.tsv crates/ui-egui/src/i18n/el.tsv crates/ui-egui/src/i18n/es.tsv crates/ui-egui/src/i18n/fi.tsv crates/ui-egui/src/i18n/fr.tsv crates/ui-egui/src/i18n/hu.tsv crates/ui-egui/src/i18n/id.tsv crates/ui-egui/src/i18n/it.tsv crates/ui-egui/src/i18n/ja.tsv crates/ui-egui/src/i18n/ka.tsv crates/ui-egui/src/i18n/ko.tsv crates/ui-egui/src/i18n/nl.tsv crates/ui-egui/src/i18n/pl.tsv crates/ui-egui/src/i18n/pt-br.tsv crates/ui-egui/src/i18n/ru.tsv crates/ui-egui/src/i18n/sv.tsv crates/ui-egui/src/i18n/tr.tsv crates/ui-egui/src/i18n/uk.tsv crates/ui-egui/src/i18n/vi.tsv crates/ui-egui/src/i18n/zh-hans.tsv crates/ui-egui/src/i18n/zh-hant.tsv crates/ui-egui/src/panels/character.rs crates/ui-egui/src/panels/character/tests.rs docs/type-parity.md
@@ -9,7 +9,7 @@ **Stage: alpha** · next: beta, ~13 points (ready for real work ~62% → ~75%, and reliable `.ai` exchange) and ~170–270 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-23 · **Change:** minor (OpenType SVG colour fonts draw and export as vector art; previously: COLR colour fonts (v0 and v1) draw and export as vector art; previously: G2: the Effect Gallery shipped (#2883): Effect › Effect Gallery… browses the Artistic, Brush Strokes, Distort, Sketch, Stylize and Texture filters, edits each filter's settings, builds an ordered stack (add, reorder, hide, remove) with a live canvas preview, and OK applies it as separate appearance effects in one undo step; remaining: filter thumbnails and a preview area inside the dialog; G2 estimate 4–6 → 1–2 h; Raster effects 82% → 85% (power-user view 49% → 52%), estimated, a modest move because the named missing dialog landed without thumbnails or an in-dialog preview; previously: colour emoji fonts with bitmap glyphs (`sbix`, `CBDT`) draw and export; headline numbers unchanged; previously:; Affinity import AF.5: master pages, tables, paragraph formats, shadows, glows, outlines, overlays and embedded documents; ruby (ふりがな) for live text: `type.ruby` sets or removes a reading over a byte range, above or below (right of the column in vertical type), aligned, sized and overhanging or squeezed; saved natively, laid out with line growth, drawn on the canvas and in SVG/PDF (outlines where ruby is present); headline numbers unchanged; previously:; 3D: original Standard, Diffuse, Top Left and Right lighting presets; command access, effective-setting recognition and preview/history; scores and estimates unchanged; Print: Japanese paper sizes in the Print dialog's Media list — JIS B4 257 × 364 mm, JIS B5 182 × 257 mm and JIS B6 128 × 182 mm beside the ISO B4/B5 (which keep their ISO 216 sizes), はがき, 往復はがき, 封筒 長形3号/長形4号/角形2号/角形3号 and 四六判; about 90 community PRs of 2026-10-19: Objects on Path, the Simplify slider, Pencil Edit Selected Paths in the middle of a path, Georgian, and more; Background Export progress dialog with Cancel, #2564; about 85 community PRs of 2026-10-18: Justification…, Word .docx placed as area type, CCITT Group 3 TIFF, Roman Hanging Punctuation, blend spines edited with the Anchor Point tool, shadows and glows in stack order, coloured 3D lights, a Dutch catalog completed; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; about 145 community PRs of 2026-10-17: image-decode overflow guards, opaque floating panels, MCP export cancel and progress, editable Illustrator EPS import, scoped `doc_inspect`, Dutch, Danish and Indonesian, Simplified Chinese messages, screen-reader names; headline numbers unchanged; about 160 community PRs of 2026-10-15/16: PDF import crash fixes, Dimension tool, legacy `.ai` save, Intertwine, web pen input, Image Trace fixes, `.ai`/EPS/PDF import fidelity, Bulgarian, Greek, Swedish and Finnish catalogs; headline numbers unchanged) · **Target:** Adobe Illustrator 2026 (30.x)+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-23 · **Change:** minor (OpenType SVG colour fonts draw and export as vector art; previously: COLR colour fonts (v0 and v1) draw and export as vector art; previously: G2: the Effect Gallery shipped (#2883): Effect › Effect Gallery… browses the Artistic, Brush Strokes, Distort, Sketch, Stylize and Texture filters, edits each filter's settings, builds an ordered stack (add, reorder, hide, remove) with a live canvas preview, and OK applies it as separate appearance effects in one undo step; remaining: filter thumbnails and a preview area inside the dialog; G2 estimate 4–6 → 1–2 h; Raster effects 82% → 85% (power-user view 49% → 52%), estimated, a modest move because the named missing dialog landed without thumbnails or an in-dialog preview; previously: colour emoji fonts with bitmap glyphs (`sbix`, `CBDT`) draw and export; headline numbers unchanged; previously:; Affinity import AF.5: master pages, tables, paragraph formats, shadows, glows, outlines, overlays and embedded documents; ruby (ふりがな) for live text: `type.ruby` sets or removes a reading over a byte range, above or below (right of the column in vertical type), aligned, sized and overhanging or squeezed, and the Character panel’s Ruby section applies it from the UI; saved natively, laid out with line growth, drawn on the canvas and in SVG/PDF (outlines where ruby is present); headline numbers unchanged; previously:; 3D: original Standard, Diffuse, Top Left and Right lighting presets; command access, effective-setting recognition and preview/history; scores and estimates unchanged; Print: Japanese paper sizes in the Print dialog's Media list — JIS B4 257 × 364 mm, JIS B5 182 × 257 mm and JIS B6 128 × 182 mm beside the ISO B4/B5 (which keep their ISO 216 sizes), はがき, 往復はがき, 封筒 長形3号/長形4号/角形2号/角形3号 and 四六判; about 90 community PRs of 2026-10-19: Objects on Path, the Simplify slider, Pencil Edit Selected Paths in the middle of a path, Georgian, and more; Background Export progress dialog with Cancel, #2564; about 85 community PRs of 2026-10-18: Justification…, Word .docx placed as area type, CCITT Group 3 TIFF, Roman Hanging Punctuation, blend spines edited with the Anchor Point tool, shadows and glows in stack order, coloured 3D lights, a Dutch catalog completed; 3D lighting: one to ten colored lights, smooth colored Inflate, legacy white-light appearance preserved; surface batching and restored seam minimum; scores and estimates unchanged; about 145 community PRs of 2026-10-17: image-decode overflow guards, opaque floating panels, MCP export cancel and progress, editable Illustrator EPS import, scoped `doc_inspect`, Dutch, Danish and Indonesian, Simplified Chinese messages, screen-reader names; headline numbers unchanged; about 160 community PRs of 2026-10-15/16: PDF import crash fixes, Dimension tool, legacy `.ai` save, Intertwine, web pen input, Image Trace fixes, `.ai`/EPS/PDF import fidelity, Bulgarian, Greek, Swedish and Finnish catalogs; headline numbers unchanged) · **Target:** Adobe Illustrator 2026 (30.x) VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adobe Illustrator workflow. It runs on macOS, Windows, Linux, FreeBSD and the web (WASM), and agents can drive all of it over MCP, the CLI and a JSON@@ -133,6 +133,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap - 2026-10-23: COLR colour fonts (v0 and v1, as in Noto Color Emoji's vector build, Segoe UI Emoji, Nabla) draw as vector art: their layers' colours, gradients, clips and blend modes on the canvas and in SVG, EPS, legacy `.ai` and PDF, and as editable paths with Create Outlines; glyphs using sweep gradients or compositing modes are drawn as images of the glyph. - 2026-10-22: The Effect Gallery shipped (#2883): Effect › Effect Gallery… browses the Artistic, Brush Strokes, Distort, Sketch, Stylize and Texture filters, edits each filter's settings, builds an ordered stack (add, reorder, hide, remove) with a live canvas preview, and OK applies it as separate appearance effects in one undo step. Filter thumbnails and a preview area inside the dialog remain ([Filter Gallery overview](https://helpx.adobe.com/photoshop/desktop/effects-filters/get-started-with-filters/filter-gallery.html)). Raster effects 82% → 85% (estimated); headline numbers unchanged. - 2026-10-22: Colour emoji fonts with bitmap glyphs draw and export: `sbix` (Apple Color Emoji, macOS's emoji font) and `CBDT` (bitmap Noto Color Emoji) glyphs show as their pictures on the canvas, whatever the type's fill, scale and turn with the type, and export to PDF (in the font's Type 3 glyphs, or as images), SVG (images when type is outlined; live text leaves emoji to the viewer), EPS and legacy `.ai` (images); Create Outlines turns them into embedded images. Fonts with bitmaps but no outlines, such as Noto Color Emoji, are now found among the installed fonts. COLR and OpenType SVG fonts remain; headline numbers unchanged.+- 2026-10-22: Character panel gets a Ruby section: the reading over the selected characters (or the selected type) is typed in and applied with its position, alignment, size and overhang, or removed, all through `type.ruby` in one undo step each; it follows the Type tool's selection and the ruby there. The engine, layout, save and SVG/PDF support landed in #3448 (merged). Headline numbers unchanged. - 2026-10-22: Effect › Sketch › Conté Crayon: a black crayon in the dark areas and a white one in the light areas, on mid-grey paper whose texture (Brick, Burlap, Canvas, Sandstone; Scaling, Relief, Light Direction, Invert) breaks the strokes, with Foreground Level and Background Level; an original approximation, defaults unverified. Every Photoshop-style filter is now in (57/57); the Effect Gallery remains (4–6 h). - 2026-10-22: Ruby (ふりがな) for live text: `type.ruby` sets or removes a reading over a byte range of the selected type — above it in horizontal type, right of the column in vertical type (or below/left), start/centre/end aligned, at 1–100% of the base size (default half), overhanging or squeezed to the base width. It is saved in the native file, laid out with its line growing (following lines move on), drawn on the canvas, and written in SVG and PDF as outlines where ruby is present. UI panel and type on a path are next. Headline numbers unchanged.@@ -5446,6 +5446,9 @@ axis Y Axis Y ос Packaged {n} file(s) in {folder} Пакетирани са {n} файла в {folder} Packaged {n} file(s) {note} Пакетирани са {n} файла {note} {count} file(s) in {folder}. Show the package folder? {count} файл(а) в {folder}. Показване на папката на пакета?+ Reading Четене+ Overhang Надвес+ applied Приложено Clockwise По часовниковата стрелка Dither Дитъринг Fine noise hides banding in the gradient fill Фин шум скрива ивиците в градиентната запълка@@ -5507,6 +5507,9 @@ axis Y Axis osa Y Packaged {n} file(s) in {folder} Zabaleno souborů: {n} do {folder} Packaged {n} file(s) {note} Zabaleno souborů: {n} {note} {count} file(s) in {folder}. Show the package folder? Souborů: {count} v {folder}. Zobrazit složku balíčku?+ Reading Čtení+ Overhang Přesah+ applied Použito Clockwise Ve směru hodinových ručiček Dither Rozptýlení Fine noise hides banding in the gradient fill Jemný šum skryje pruhy ve výplni přechodem@@ -5461,6 +5461,9 @@ axis Y Axis Y-akse Packaged {n} file(s) in {folder} Pakkede {n} fil(er) i {folder} Packaged {n} file(s) {note} Pakkede {n} fil(er) {note} {count} file(s) in {folder}. Show the package folder? {count} fil(er) i {folder}. Vis pakkemappen?+ Reading Læsning+ Overhang Overhæng+ applied Anvendt Clockwise Med uret Dither Dither Fine noise hides banding in the gradient fill Fin støj skjuler striber i gradientfyldet@@ -5088,6 +5088,9 @@ axis Y Axis Y-Achse Packaged {n} file(s) in {folder} {n} Datei(en) in {folder} verpackt Packaged {n} file(s) {note} {n} Datei(en) verpackt {note} {count} file(s) in {folder}. Show the package folder? {count} Datei(en) in {folder}. Paketordner anzeigen?+ Reading Lesung+ Overhang Überhang+ applied Angewendet Clockwise Im Uhrzeigersinn Dither Dithering Fine noise hides banding in the gradient fill Feines Rauschen verbirgt Streifen in der Verlaufsfläche@@ -5459,6 +5459,9 @@ axis Y Axis Άξονας Y Packaged {n} file(s) in {folder} Συσκευάστηκαν {n} αρχεία στον φάκελο {folder} Packaged {n} file(s) {note} Συσκευάστηκαν {n} αρχεία {note} {count} file(s) in {folder}. Show the package folder? {count} αρχεία στον φάκελο {folder}. Να εμφανιστεί ο φάκελος συσκευασίας;+ Reading Ανάγνωση+ Overhang Προεξοχή+ applied Εφαρμόστηκε Clockwise Δεξιόστροφα Dither Πρόσθεση θορύβου Fine noise hides banding in the gradient fill Ο λεπτός θόρυβος κρύβει τις λωρίδες στο γέμισμα ντεγκραντέ@@ -5449,6 +5449,9 @@ axis Y Axis Eje Y Packaged {n} file(s) in {folder} Se empaquetaron {n} archivo(s) en {folder} Packaged {n} file(s) {note} Se empaquetaron {n} archivo(s) {note} {count} file(s) in {folder}. Show the package folder? {count} archivo(s) en {folder}. ¿Mostrar la carpeta del paquete?+ Reading Lectura+ Overhang Desbordamiento+ applied Aplicado Clockwise En sentido horario Dither Tramado Fine noise hides banding in the gradient fill Un ruido fino oculta las bandas del relleno de degradado@@ -5538,6 +5538,9 @@ axis Y Axis Y-akseli Packaged {n} file(s) in {folder} Paketoitiin {n} tiedosto(a) kansioon {folder} Packaged {n} file(s) {note} Paketoitiin {n} tiedosto(a) {note} {count} file(s) in {folder}. Show the package folder? {count} tiedosto(a) kansiossa {folder}. Näytetäänkö pakettikansio?+ Reading Lukutapa+ Overhang Ylitys+ applied Käytetty Clockwise Myötäpäivään Dither Rasterointi Fine noise hides banding in the gradient fill Hieno kohina piilottaa raidat liukuväritäytöstä@@ -5506,6 +5506,9 @@ axis Y Axis Axe Y Packaged {n} file(s) in {folder} {n} fichier(s) assemblé(s) dans {folder} Packaged {n} file(s) {note} {n} fichier(s) assemblé(s) {note} {count} file(s) in {folder}. Show the package folder? {count} fichier(s) dans {folder}. Afficher le dossier de l’assemblage ?+ Reading Lecture+ Overhang Dépassement+ applied Appliqué Clockwise Sens horaire Dither Simulation Fine noise hides banding in the gradient fill Un bruit fin masque les bandes du fond en dégradé@@ -5493,6 +5493,9 @@ axis Y Axis Y tengely Packaged {n} file(s) in {folder} Becsomagolt fájlok: {n}, mappa: {folder} Packaged {n} file(s) {note} Becsomagolt fájlok: {n} {note} {count} file(s) in {folder}. Show the package folder? Fájlok: {count}, mappa: {folder}. Megnyitod a csomag mappáját?+ Reading Olvasat+ Overhang Túlnyúlás+ applied Alkalmazva Clockwise Óramutató járásával megegyező Dither Árnyalatkeverés Fine noise hides banding in the gradient fill A finom zaj elrejti a sávokat a színátmenetes kitöltésben@@ -5461,6 +5461,9 @@ axis Y Axis Sumbu Y Packaged {n} file(s) in {folder} Berhasil mengemas {n} file di {folder} Packaged {n} file(s) {note} Berhasil mengemas {n} file {note} {count} file(s) in {folder}. Show the package folder? {count} file dalam {folder}. Tampilkan folder paket?+ Reading Bacaan+ Overhang Tonjolan+ applied Diterapkan Clockwise Searah jarum jam Dither Dither Fine noise hides banding in the gradient fill Derau halus menyembunyikan pita warna pada isian gradien@@ -5122,6 +5122,9 @@ axis Y Axis Asse Y Packaged {n} file(s) in {folder} Pacchetto creato con {n} file in {folder} Packaged {n} file(s) {note} Pacchetto creato con {n} file {note} {count} file(s) in {folder}. Show the package folder? {count} file in {folder}. Vuoi mostrare la cartella del pacchetto?+ Reading Lettura+ Overhang Sporgenza+ applied Applicato Clockwise In senso orario Dither Dithering Fine noise hides banding in the gradient fill Un rumore fine nasconde le bande nel riempimento sfumato@@ -5184,6 +5184,9 @@ axis Y Axis Y 軸 Packaged {n} file(s) in {folder} {n} 個のファイルを {folder} にパッケージしました Packaged {n} file(s) {note} {n} 個のファイルをパッケージしました {note} {count} file(s) in {folder}. Show the package folder? {folder} に {count} 個のファイル。パッケージフォルダーを表示しますか?+ Reading 読み+ Overhang はみ出し+ applied 適用済み Clockwise 時計回り Dither ディザ Fine noise hides banding in the gradient fill 細かいノイズでグラデーションの塗りの縞を目立たなくします@@ -5423,6 +5423,9 @@ axis Y Axis Y ღერძი @msg this kind of brush has no options dialog yet ამ ტიპის ფუნჯს პარამეტრების დიალოგი ჯერ არ აქვს Drag art, colors and text styles here to reuse them across documents. Libraries are stored on this machine — no account required. გადმოათრიეთ აქ გრაფიკა, ფერები და ტექსტის სტილები, რომ სხვა დოკუმენტებშიც გამოიყენოთ. ბიბლიოთეკები ამ კომპიუტერზე ინახება — ანგარიში არ არის საჭირო. Choose a filter ფილტრის არჩევა+ Reading კითხვა+ Overhang გადაკიდება+ applied გამოყენებულია Clockwise საათის ისრის მიმართულებით Dither დიზერინგი Fine noise hides banding in the gradient fill წვრილი ხმაური მალავს ზოლებს გრადიენტულ შევსებაში@@ -5085,6 +5085,9 @@ axis Y Axis Y축 Packaged {n} file(s) in {folder} {folder}에 파일 {n}개를 패키지했습니다 Packaged {n} file(s) {note} 파일 {n}개를 패키지했습니다 {note} {count} file(s) in {folder}. Show the package folder? {folder}에 파일 {count}개. 패키지 폴더를 표시하시겠습니까?+ Reading 읽기+ Overhang 오버행+ applied 적용됨 Clockwise 시계 방향 Dither 디더 Fine noise hides banding in the gradient fill 미세한 노이즈로 그레이디언트 칠의 띠를 감춥니다@@ -5505,6 +5505,9 @@ axis Y Axis Y-as Packaged {n} file(s) in {folder} {n} bestand(en) verpakt in {folder} Packaged {n} file(s) {note} {n} bestand(en) verpakt {note} {count} file(s) in {folder}. Show the package folder? {count} bestand(en) in {folder}. De pakketmap openen?+ Reading Leeswijze+ Overhang Overhang+ applied Toegepast Clockwise Met de klok mee Dither Dither Fine noise hides banding in the gradient fill Fijne ruis verbergt banden in de verloopvulling@@ -5359,6 +5359,9 @@ axis Y Axis Oś Y Packaged {n} file(s) in {folder} Spakowane pliki: {n} w {folder} Packaged {n} file(s) {note} Spakowane pliki: {n} {note} {count} file(s) in {folder}. Show the package folder? Pliki: {count} w {folder}. Pokazać folder pakietu?+ Reading Odczyt+ Overhang Wystawanie+ applied Zastosowano Clockwise Zgodnie z ruchem wskazówek zegara Dither Rozpraszanie Fine noise hides banding in the gradient fill Drobny szum ukrywa pasy w wypełnieniu gradientem@@ -5424,6 +5424,9 @@ axis Y Axis Eixo Y Packaged {n} file(s) in {folder} {n} arquivo(s) empacotado(s) em {folder} Packaged {n} file(s) {note} {n} arquivo(s) empacotado(s) {note} {count} file(s) in {folder}. Show the package folder? {count} arquivo(s) em {folder}. Mostrar a pasta do pacote?+ Reading Leitura+ Overhang Transbordamento+ applied Aplicado Clockwise Sentido horário Dither Dither Fine noise hides banding in the gradient fill Um ruído fino esconde as faixas do preenchimento em gradiente@@ -5118,6 +5118,9 @@ axis Y Axis Ось Y Packaged {n} file(s) in {folder} Упаковано файлов: {n} в {folder} Packaged {n} file(s) {note} Упаковано файлов: {n} {note} {count} file(s) in {folder}. Show the package folder? Файлов: {count} в {folder}. Показать папку пакета?+ Reading Чтение+ Overhang Выступ+ applied Применено Clockwise По часовой стрелке Dither Дизеринг Fine noise hides banding in the gradient fill Мелкий шум скрывает полосы в градиентной заливке@@ -5531,6 +5531,9 @@ axis Y Axis Y-axel Packaged {n} file(s) in {folder} Paketerade {n} fil(er) i {folder} Packaged {n} file(s) {note} Paketerade {n} fil(er) {note} {count} file(s) in {folder}. Show the package folder? {count} fil(er) i {folder}. Visa paketmappen?+ Reading Läsning+ Overhang Överhäng+ applied Använd Clockwise Medurs Dither Dithering Fine noise hides banding in the gradient fill Fint brus döljer bandningen i övertoningsfyllningen@@ -5460,6 +5460,9 @@ axis Y Axis Y ekseni Packaged {n} file(s) in {folder} {folder} biçiminde paketlenmiş {n} dosyası/dosyaları Packaged {n} file(s) {note} Paketlenmiş {n} dosyası/dosyaları {note} {count} file(s) in {folder}. Show the package folder? {folder} içindeki {count} dosyası/dosyaları. Paket klasörü gösterilsin mi?+ Reading Okuma+ Overhang Taşma+ applied Uygulandı Clockwise Saat yönünde Dither Titreme Fine noise hides banding in the gradient fill İnce gürültü, degrade dolgudaki bantları gizler@@ -5102,6 +5102,9 @@ axis Y Axis Вісь Y Packaged {n} file(s) in {folder} Упаковано файли: {n} {folder} Packaged {n} file(s) {note} Упаковано файли: {n} {note} {count} file(s) in {folder}. Show the package folder? файлів: {count} {folder}. Показати папку пакета?+ Reading Читання+ Overhang Виступ+ applied Застосовано Clockwise За годинниковою стрілкою Dither Дизеринг Fine noise hides banding in the gradient fill Дрібний шум приховує смуги в градієнтній заливці@@ -5102,6 +5102,9 @@ axis Y Axis Trục Y Packaged {n} file(s) in {folder} Đã đóng gói {n} tệp trong {folder} Packaged {n} file(s) {note} Đã đóng gói {n} tệp {note} {count} file(s) in {folder}. Show the package folder? {count} tệp trong {folder}. Hiện thư mục gói?+ Reading Cách đọc+ Overhang Độ nhô+ applied Đã áp dụng Clockwise Theo chiều kim đồng hồ Dither Dither Fine noise hides banding in the gradient fill Nhiễu mịn che dải màu trong vùng tô chuyển sắc@@ -5346,6 +5346,9 @@ axis Y Axis Y 轴 Packaged {n} file(s) in {folder} 已将 {n} 个文件打包到 {folder} Packaged {n} file(s) {note} 已打包 {n} 个文件 {note} {count} file(s) in {folder}. Show the package folder? {folder} 中有 {count} 个文件。要显示打包文件夹吗?+ Reading 读音+ Overhang 悬挂+ applied 已应用 Clockwise 顺时针 Dither 仿色 Fine noise hides banding in the gradient fill 细微杂色可隐藏渐变填色中的色带@@ -5141,6 +5141,9 @@ axis Y Axis Y 軸 Packaged {n} file(s) in {folder} 已將 {n} 個檔案封裝到 {folder} Packaged {n} file(s) {note} 已封裝 {n} 個檔案 {note} {count} file(s) in {folder}. Show the package folder? {folder} 中有 {count} 個檔案。要顯示封裝資料夾嗎?+ Reading 讀音+ Overhang 懸掛+ applied 已套用 Clockwise 順時針 Dither 仿色 Fine noise hides banding in the gradient fill 細微雜色可隱藏漸層填色中的色帶@@ -6,7 +6,7 @@ use std::sync::{Mutex, OnceLock}; use egui::{Key, Ui, vec2}; use serde_json::{Value, json};-use vectorcraft_doc::{CharStyle, NodeId, NodeKind};+use vectorcraft_doc::{CharStyle, NodeId, NodeKind, RubyAlign, RubyPosition, RubySpan, TextObject}; use vectorcraft_tools::{Mods, ToolKey}; use super::{pstate, set_pstate};@@ -722,6 +722,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.add_space(2.0); let hidden = pstate::<bool>(ui.ctx(), "ch-hide-options"); metrics_grid(app, ui, "ch-grid", &s, w, !hidden);+ // Ruby (ふりがな): the reading over the selected characters, its placement and options.+ ruby_section(app, ui); if hidden { return; }@@ -979,5 +981,204 @@ fn reset_panel(app: &mut VectorcraftApp) { ); } +// ---------- Ruby (ふりがな, phonetic guide) ----------++/// The Ruby section's fields, kept between frames; `synced` is the selection and ruby they were+/// read from, so they reload when either changes.+#[derive(Clone, Debug, PartialEq)]+struct RubyFields {+ reading: String,+ /// "above" or "below" (right/left of the column in vertical type).+ position: String,+ /// "start", "center" or "end".+ align: String,+ /// Percentage of the base text's size, 1–100.+ size: f64,+ overhang: bool,+ synced: String,+}++impl Default for RubyFields {+ fn default() -> Self {+ Self { reading: String::new(), position: "above".into(), align: "center".into(), size: 50.0, overhang: true, synced: String::new() }+ }+}++impl RubyFields {+ /// The fields of `span`, or the defaults (no reading, above, centred, half size, overhanging).+ fn from_span(span: Option<&RubySpan>) -> Self {+ let mut f = Self::default();+ if let Some(r) = span {+ f.reading.clone_from(&r.text);+ f.position = match r.position {+ RubyPosition::Above => "above",+ RubyPosition::Below => "below",+ }+ .into();+ f.align = match r.align {+ RubyAlign::Center => "center",+ RubyAlign::Start => "start",+ RubyAlign::End => "end",+ }+ .into();+ f.size = r.safe_size_percent();+ f.overhang = r.overhang;+ }+ f+ }+}++/// The text the Ruby section acts on: the Type tool's selected characters when it edits a range,+/// else the first selected text object's whole text.+fn ruby_target(app: &VectorcraftApp) -> Option<(NodeId, usize, usize)> {+ if let Some((id, a, b)) = text_editing(app) {+ return (b > a).then_some((id, a, b));+ }+ let st = app.session.active()?;+ for node in st.selection.objects.iter().filter_map(|id| st.doc.node(*id)) {+ let mut target: Option<(NodeId, usize)> = None;+ node.walk(&mut |n| {+ if target.is_none()+ && let NodeKind::Text(t) = &n.kind+ {+ target = Some((n.id, t.plain_text().len()));+ }+ });+ if let Some((id, len)) = target {+ return (len > 0).then_some((id, 0, len));+ }+ }+ None+}++/// The ruby over `a..b` of `t`: the first span touching the range.+fn ruby_in(t: &TextObject, a: usize, b: usize) -> Option<&RubySpan> {+ t.ruby_in(a, b).next()+}++/// Whether `a..b` is exactly one ruby span: an option edit can re-apply it in place, without+/// dropping another reading under the selection or changing the span's range. Apply replaces the+/// ruby over the selection when it is not.+fn ruby_covers(t: &TextObject, a: usize, b: usize) -> bool {+ let mut spans = t.ruby_in(a, b);+ spans.next().is_some_and(|r| r.start == a && r.end == b) && spans.next().is_none()+}++/// The selection and ruby the section's fields were built from.+fn ruby_sync_key(id: NodeId, a: usize, b: usize, span: Option<&RubySpan>) -> String {+ let ruby = span.map_or_else(String::new, |r| format!("{}|{:?}|{:?}|{}|{}", r.text, r.position, r.align, r.size_percent, r.overhang));+ format!("{}:{a}:{b}:{ruby}", id.0)+}++/// The `type.ruby` params of the section: set the reading over `a..b`, or `remove` it.+fn ruby_params(id: NodeId, a: usize, b: usize, f: &RubyFields, remove: bool) -> Value {+ let mut p = json!({"id": id.0, "start": a, "end": b});+ if remove {+ p["remove"] = json!(true);+ return p;+ }+ p["text"] = json!(f.reading.trim());+ p["position"] = json!(f.position);+ p["align"] = json!(f.align);+ p["size"] = json!(f.size);+ p["overhang"] = json!(f.overhang);+ p+}++/// Draw the Ruby section: the reading over the selected characters, its position, alignment,+/// size and overhang, with Apply and Remove. Every action goes through `type.ruby` in one undo+/// step; the fields follow the selection and the ruby it covers. An option edit re-applies the+/// ruby only when it is the one span over exactly the selection; Apply replaces the ruby over+/// the selection.+fn ruby_section(app: &mut VectorcraftApp, ui: &mut Ui) {+ let Some((id, a, b)) = ruby_target(app) else { return };+ let (span, auto_apply) = app.session.active().and_then(|st| st.doc.node(id)).map_or((None, false), |n| match &n.kind {+ NodeKind::Text(t) => (ruby_in(t, a, b).cloned(), ruby_covers(t, a, b)),+ _ => (None, false),+ });+ let key = ruby_sync_key(id, a, b, span.as_ref());+ let mut f = pstate::<RubyFields>(ui.ctx(), "ruby-fields");+ if f.synced != key {+ f = RubyFields::from_span(span.as_ref());+ f.synced.clone_from(&key);+ }+ ui.add_space(4.0);+ ui.separator();+ ui.horizontal(|ui| {+ ui.label(egui::RichText::new(tl!("Ruby")).strong());+ if span.is_some() {+ crate::widgets::dim_label(ui, tl!("applied"));+ }+ });+ let mut apply = false;+ ui.horizontal(|ui| {+ crate::widgets::dim_label(ui, &format!("{}:", tl!("Reading")));+ ui.add(egui::TextEdit::singleline(&mut f.reading).hint_text(tl!("Reading")).desired_width(f32::INFINITY));+ });+ ui.horizontal(|ui| {+ crate::widgets::dim_label(ui, &format!("{}:", tl!("Position")));+ let position = tl!(match f.position.as_str() {+ "below" => "Below",+ _ => "Above",+ });+ egui::ComboBox::from_id_salt("ruby-position").selected_text(position).width(64.0).show_ui(ui, |ui| {+ for (value, label) in [("above", "Above"), ("below", "Below")] {+ if ui.selectable_label(f.position == value, tl!(label)).clicked() && f.position != value {+ f.position = value.into();+ apply = auto_apply;+ }+ }+ });+ crate::widgets::dim_label(ui, &format!("{}:", tl!("Align")));+ let align = tl!(match f.align.as_str() {+ "start" => "Start",+ "end" => "End",+ _ => "Center",+ });+ egui::ComboBox::from_id_salt("ruby-align").selected_text(align).width(64.0).show_ui(ui, |ui| {+ for (value, label) in [("start", "Start"), ("center", "Center"), ("end", "End")] {+ if ui.selectable_label(f.align == value, tl!(label)).clicked() && f.align != value {+ f.align = value.into();+ apply = auto_apply;+ }+ }+ });+ });+ ui.horizontal(|ui| {+ crate::widgets::dim_label(ui, &format!("{}:", tl!("Size")));+ let size = ui.add(egui::DragValue::new(&mut f.size).range(1.0..=100.0).speed(1.0).suffix("%"));+ // Dragging sets the value as it goes and ends here; typed, it commits on Enter or focus+ // loss. Apply then, unless it is already the ruby's size.+ if (size.drag_stopped() || size.lost_focus()) && auto_apply && span.as_ref().is_some_and(|r| r.safe_size_percent() != f.size) {+ apply = true;+ }+ if ui.checkbox(&mut f.overhang, tl!("Overhang")).changed() && auto_apply {+ apply = true;+ }+ });+ let remove = ui+ .horizontal(|ui| {+ let mut remove = false;+ if ui.add_enabled(!f.reading.trim().is_empty(), egui::Button::new(tl!("Apply"))).clicked() {+ apply = true;+ }+ if ui.add_enabled(span.is_some(), egui::Button::new(tl!("Remove"))).clicked() {+ remove = true;+ }+ remove+ })+ .inner;+ if remove || apply {+ // A typing session is its own undo step; the ruby command is the next one.+ end_typing(app);+ if remove {+ app.run("type.ruby", ruby_params(id, a, b, &f, true)).ok();+ } else {+ app.run("type.ruby", ruby_params(id, a, b, &f, false)).ok();+ }+ }+ set_pstate(ui.ctx(), "ruby-fields", f);+}+ #[cfg(test)] mod tests;@@ -654,3 +654,166 @@ fn reset_panel_resets_every_attribute_but_the_font() { reset_panel(&mut app); assert_eq!(runs(&app), [("One ".to_string(), styled), ("12".to_string(), plain)]); }++#[test]+fn ruby_params_set_and_remove_the_reading() {+ let id = NodeId(7);+ let f = RubyFields {+ reading: " かんじ ".into(),+ position: "below".into(),+ align: "end".into(),+ size: 40.0,+ overhang: false,+ ..RubyFields::default()+ };+ let p = ruby_params(id, 3, 9, &f, false);+ assert_eq!(p["id"], 7);+ assert_eq!((p["start"].as_u64(), p["end"].as_u64()), (Some(3), Some(9)));+ assert_eq!(p["text"], "かんじ", "the reading is trimmed");+ assert_eq!(p["position"], "below");+ assert_eq!(p["align"], "end");+ assert_eq!(p["size"], 40.0);+ assert_eq!(p["overhang"], false);+ let r = ruby_params(id, 3, 9, &f, true);+ assert_eq!(r["remove"], true);+ assert!(r.get("text").is_none());+}++#[test]+fn ruby_fields_load_the_ruby_under_the_selection() {+ let mut t = TextObject::point(vectorcraft_geom::Point::ZERO, "漢字です", CharStyle::default());+ let span =+ RubySpan { position: RubyPosition::Below, align: RubyAlign::End, size_percent: 40.0, overhang: false, ..RubySpan::new(0, 6, "かんじ") };+ t.ruby = vec![span];+ let found = ruby_in(&t, 0, 6).expect("the ruby under the selection");+ let f = RubyFields::from_span(Some(found));+ assert_eq!(f.reading, "かんじ");+ assert_eq!((f.position.as_str(), f.align.as_str()), ("below", "end"));+ assert!((f.size - 40.0).abs() < 1e-9 && !f.overhang);+ assert!(ruby_covers(&t, 0, 6), "the only span over exactly the selection");+ // A selection outside it has none; no span is the defaults.+ assert!(ruby_in(&t, 9, 12).is_none() && !ruby_covers(&t, 9, 12));+ assert_eq!(RubyFields::from_span(None), RubyFields::default());+ // The fields reload when the selection or its ruby changes.+ assert_ne!(ruby_sync_key(NodeId(1), 0, 6, Some(found)), ruby_sync_key(NodeId(1), 0, 3, Some(found)));+ assert_ne!(ruby_sync_key(NodeId(1), 0, 6, Some(found)), ruby_sync_key(NodeId(1), 0, 6, None));+ // A partial selection and a second reading under it are not re-applied in place.+ assert!(!ruby_covers(&t, 0, 3), "the selection is only part of the span");+ t.ruby.push(RubySpan::new(3, 6, "じ"));+ assert!(!ruby_covers(&t, 0, 6), "two readings under the selection");+}++/// The reading typed into the panel is applied to the selected text through `type.ruby`, shown+/// as applied, and removable again; Undo brings it back (one step per action).+#[test]+fn ruby_panel_applies_and_removes_the_reading() {+ use super::super::tests_appearance::{click, frame_events, text_rect};+ let mut app = VectorcraftApp::new(Session::new(), Default::default());+ app.run("file.new", json!({"width": 300, "height": 200})).unwrap();+ let id = NodeId(app.run("text.create", json!({"x": 20, "y": 50, "text": "漢字です", "size": 12})).unwrap()["id"].as_u64().unwrap());+ app.run("select.set", json!({"ids": [id.0]})).unwrap();+ let ctx = egui::Context::default();+ let texts = frame_events(&ctx, &mut app, vec![], show);+ let labels: Vec<&str> = texts.iter().map(|(s, _)| s.as_str()).collect();+ for label in ["Ruby", "Reading:", "Position:", "Align:", "Size:", "Overhang", "Apply", "Remove"] {+ assert!(labels.contains(&label), "`{label}` not drawn: {labels:?}");+ }+ assert!(labels.contains(&"Above") && labels.contains(&"Center"), "the defaults: {labels:?}");+ // Type the reading into its field, then Apply.+ let row = text_rect(&texts, "Reading:");+ click(&ctx, &mut app, row.right_center() + egui::vec2(30.0, 0.0), show);+ frame_events(&ctx, &mut app, vec![egui::Event::Text("かんじ".into())], show);+ let texts = frame_events(&ctx, &mut app, vec![], show);+ assert!(texts.iter().any(|(s, _)| s == "かんじ"), "the field shows the typed reading: {texts:?}");+ click(&ctx, &mut app, text_rect(&texts, "Apply").center(), show);+ let ruby = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(id).unwrap().kind {+ NodeKind::Text(t) => t.ruby.clone(),+ _ => panic!("text"),+ };+ let r = ruby(&app);+ assert_eq!((r.len(), r[0].start, r[0].end, r[0].text.as_str()), (1, 0, 12, "かんじ"), "the whole selected text (12 bytes)");+ assert_eq!(r[0].position, RubyPosition::Above);+ assert!(r[0].overhang);+ // The panel shows it as applied, and Remove takes it off; Undo puts it back.+ let texts = frame_events(&ctx, &mut app, vec![], show);+ assert!(texts.iter().any(|(s, _)| s == "かんじ"), "the reading stays in the field: {texts:?}");+ click(&ctx, &mut app, text_rect(&texts, "Remove").center(), show);+ assert!(ruby(&app).is_empty());+ app.run("edit.undo", json!({})).unwrap();+ assert_eq!(ruby(&app).len(), 1);+}++/// With the Type tool, the section acts on the selected characters only, not the whole text.+#[test]+fn ruby_panel_acts_on_the_type_tools_selection() {+ use super::super::tests_appearance::{click, frame_events, text_rect};+ let mut app = VectorcraftApp::new(Session::new(), Default::default());+ app.run("file.new", json!({"width": 300, "height": 200})).unwrap();+ app.session.select_tool("type", Default::default()).unwrap();+ let id = NodeId(app.run("text.create", json!({"x": 20, "y": 50, "text": "漢字です", "size": 12})).unwrap()["id"].as_u64().unwrap());+ app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();+ app.session.set_tool_option("select", &json!({"start": 0, "end": 3}));+ let ctx = egui::Context::default();+ let texts = frame_events(&ctx, &mut app, vec![], show);+ let row = text_rect(&texts, "Reading:");+ click(&ctx, &mut app, row.right_center() + egui::vec2(30.0, 0.0), show);+ frame_events(&ctx, &mut app, vec![egui::Event::Text("かん".into())], show);+ let texts = frame_events(&ctx, &mut app, vec![], show);+ click(&ctx, &mut app, text_rect(&texts, "Apply").center(), show);+ let NodeKind::Text(t) = &app.session.active().unwrap().doc.node(id).unwrap().kind else { panic!("text") };+ assert_eq!(t.ruby.len(), 1);+ assert_eq!((t.ruby[0].start, t.ruby[0].end, t.ruby[0].text.as_str()), (0, 3, "かん"), "only the selected character");+}++/// A typed size commits on Enter and re-applies the ruby, like dragging the value does.+#[test]+fn ruby_panel_typing_a_size_reapplies_the_ruby() {+ use super::super::tests_appearance::{click, frame_events, text_rect};+ let mut app = VectorcraftApp::new(Session::new(), Default::default());+ app.run("file.new", json!({"width": 300, "height": 200})).unwrap();+ let id = NodeId(app.run("text.create", json!({"x": 20, "y": 50, "text": "漢字", "size": 12})).unwrap()["id"].as_u64().unwrap());+ app.run("select.set", json!({"ids": [id.0]})).unwrap();+ app.run("type.ruby", json!({"text": "かんじ"})).unwrap();+ let ctx = egui::Context::default();+ let texts = frame_events(&ctx, &mut app, vec![], show);+ click(&ctx, &mut app, text_rect(&texts, "Size:").right_center() + egui::vec2(30.0, 0.0), show);+ let key = |key, modifiers| egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers };+ frame_events(&ctx, &mut app, vec![key(egui::Key::A, egui::Modifiers::COMMAND), egui::Event::Text("40".into())], show);+ frame_events(&ctx, &mut app, vec![key(egui::Key::Enter, Default::default())], show);+ let NodeKind::Text(t) = &app.session.active().unwrap().doc.node(id).unwrap().kind else { panic!("text") };+ assert_eq!(t.ruby.len(), 1);+ assert_eq!(t.ruby[0].text, "かんじ");+ assert!((t.ruby[0].size_percent - 40.0).abs() < 1e-9, "the typed size is applied: {:?}", t.ruby[0]);+}++/// Option edits re-apply only the one ruby over exactly the selection: over two readings,+/// changing Position leaves them alone until Apply replaces them over the selection.+#[test]+fn ruby_panel_option_edits_do_not_merge_overlapping_readings() {+ use super::super::tests_appearance::{click, frame_events, text_rect};+ let mut app = VectorcraftApp::new(Session::new(), Default::default());+ app.run("file.new", json!({"width": 300, "height": 200})).unwrap();+ let id = NodeId(app.run("text.create", json!({"x": 20, "y": 50, "text": "漢字", "size": 12})).unwrap()["id"].as_u64().unwrap());+ app.run("select.set", json!({"ids": [id.0]})).unwrap();+ app.run("type.ruby", json!({"start": 0, "end": 3, "text": "かん"})).unwrap();+ app.run("type.ruby", json!({"start": 3, "end": 6, "text": "じ"})).unwrap();+ let readings = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(id).unwrap().kind {+ NodeKind::Text(t) => t.ruby.iter().map(|r| (r.start, r.end, r.text.clone(), r.position)).collect::<Vec<_>>(),+ _ => panic!("text"),+ };+ let ctx = egui::Context::default();+ let texts = frame_events(&ctx, &mut app, vec![], show);+ // Open Position and pick Below: the field shows it, but both readings stay.+ click(&ctx, &mut app, text_rect(&texts, "Above").center(), show);+ let texts = frame_events(&ctx, &mut app, vec![], show);+ click(&ctx, &mut app, text_rect(&texts, "Below").center(), show);+ assert_eq!(+ readings(&app),+ [(0, 3, "かん".to_string(), RubyPosition::Above), (3, 6, "じ".to_string(), RubyPosition::Above)],+ "no option edit collapses the readings"+ );+ // Apply still replaces the ruby over the whole selection.+ let texts = frame_events(&ctx, &mut app, vec![], show);+ click(&ctx, &mut app, text_rect(&texts, "Apply").center(), show);+ assert_eq!(readings(&app), [(0, 6, "かん".to_string(), RubyPosition::Below)]);+}@@ -1,6 +1,6 @@ # Type parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (threading ports: a double-click on a threaded port breaks the thread, `text.thread.break`; previously: Ruby: Create Outlines, EPS, EMF/WMF and legacy `.ai` keep the reading where the canvas draws it; previously: OpenType SVG: images in glyph documents draw, export and outline with the glyph; previously: bitmap glyphs (emoji) go with type that effects reshape, into Expand Appearance, envelope Expand and EMF/WMF export, as images; previously: threading ports: every selected area type and type on a path shows its in and out ports (empty, arrow, red "+"), and the Selection tool threads from either, the in port into a frame before it, #2572; scores unchanged; font menu: Font Technology › Variable lists only variable font families, G4; previously:; colour glyphs (COLR, SVG) go with type that effects reshape, into Expand Appearance and EMF/WMF export; previously:; OpenType SVG glyphs follow the `SVG ` table's coordinate rules: a root size without a `viewBox` sets up the em, a `viewBox` that isn't square is centred in it, and a glyph element nested in others is drawn on its own; previously: OpenType SVG colour fonts draw and export as vector art; hanging punctuation: Glyph Scaling leaves the hung marks at their width, Standard burasagari counts a hung line start as room, G4; a paragraph's Auto Leading also drives the Character panel's Leading, the Alt+↑/↓ leading nudge and Fill with Placeholder Text; scores unchanged; COLR colour fonts (v0 and v1) draw and export as vector art; previously: colour emoji fonts with bitmap glyphs (`sbix`: Apple Color Emoji; `CBDT`: Noto Color Emoji) draw on the canvas and export: PDF Type 3 glyphs or images, SVG images with outlined text, EPS and legacy `.ai` images, Create Outlines images; scores unchanged; previously: #3515: recent-font groups in font pickers; manual Tate-chu-yoko marks characters across one em of a vertical column; System Layout: Character panel menu sets whole-point advances for every text object in the document (off by default); ruby (ふりがな) for live text: `type.ruby` sets or removes a reading over a byte range, above or below (right of the column in vertical type), start/centre/end aligned, sized and overhanging or squeezed; saved natively, laid out with its line growing, drawn on the canvas and in SVG/PDF (outlines where ruby is present); scores unchanged; previously:; Fractional Widths: Character panel menu turns whole-point advances on or off per type object, on by default; scores unchanged; type on a path threads, mixed with area type in one thread; scores unchanged; No Break keeps marked characters on one line; scores unchanged; a click on the out port threads the text into a closed path, a new frame at the click or a dragged rectangle, #2572 in part; scores unchanged; native family labels fall back to localized legacy names when typographic names are English-only, #2208; scores unchanged; Cmd+Shift+C aligns centre off the Mac, #1940; tab leaders are drawn in the tab's gap, #1877; type in symbols, pattern tiles, graph designs and opacity masks gets its layout bounds back on opening a native file, #1738; score unchanged; Escape while typing ends the edit, #1686; Select › Same on type, #1737; `justify` when creating point type, #1748; scores unchanged; OpenType stylistic sets, alternates, figure styles, superscript and subscript as character features, #1607; a wide word in non-rectangular area type moves down, #1615; scores unchanged; Character Styles New/Redefine capture the first selected character at run boundaries and end prior typing before capture; boundary and typing-history input tests and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Linux font folders from fontconfig, #1529; commands and MCP `add_text` report area type that does not fit its frame, #1471; Tabs panel stop drags, #1458; Fit Headline in a frame one line tall, #1444; frames and paths beyond a billion points hold no text, #1451; scores unchanged; missing glyphs are looked for again after a font rescan, #1376; scores unchanged; Full-name font matching (#1258); Windows font-service QA pending; scores unchanged; Middle Eastern digit types: Character panel menu › Digits, `{digits}`; the font menu's script filter lists Thai fonts; Thai, Lao, Khmer and Myanmar lines break between words; the caret steps over whole clusters such as Thai consonants with their marks; Thai typed in a font without Thai falls back to a font made for Thai; earlier: Proportional Metrics in vertical type, `vpal`, #966; Cmd+Shift+C aligns centre off the Mac, a double-click on area type's bottom handle turns on Auto Size, #1940; MCP `add_text` exposes the existing `vertical` option for point, area and path type; horizontal remains the default; scores and hours unchanged; Vertical fullwidth question and exclamation marks keep their nominal glyph upright when the font has no vertical alternate; scores and hours unchanged. tab leaders are drawn in the tab's gap, #1877; type in symbols, pattern tiles, graph designs and opacity masks gets its layout bounds back on opening a native file, #1738; score unchanged; Escape while typing ends the edit, #1686; Select › Same on type, #1737; `justify` when creating point type, #1748; scores unchanged; OpenType stylistic sets, alternates, figure styles, superscript and subscript as character features, #1607; a wide word in non-rectangular area type moves down, #1615; scores unchanged; Character Styles New/Redefine capture the first selected character at run boundaries and end prior typing before capture; boundary and typing-history input tests and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Linux font folders from fontconfig, #1529; commands and MCP `add_text` report area type that does not fit its frame, #1471; Tabs panel stop drags, #1458; Fit Headline in a frame one line tall, #1444; frames and paths beyond a billion points hold no text, #1451; scores unchanged; missing glyphs are looked for again after a font rescan, #1376; scores unchanged; Full-name font matching (#1258); Windows font-service QA pending; scores unchanged; Middle Eastern digit types: Character panel menu › Digits, `{digits}`; the font menu's script filter lists Thai fonts; Thai, Lao, Khmer and Myanmar lines break between words; the caret steps over whole clusters such as Thai consonants with their marks; Thai typed in a font without Thai falls back to a font made for Thai; earlier: Proportional Metrics in vertical type, `vpal`, #966) · **Target:** Adobe Illustrator 2026 (30.x)+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (threading ports: a double-click on a threaded port breaks the thread, `text.thread.break`; previously: Ruby: Create Outlines, EPS, EMF/WMF and legacy `.ai` keep the reading where the canvas draws it; previously: OpenType SVG: images in glyph documents draw, export and outline with the glyph; previously: bitmap glyphs (emoji) go with type that effects reshape, into Expand Appearance, envelope Expand and EMF/WMF export, as images; previously: threading ports: every selected area type and type on a path shows its in and out ports (empty, arrow, red "+"), and the Selection tool threads from either, the in port into a frame before it, #2572; scores unchanged; font menu: Font Technology › Variable lists only variable font families, G4; previously:; colour glyphs (COLR, SVG) go with type that effects reshape, into Expand Appearance and EMF/WMF export; previously:; OpenType SVG glyphs follow the `SVG ` table's coordinate rules: a root size without a `viewBox` sets up the em, a `viewBox` that isn't square is centred in it, and a glyph element nested in others is drawn on its own; previously: OpenType SVG colour fonts draw and export as vector art; hanging punctuation: Glyph Scaling leaves the hung marks at their width, Standard burasagari counts a hung line start as room, G4; a paragraph's Auto Leading also drives the Character panel's Leading, the Alt+↑/↓ leading nudge and Fill with Placeholder Text; scores unchanged; COLR colour fonts (v0 and v1) draw and export as vector art; previously: colour emoji fonts with bitmap glyphs (`sbix`: Apple Color Emoji; `CBDT`: Noto Color Emoji) draw on the canvas and export: PDF Type 3 glyphs or images, SVG images with outlined text, EPS and legacy `.ai` images, Create Outlines images; scores unchanged; previously: #3515: recent-font groups in font pickers; manual Tate-chu-yoko marks characters across one em of a vertical column; System Layout: Character panel menu sets whole-point advances for every text object in the document (off by default); ruby (ふりがな) for live text: `type.ruby` sets or removes a reading over a byte range, above or below (right of the column in vertical type), start/centre/end aligned, sized and overhanging or squeezed, and the Character panel’s Ruby section applies it from the UI; saved natively, laid out with its line growing, drawn on the canvas and in SVG/PDF (outlines where ruby is present); scores unchanged; previously:; Fractional Widths: Character panel menu turns whole-point advances on or off per type object, on by default; scores unchanged; type on a path threads, mixed with area type in one thread; scores unchanged; No Break keeps marked characters on one line; scores unchanged; a click on the out port threads the text into a closed path, a new frame at the click or a dragged rectangle, #2572 in part; scores unchanged; native family labels fall back to localized legacy names when typographic names are English-only, #2208; scores unchanged; Cmd+Shift+C aligns centre off the Mac, #1940; tab leaders are drawn in the tab's gap, #1877; type in symbols, pattern tiles, graph designs and opacity masks gets its layout bounds back on opening a native file, #1738; score unchanged; Escape while typing ends the edit, #1686; Select › Same on type, #1737; `justify` when creating point type, #1748; scores unchanged; OpenType stylistic sets, alternates, figure styles, superscript and subscript as character features, #1607; a wide word in non-rectangular area type moves down, #1615; scores unchanged; Character Styles New/Redefine capture the first selected character at run boundaries and end prior typing before capture; boundary and typing-history input tests and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Linux font folders from fontconfig, #1529; commands and MCP `add_text` report area type that does not fit its frame, #1471; Tabs panel stop drags, #1458; Fit Headline in a frame one line tall, #1444; frames and paths beyond a billion points hold no text, #1451; scores unchanged; missing glyphs are looked for again after a font rescan, #1376; scores unchanged; Full-name font matching (#1258); Windows font-service QA pending; scores unchanged; Middle Eastern digit types: Character panel menu › Digits, `{digits}`; the font menu's script filter lists Thai fonts; Thai, Lao, Khmer and Myanmar lines break between words; the caret steps over whole clusters such as Thai consonants with their marks; Thai typed in a font without Thai falls back to a font made for Thai; earlier: Proportional Metrics in vertical type, `vpal`, #966; Cmd+Shift+C aligns centre off the Mac, a double-click on area type's bottom handle turns on Auto Size, #1940; MCP `add_text` exposes the existing `vertical` option for point, area and path type; horizontal remains the default; scores and hours unchanged; Vertical fullwidth question and exclamation marks keep their nominal glyph upright when the font has no vertical alternate; scores and hours unchanged. tab leaders are drawn in the tab's gap, #1877; type in symbols, pattern tiles, graph designs and opacity masks gets its layout bounds back on opening a native file, #1738; score unchanged; Escape while typing ends the edit, #1686; Select › Same on type, #1737; `justify` when creating point type, #1748; scores unchanged; OpenType stylistic sets, alternates, figure styles, superscript and subscript as character features, #1607; a wide word in non-rectangular area type moves down, #1615; scores unchanged; Character Styles New/Redefine capture the first selected character at run boundaries and end prior typing before capture; boundary and typing-history input tests and full workspace CI pass; scores and hours unchanged; Character styles retain explicit Auto leading, propagate resets and preserve local leading overrides; scores and hours unchanged; Linux font folders from fontconfig, #1529; commands and MCP `add_text` report area type that does not fit its frame, #1471; Tabs panel stop drags, #1458; Fit Headline in a frame one line tall, #1444; frames and paths beyond a billion points hold no text, #1451; scores unchanged; missing glyphs are looked for again after a font rescan, #1376; scores unchanged; Full-name font matching (#1258); Windows font-service QA pending; scores unchanged; Middle Eastern digit types: Character panel menu › Digits, `{digits}`; the font menu's script filter lists Thai fonts; Thai, Lao, Khmer and Myanmar lines break between words; the caret steps over whole clusters such as Thai consonants with their marks; Thai typed in a font without Thai falls back to a font made for Thai; earlier: Proportional Metrics in vertical type, `vpal`, #966) · **Target:** Adobe Illustrator 2026 (30.x) Type is Illustrator's largest single area by use (weight 13 of 105 across "Type core" and "Type advanced" in [target-app-parity.md](target-app-parity.md)). This checklist lists the features one by one. Status is from the@@ -58,9 +58,8 @@ against Illustrator's public documentation; **estimated** unless noted. | CJK: kinsoku (Hard, Soft, None), Line-end Punctuation Half Width, tate-chu-yoko (auto) | done / partial | custom kinsoku sets, the Mojikumi Settings dialog and the other mojikumi sets missing | | CJK: Proportional Metrics (`palt` in horizontal type, `vpal` in vertical type) | done | #966 | | CJK: manual tate-chu-yoko | done | Character panel menu › Tate-chu-yoko (`text.setFormat` / `text.setRangeStyle {tateChuYoko}`), with Show East Asian Options: marked characters stand side by side across one em of a vertical column, squeezed to the em when wider; two or three half-width digits still do so without the mark; horizontal type keeps the mark and does not move them. Up/Down and Left/Right settings remain. Evidence: `tests_vertical::manual_tate_chu_yoko_sets_marked_characters_across_one_em`, `typecmd::tate_chu_yoko_is_a_character_attribute_saved_only_when_on` |-| CJK: ruby, manual tate-chu-yoko | partial | ruby (ふりがな) for live text (`type.ruby`): a reading over a byte range, above (right of the column in vertical type) or below, start/centre/end alignment, size, overhang or squeeze; saved natively, laid out with its line growing, drawn on the canvas, in SVG and PDF (as outlines where ruby is present), in EPS, EMF/WMF and legacy `.ai` (legacy `.ai` writes type with ruby as outlines), and kept by Create Outlines, Expand Appearance and flattening, each glyph where the layout puts it (`typecmd::ruby_tests::create_outlines_keeps_the_ruby_where_it_is_laid_out`); manual tate-chu-yoko missing | #633 |+| CJK: ruby, manual tate-chu-yoko | partial | ruby (ふりがな) for live text (`type.ruby`, and the Character panel's Ruby section for the selected characters: reading, Apply/Remove, position, alignment, size, overhang): a reading over a byte range, above (right of the column in vertical type) or below, start/centre/end alignment, size, overhang or squeeze; saved natively, laid out with its line growing, drawn on the canvas, in SVG and PDF (as outlines where ruby is present), in EPS, EMF/WMF and legacy `.ai` (legacy `.ai` writes type with ruby as outlines), and kept by Create Outlines, Expand Appearance and flattening, each glyph where the layout puts it (`typecmd::ruby_tests::create_outlines_keeps_the_ruby_where_it_is_laid_out`); manual tate-chu-yoko missing | #633 | | No Break | done | Character panel menu (`text.setFormat` / `text.setRangeStyle {noBreak}`): the marked characters stay on one line, including across a space; a run that does not fit after earlier text moves together, and a run longer than the line stays together. Evidence: `layout::wrapping_tests::no_break_keeps_marked_characters_on_one_line`, `tests::no_break_keeps_marked_words_on_one_line`, `typecmd::no_break_is_a_character_attribute_saved_only_when_on` |-| CJK: ruby | missing | #633 | | Inline symbols in text | beyond Illustrator | not scored | | Align Stroke (inside, outside) on live type's character stroke | beyond Illustrator | not scored; `stroke.set {align}` and `text.setRangeStyle {strokeOptions: {align}}`; saved natively, drawn around the glyphs on the canvas and in PDF, EPS and SVG (live text, stroke on the glyph outlines); visual bounds grow with it (#2294) | @@ -87,6 +86,7 @@ Character Styles menu New/Redefine and footer New capture the first selected cha | 2026-10-21 | minor | Manual Tate-chu-yoko: Character panel menu marks characters that stand across one em of a vertical column; ruby and the up/down settings remain; scores unchanged | | 2026-10-21 | minor | System Layout: Character panel menu sets whole-point advances for every text object in the document (off by default); scores and hours unchanged | | 2026-10-22 | minor | Ruby (ふりがな) for live text: `type.ruby` sets/removes a reading over a byte range, above or below (right/left of the column in vertical type), aligned, sized and overhanging or squeezed; saved natively, laid out with line growth, drawn on the canvas and in SVG/PDF (outlines where ruby is present); scores unchanged |+| 2026-10-22 | minor | Character panel: a Ruby section applies the reading to the selected characters (or the selected type), with position, alignment, size and overhang, Apply and Remove, all through `type.ruby` in one undo step; scores unchanged | | 2026-10-21 | minor | Fractional Widths: Character panel menu turns whole-point advances on or off per type object (on by default); System Layout remains a stub; scores and hours unchanged | | 2026-10-21 | minor | Type on a path threads, mixed with area type in one thread: its text that doesn't fit flows on into the next frame, and its red out port, just past its end bracket, loads the Selection tool with that text as area type's does; a new frame threaded from it at a point is type on a path of its shape; scores unchanged | | 2026-10-21 | minor | No Break: marked characters stay on one line; scores unchanged |