mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
Merge pull request #463 from imachus/pr/ja-burasagari
Type: burasagari — a Japanese comma or full stop ending a line may hang outside it
This commit is contained in:
16 files changed
+269
-20
No files matched your search
+3
-3
@@ -4,7 +4,7 @@ VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adob
|
||||
|
||||
This file tracks **how far we are and what's left**. Time estimates are wall-clock hours of continuous Claude Opus 5.5 agent work (including builds and the CI gate), given both for **one agent** and for **4–6 parallel agents** on disjoint crates. They are counted from the remaining work (see [Parity estimate](#parity-estimate)), calibrated against measured throughput, and updated as work lands.
|
||||
|
||||
_Last updated: 2026-10-08 (after M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback); see [Honest assessment](#honest-assessment-2026-10-05))._
|
||||
_Last updated: 2026-10-08 (after M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463); see [Honest assessment](#honest-assessment-2026-10-05))._
|
||||
|
||||
## Where we are
|
||||
|
||||
@@ -134,7 +134,7 @@ grade by behaviour against `plan/illustrator/`, not by whether a menu item exist
|
||||
- Pathfinder (10 exact curve booleans), Offset, Outline Stroke, Simplify, Clean Up, Split Into Grid, Divide Objects Below. Object › Path › Remove Anchor Points (`path.removeAnchors`) removes the direct-selected anchors without opening their paths (the Delete key still deletes them with their segments): the neighbours keep their handle directions and their facing handles are refitted so one cubic follows the two old segments, and straight sides stay straight. The Delete Anchor Point tool uses the same refit. The Control bar, the contextual task bar, the Properties quick actions and the canvas context menu offer it when anchors are selected. With anchors selected, the Control bar and the Properties panel also convert them to corner or smooth (`path.convertAnchors`), connect end points (Join) and cut the path at them (`path.cutAtAnchors`, also on the task bar and the context menu): a closed path opens there and an open one becomes one path per piece, each cut leaving one of its two coincident anchors selected so a drag pulls the path apart.
|
||||
- Live effects with previewing dialogs: Distort & Transform, Path, Convert to Shape, 15 Warp styles, Round Corners, Scribble, Effect → Pathfinder (all 10 operations, live on groups; several loose objects are grouped first, in one undo step), Color Adjustments (Brightness/Contrast, Curves, Levels, Hue/Saturation, Shift to Color, Temperature/Tint, on vectors, live type and embedded images), and raster drop shadow, glows and feather. SVG and PDF export keep live effects (geometry baked; SVG raster effects as filters).
|
||||
- **Type:** Text Wrap, Type on a Path effects (Rainbow/Skew/3D Ribbon/Stair Step/Gravity) and Options (Flip, Align to Path, Spacing; OK keeps the type where it is), type on a path's start, centre and end brackets (Selection and Direct Selection drag them to set where the type runs, slide it along its path and, across the path, flip it: #429), Character and Paragraph Styles (override-preserving redefine), Area Type Options (rows/columns/inset/first baseline), threaded text across any closed shapes, Fit Headline. Area type resizes like Illustrator's type areas: a bounding-box handle drag (Selection tool) or Area Type Options Width/Height size the frame and the text reflows at its size (threads too); Direct Selection drags a frame corner or edge (`text.reshapeArea`). Point type still scales. The Selection tool shows the type widget beside selected type's bounding box (hollow on point type, filled on area type); double-clicking it converts between them (Type › Convert To Area Type / Convert To Point Type), keeping the text and its styles in place. Variable fonts list every named instance as a style of their family (Figtree Light to Black and their italics, Bahnschrift, Segoe UI Variable), and the instance chosen is shaped, drawn, outlined and measured at its axis settings; PDF and SVG embed it as that instance. Variation axis sliders are not done. Hebrew and Arabic are laid out right to left with the Unicode bidirectional algorithm (embedded Latin and numbers keep their direction, lines put in visual order after wrapping) in point, area and path type, with caret, selection and arrow keys in visual order; each paragraph's direction comes from its first strong character unless Paragraph Direction sets it (Paragraph panel with Show Indic Options, `text.setFormat {direction}`), and new type aligns to the start of its direction. SVG export outlines right-to-left text. Font Size and Leading are combo boxes in the Character and Properties panels (Font Size in the Control bar too, beside the Character link, font family and style): type, step with ↑/↓ or the spinner, scrub the label, or pick a preset (6–72 pt, shown in the type unit; Leading also Auto), each pick one undo step; Kerning and Tracking list -100…200; a size, leading, kerning or tracking the selected text doesn't share shows blank (#458).
|
||||
- **Interface languages, vertical type:** VectorCraft ▸ Language (or Preferences ▸ User Interface) picks Automatic (the system locale) or a registered language, persisted as `interfaceLanguage`. Translations come from per-language catalogs (`i18n/*.tsv`): Traditional and Simplified Chinese, Japanese and Spanish (neutral international Spanish, for every `es-*` locale) cover the whole interface, Czech and Brazilian Portuguese every menu label, Brazilian Portuguese the shell's `tl!` literals too (tested); Spanish also translates status-bar and error messages as they are shown (`@msg` templates, every message literal tested; the messages themselves stay English for agents); untranslated text stays English. Japanese and Chinese glyphs come from the craft-fonts build input (release builds) or the installed fonts, Czech and Spanish ones from the bundled UI fonts. The web app follows the browser's languages (or `?lang=`). The Basic toolbar's Type button holds Vertical Type in its flyout. Vertical Type, Vertical Area Type and Vertical Type on a Path create vertical text (columns right to left, upright CJK glyphs and marks with `vert` alternates, Latin and longer numbers on their side, two- and three-digit numbers set as tate-chu-yoko in one em of the column (kept whole when justified), upright glyphs centred on the column (tracked or justified too), point type's anchor on the first column's centre line); kinsoku (the strict set) applies to horizontal and vertical Japanese, and justified Japanese lines spread their room between characters (not inside Latin words); Paragraph panel › Mojikumi Set (shown with Preferences › Type › Show East Asian Options, `text.setFormat {mojikumi}`): None or Line-end Punctuation Half Width (new type's default, as in Illustrator; older documents and imported text keep None) shares one half-em space between consecutive punctuation, sets a closing mark ending a line half width and an opening bracket starting a wrapped line flush with its start, and puts a quarter em between Japanese and Latin letters or digits; Paragraph panel menu › Top-to-Top / Bottom-to-Bottom Leading measures leading from em box top to top (area type's first line touches the frame) or from baseline to baseline; Character panel menu › Character Alignment (with the East Asian options; `text.setFormat` or `text.setRangeStyle {charAlign}`) lines characters smaller than the largest on their line up on the Roman baseline or the em box top/right, centre or bottom/left (ICF top and bottom not yet); Type ▸ Type Orientation switches existing text; caret, selection, hit testing and arrow keys follow the writing direction. Vertical text exports to PDF as real text and to SVG as outlines.
|
||||
- **Interface languages, vertical type:** VectorCraft ▸ Language (or Preferences ▸ User Interface) picks Automatic (the system locale) or a registered language, persisted as `interfaceLanguage`. Translations come from per-language catalogs (`i18n/*.tsv`): Traditional and Simplified Chinese, Japanese and Spanish (neutral international Spanish, for every `es-*` locale) cover the whole interface, Czech and Brazilian Portuguese every menu label, Brazilian Portuguese the shell's `tl!` literals too (tested); Spanish also translates status-bar and error messages as they are shown (`@msg` templates, every message literal tested; the messages themselves stay English for agents); untranslated text stays English. Japanese and Chinese glyphs come from the craft-fonts build input (release builds) or the installed fonts, Czech and Spanish ones from the bundled UI fonts. The web app follows the browser's languages (or `?lang=`). The Basic toolbar's Type button holds Vertical Type in its flyout. Vertical Type, Vertical Area Type and Vertical Type on a Path create vertical text (columns right to left, upright CJK glyphs and marks with `vert` alternates, Latin and longer numbers on their side, two- and three-digit numbers set as tate-chu-yoko in one em of the column (kept whole when justified), upright glyphs centred on the column (tracked or justified too), point type's anchor on the first column's centre line); kinsoku (the strict set) applies to horizontal and vertical Japanese, and justified Japanese lines spread their room between characters (not inside Latin words); Paragraph panel › Mojikumi Set (shown with Preferences › Type › Show East Asian Options, `text.setFormat {mojikumi}`): None or Line-end Punctuation Half Width (new type's default, as in Illustrator; older documents and imported text keep None) shares one half-em space between consecutive punctuation, sets a closing mark ending a line half width and an opening bracket starting a wrapped line flush with its start, and puts a quarter em between Japanese and Latin letters or digits; Paragraph panel menu › Top-to-Top / Bottom-to-Bottom Leading measures leading from em box top to top (area type's first line touches the frame) or from baseline to baseline; Paragraph panel menu › Burasagari None / Regular / Force (`text.setFormat {burasagari: "none"|"standard"|"forced"}`) hangs an East Asian comma or full stop (、。,.、。) ending an area type line outside it, Regular (new type's default) when it doesn't fit and Force always (the rest of the line then fills the measure; closing brackets and Latin punctuation don't hang); Character panel menu › Character Alignment (with the East Asian options; `text.setFormat` or `text.setRangeStyle {charAlign}`) lines characters smaller than the largest on their line up on the Roman baseline or the em box top/right, centre or bottom/left (ICF top and bottom not yet); Type ▸ Type Orientation switches existing text; caret, selection, hit testing and arrow keys follow the writing direction. Vertical text exports to PDF as real text and to SVG as outlines.
|
||||
- **Transparency:** opacity masks (clip/invert/disable/link), exported as SVG `<mask>` and PDF soft masks.
|
||||
- **Advanced art:** live Blends (steps/distance/smooth colour, editable spine, anchor-targeted Blend tool, faithful interpolation, knockout), Envelope Distort (warp/mesh/top object, Reset, full Envelope Options, mesh handles, Edit Contents; type, images, symbols, gradients, patterns and appearance distorted everywhere), Liquify tools with options and pen pressure, Puppet Warp with rotating pins, Perspective Grid with presets, movable planes and projected type (choosing a perspective tool shows the grid; Hide Grid hides it with the tool still chosen), Gradient Mesh, Shape Builder (stroke-only open paths and lines cut the regions they cross and enclose regions of their own; filled open paths count as closed; erase mode while Alt/Option is held: a minus pointer, red highlights of what will go, a click or drag deletes the regions touched, the pieces of open lines and the edges (outline pieces between the points where paths meet, opening their paths there) it passes over, one undo step, `shapeBuilder.merge {erase}` and `shapeBuilder.regions` edges over MCP), Live Paint (every path an edge, open lines included), Image Trace (12 presets; with an image selected the Control bar and the Properties panel show Image Trace, whose arrow traces with any preset, Mask (`object.maskImage`: a clipping path on the image's outline, selected so its handles crop), Crop Image and Embed or Unembed in place of Fill and Stroke; with an Image Trace object selected, its Preset (choosing another traces again), the Image Trace panel and Expand, plus Release in Properties; the View modes are not done), pattern swatches with pattern editing mode, live Repeat (radial/grid/mirror).
|
||||
- **Paint and appearance (M3):**
|
||||
@@ -205,7 +205,7 @@ about 1–1.5 agent-hours each, so the other rows (estimated on the older scale)
|
||||
| Raster effects (Effect Gallery, Document Raster Effects Settings) | 4 | 20% | ~55 Photoshop-style filters (Artistic, Brush Strokes, Distort, Pixelate, Sketch, Stylize, Texture, Video) and the Effect Gallery; Document Raster Effects Settings and raster effects in PDF are done | 28–42 |
|
||||
| 3D and Materials | 4 | 0% | Extrude & Bevel, Revolve, Inflate, Rotate, lighting, materials (software renderer) | 50–80 |
|
||||
| Type core | 9 | 79% | Middle Eastern features beyond bidi layout and Paragraph Direction (Character Direction, digit types, kashidas, the Middle Eastern composers, editable bidirectional SVG text, a split caret where directions meet); composer/hyphenation options, Optical Margin Alignment, hidden characters, Transform Again after a type-area resize (it scales the type), partial selection of type-area anchors by marquee | 15–20 |
|
||||
| Type advanced | 4 | 50% | CJK composition for vertical type (ruby, mojikumi beyond Line-end Punctuation Half Width — the other sets (and their paragraph-start bracket and indent rules), hanging punctuation, kinsoku sets and the Mojikumi Settings dialog —, proportional vertical metrics, manual tate-chu-yoko, Character Alignment on the ICF top and bottom), variable font axis sliders (named instances are styles), tab leaders, spell check (open dictionary), Touch Type, Retype; vertical point/area/path type and Type Orientation have initial support | 20–28 |
|
||||
| Type advanced | 4 | 50% | CJK composition for vertical type (ruby, mojikumi beyond Line-end Punctuation Half Width — the other sets (and their paragraph-start bracket and indent rules), kinsoku sets and the Mojikumi Settings dialog —, proportional vertical metrics, manual tate-chu-yoko, Character Alignment on the ICF top and bottom), variable font axis sliders (named instances are styles), tab leaders, spell check (open dictionary), Touch Type, Retype; vertical point/area/path type and Type Orientation have initial support | 20–28 |
|
||||
| Symbols, blends, envelopes, Repeat, perspective | 5 | 88% | symbol libraries (original), dynamic symbols, envelope Anti-Alias/Preserve Shape output and warp point editing, brush interpolation in blends (M8 fidelity pass done) | 5–8 |
|
||||
| Image Trace, graphs, image tools | 3 | 70% | graph Design/Column/Marker, Create Object Mosaic, Crop Image polish (an on-canvas crop box), Image Trace View modes (Outlines, Source Image) (Vector Halftone is done) | 6–10 |
|
||||
| Layers, artboards, document setup | 5 | 82% | Layers panel: a confirmation before deleting layers that hold art, Collect for Export, row context menus, the Layer Options dialog for several rows with mixed values, Paste Remembers Layers for sublayers, drag-scrolling a long panel while dragging rows; artboard presets/rearrange polish (Document Setup, New Document and the rest of the Layers panel are done) | 4–7 |
|
||||
|
||||
@@ -78,8 +78,8 @@ pub use setup::{Background, DocSetup, ExportText, GridSize, Quotes};
|
||||
pub use slices::{CellAlign, CellVAlign, Slice, SliceArea, SliceKind, SliceOptions, SliceSource};
|
||||
pub use style_libs::StyleLibrary;
|
||||
pub use text::{
|
||||
AreaOptions, CharAlign, CharPosition, CharStyle, FirstBaseline, Justify, LeadingModel, Mojikumi, ParaDirection, ParaStyle, PathAlign, PathEffect,
|
||||
ScriptMetrics, TabAlign, TabStop, TextKind, TextObject, TextRun, TextStyleDef, TextWrap, WrapShape,
|
||||
AreaOptions, Burasagari, CharAlign, CharPosition, CharStyle, FirstBaseline, Justify, LeadingModel, Mojikumi, ParaDirection, ParaStyle, PathAlign,
|
||||
PathEffect, ScriptMetrics, TabAlign, TabStop, TextKind, TextObject, TextRun, TextStyleDef, TextWrap, WrapShape,
|
||||
};
|
||||
pub use vectorcraft_color as color;
|
||||
pub use vectorcraft_geom as geom;
|
||||
|
||||
@@ -314,6 +314,10 @@ pub struct ParaStyle {
|
||||
/// How leading is measured (Paragraph panel menu): from baseline to baseline, or from the top
|
||||
/// of one line's ideographic em box to the next.
|
||||
pub leading_model: LeadingModel,
|
||||
/// Hanging punctuation (Paragraph panel menu › Burasagari): a comma or full stop ending a line
|
||||
/// may stand outside the frame. New type takes [`Burasagari::Standard`]; documents from before
|
||||
/// it and imported text keep [`Burasagari::None`].
|
||||
pub burasagari: Burasagari,
|
||||
}
|
||||
|
||||
/// [`ParaStyle`] as saved. [`Justify::Auto`] is written as the alignment it has in the paragraph
|
||||
@@ -347,6 +351,8 @@ struct ParaStyleFile {
|
||||
direction: Option<ParaDirection>,
|
||||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||||
leading_model: LeadingModel,
|
||||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||||
burasagari: Burasagari,
|
||||
}
|
||||
|
||||
impl From<ParaStyle> for ParaStyleFile {
|
||||
@@ -369,6 +375,7 @@ impl From<ParaStyle> for ParaStyleFile {
|
||||
mojikumi,
|
||||
direction,
|
||||
leading_model,
|
||||
burasagari,
|
||||
..
|
||||
} = p;
|
||||
Self {
|
||||
@@ -385,6 +392,7 @@ impl From<ParaStyle> for ParaStyleFile {
|
||||
mojikumi,
|
||||
direction,
|
||||
leading_model,
|
||||
burasagari,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -404,6 +412,7 @@ impl From<ParaStyleFile> for ParaStyle {
|
||||
mojikumi,
|
||||
direction,
|
||||
leading_model,
|
||||
burasagari,
|
||||
..
|
||||
} = f;
|
||||
Self {
|
||||
@@ -419,6 +428,7 @@ impl From<ParaStyleFile> for ParaStyle {
|
||||
mojikumi,
|
||||
direction,
|
||||
leading_model,
|
||||
burasagari,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -465,6 +475,22 @@ impl Mojikumi {
|
||||
}
|
||||
}
|
||||
|
||||
/// Hanging punctuation (burasagari): an East Asian comma or full stop ending a line (、。,.、。)
|
||||
/// stands outside the line's measure, in the space its punctuation spacing gives it (half width
|
||||
/// with Line-end Punctuation Half Width). Closing brackets and Latin punctuation don't hang. Shown
|
||||
/// as None / Regular / Force.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Burasagari {
|
||||
/// A comma or full stop that doesn't fit goes to the next line with the character before it.
|
||||
#[default]
|
||||
None,
|
||||
/// A comma or full stop that doesn't fit hangs outside the line; one that fits stays inside.
|
||||
Standard,
|
||||
/// A comma or full stop ending a line always hangs, and the rest of the line fills the measure.
|
||||
Forced,
|
||||
}
|
||||
|
||||
/// Area Type Options "First Baseline" offset.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
||||
@@ -424,9 +424,15 @@ fn text_create(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
/// The paragraph attributes new type gets (the Type tools, `text.create`): aligned to the start of
|
||||
/// each paragraph's direction (left, right for Hebrew and Arabic), Japanese punctuation composed
|
||||
/// with Line-end Punctuation Half Width. Imported text keeps its own.
|
||||
/// with Line-end Punctuation Half Width and Standard burasagari (as in Illustrator). Imported text
|
||||
/// keeps its own.
|
||||
pub(crate) fn new_type_para() -> vectorcraft_doc::ParaStyle {
|
||||
vectorcraft_doc::ParaStyle { justify: vectorcraft_doc::Justify::Auto, mojikumi: vectorcraft_doc::Mojikumi::LineEndHalf, ..Default::default() }
|
||||
vectorcraft_doc::ParaStyle {
|
||||
justify: vectorcraft_doc::Justify::Auto,
|
||||
mojikumi: vectorcraft_doc::Mojikumi::LineEndHalf,
|
||||
burasagari: vectorcraft_doc::Burasagari::Standard,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// The character style new type gets: `size`, `font`, `style` and `color` from `p`, else the
|
||||
|
||||
@@ -43,7 +43,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Character / Paragraph",
|
||||
[],
|
||||
None,
|
||||
"{ids?|id?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, underline?, strikethrough?, allCaps?, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), leftIndent?, rightIndent?, firstLineIndent?, spaceBefore?, spaceAfter?: pt, hyphenate?: bool, mojikumi?: \"none\"|\"lineEndHalf\" (Japanese punctuation spacing), direction?: \"auto\"|\"leftToRight\"|\"rightToLeft\" (paragraph direction; auto: from each paragraph's first strong character), leadingModel?: \"romanBaseline\"|\"emBoxTop\" (leading measured baseline to baseline, or em box top to top), charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\" (where characters smaller than the largest on their line line up with it)}",
|
||||
"{ids?|id?, kerning?: 1/1000 em|\"auto\", baselineShift?: pt, hScale?: %, vScale?: %, rotation?: deg, underline?, strikethrough?, allCaps?, smallCaps?: bool, position?: \"normal\"|\"superscript\"|\"subscript\" (sizes from Document Setup), leftIndent?, rightIndent?, firstLineIndent?, spaceBefore?, spaceAfter?: pt, hyphenate?: bool, mojikumi?: \"none\"|\"lineEndHalf\" (Japanese punctuation spacing), direction?: \"auto\"|\"leftToRight\"|\"rightToLeft\" (paragraph direction; auto: from each paragraph's first strong character), leadingModel?: \"romanBaseline\"|\"emBoxTop\" (leading measured baseline to baseline, or em box top to top), charAlign?: \"romanBaseline\"|\"emBoxTop\"|\"emBoxCenter\"|\"emBoxBottom\" (where characters smaller than the largest on their line line up with it), burasagari?: \"none\"|\"standard\"|\"forced\" (Paragraph panel menu › Burasagari None/Regular/Force: an East Asian comma or full stop ending an area type line hangs outside it: when it doesn't fit, or always; new type: \"standard\")}",
|
||||
has_doc,
|
||||
set_format
|
||||
),
|
||||
@@ -218,6 +218,7 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
"direction",
|
||||
"leadingModel",
|
||||
"charAlign",
|
||||
"burasagari",
|
||||
];
|
||||
if !keys.iter().any(|k| p.get(*k).is_some()) {
|
||||
return Err(bad(C, "nothing to change"));
|
||||
@@ -249,6 +250,15 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
_ => return Err(bad(C, "`leadingModel` must be \"romanBaseline\" or \"emBoxTop\"")),
|
||||
}),
|
||||
};
|
||||
let burasagari = match p.get("burasagari") {
|
||||
None => None,
|
||||
Some(v) => Some(match v.as_str() {
|
||||
Some("none") => vectorcraft_doc::Burasagari::None,
|
||||
Some("standard") => vectorcraft_doc::Burasagari::Standard,
|
||||
Some("forced") => vectorcraft_doc::Burasagari::Forced,
|
||||
_ => return Err(bad(C, "`burasagari` must be \"none\", \"standard\" or \"forced\"")),
|
||||
}),
|
||||
};
|
||||
s.edit("Character", |d, _| {
|
||||
for id in &ids {
|
||||
let Some(NodeKind::Text(t)) = d.node_mut(*id).map(|n| &mut n.kind) else { continue };
|
||||
@@ -316,6 +326,9 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(v) = leading_model {
|
||||
para.leading_model = v;
|
||||
}
|
||||
if let Some(v) = burasagari {
|
||||
para.burasagari = v;
|
||||
}
|
||||
super::typecmd::refresh_bounds(t);
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -413,6 +413,36 @@ fn reshape_area(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
mod area_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn burasagari_is_set_per_paragraph_and_saved_only_when_on() {
|
||||
use vectorcraft_doc::Burasagari;
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||||
let id = s.execute("text.create", &json!({"x": 10, "y": 10, "text": "雅楽、笙。", "area": {"width": 200, "height": 100}})).unwrap()["id"]
|
||||
.as_u64()
|
||||
.unwrap();
|
||||
let para = |s: &Session| match &s.doc().unwrap().doc.node(NodeId(id)).unwrap().kind {
|
||||
NodeKind::Text(t) => t.para.clone(),
|
||||
_ => panic!("text"),
|
||||
};
|
||||
// New type: Standard, as in Illustrator.
|
||||
assert_eq!(para(&s).burasagari, Burasagari::Standard);
|
||||
s.execute("select.set", &json!({"ids": [id]})).unwrap();
|
||||
assert!(s.execute("text.setFormat", &json!({"burasagari": "strong"})).is_err());
|
||||
s.execute("text.setFormat", &json!({"burasagari": "forced"})).unwrap();
|
||||
assert_eq!(para(&s).burasagari, Burasagari::Forced);
|
||||
assert_eq!(serde_json::to_value(para(&s)).unwrap()["burasagari"], "forced");
|
||||
s.execute("text.setFormat", &json!({"burasagari": "none"})).unwrap();
|
||||
assert_eq!(para(&s).burasagari, Burasagari::None);
|
||||
// Saved only when on; documents from before it read as None.
|
||||
assert!(serde_json::to_value(para(&s)).unwrap().get("burasagari").is_none());
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(para(&s).burasagari, Burasagari::Standard);
|
||||
let old: vectorcraft_doc::ParaStyle = serde_json::from_value(json!({"justify": "Left"})).unwrap();
|
||||
assert_eq!(old.burasagari, Burasagari::None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leading_model_is_set_per_paragraph_and_saved_only_when_top_to_top() {
|
||||
use vectorcraft_doc::LeadingModel;
|
||||
|
||||
+47
-11
@@ -4,7 +4,9 @@ use std::ops::Range;
|
||||
|
||||
use kurbo::{Affine, BezPath, PathEl, Point, Rect, Shape, Vec2};
|
||||
use unicode_bidi::{BidiInfo, Level};
|
||||
use vectorcraft_doc::{CharStyle, Justify, LeadingModel, Mojikumi, ParaDirection, ParaStyle, PathAlign, PathEffect, TextKind, TextObject};
|
||||
use vectorcraft_doc::{
|
||||
Burasagari, CharStyle, Justify, LeadingModel, Mojikumi, ParaDirection, ParaStyle, PathAlign, PathEffect, TextKind, TextObject,
|
||||
};
|
||||
use vectorcraft_geom::{ArcPath, PathData};
|
||||
|
||||
use crate::composer::{Breakpoint, compose};
|
||||
@@ -749,7 +751,8 @@ fn tab_advance(tabs: &[vectorcraft_doc::TabStop], origin: f64, x: f64, rest: &[S
|
||||
}
|
||||
|
||||
/// Greedy break: returns (end glyph index, hyphenated) for a line starting at `i` of `width`.
|
||||
fn break_line(text: &str, g: &[SGlyph], i: usize, width: f64, hyphenate: bool) -> (usize, bool) {
|
||||
/// With burasagari, a comma or full stop that doesn't fit ends the line, hanging outside it.
|
||||
fn break_line(text: &str, g: &[SGlyph], i: usize, width: f64, hyphenate: bool, burasagari: Burasagari) -> (usize, bool) {
|
||||
if !width.is_finite() {
|
||||
return (g.len(), false);
|
||||
}
|
||||
@@ -759,6 +762,14 @@ fn break_line(text: &str, g: &[SGlyph], i: usize, width: f64, hyphenate: bool) -
|
||||
while j < g.len() {
|
||||
let gl = &g[j];
|
||||
if j > i && !gl.is_space() && x + gl.adv > width + EPS {
|
||||
// It ends the line with the spaces after it.
|
||||
if burasagari != Burasagari::None && hangs(gl) && kinsoku_allows(g, j) && g.get(j + 1).is_none_or(|n| n.byte != gl.byte) {
|
||||
let mut end = j + 1;
|
||||
while g.get(end).is_some_and(SGlyph::is_space) {
|
||||
end += 1;
|
||||
}
|
||||
return (end, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
x += gl.adv;
|
||||
@@ -805,6 +816,13 @@ fn break_line(text: &str, g: &[SGlyph], i: usize, width: f64, hyphenate: bool) -
|
||||
(end, hy)
|
||||
}
|
||||
|
||||
/// Can glyph `g` hang outside the line (burasagari)? An East Asian comma or full stop, full width
|
||||
/// (、。,.) or half width (、。); not a closing bracket, nor Latin punctuation (Latin text keeps
|
||||
/// its line breaks and composer).
|
||||
fn hangs(g: &SGlyph) -> bool {
|
||||
matches!(g.ch, '、' | '。' | ',' | '.' | '、' | '。') && g.tcy.is_none()
|
||||
}
|
||||
|
||||
/// Does kinsoku allow a line break after glyph `j`? Not after an opening bracket, nor before a
|
||||
/// closing one, a comma, a full stop, a small kana… (the character is pushed to the next line
|
||||
/// with the one before it).
|
||||
@@ -900,7 +918,10 @@ fn candidates(text: &str, g: &[SGlyph], hyphenate: bool) -> Vec<Breakpoint> {
|
||||
/// line metrics); `None` falls back to the greedy single-line composer.
|
||||
fn compose_para(cx: &Ctx<'_>, sg: &[SGlyph], para: &ParaStyle, pen: &Pen<'_>) -> Option<Vec<(usize, bool)>> {
|
||||
let justified = !matches!(para.justify, Justify::Auto | Justify::Left | Justify::Center | Justify::Right);
|
||||
if cx.opts.composer != Composer::EveryLine || !justified || pen.regions.is_none() || sg.len() < 2 {
|
||||
// A paragraph whose commas or full stops may hang is composed line by line (as the Japanese
|
||||
// single-line composer does).
|
||||
let may_hang = para.burasagari != Burasagari::None && sg.iter().any(hangs);
|
||||
if cx.opts.composer != Composer::EveryLine || !justified || pen.regions.is_none() || sg.len() < 2 || may_hang {
|
||||
return None;
|
||||
}
|
||||
let m = Metrics::of(&sg[0]);
|
||||
@@ -994,7 +1015,7 @@ fn flow(cx: &mut Ctx<'_>, paras: &[Range<usize>], para: &ParaStyle, regions: Opt
|
||||
let width = x1 - x0 - ind_l - para.right_indent;
|
||||
let (end, hyph) = match composed.as_ref().and_then(|c| c.get(li_para)) {
|
||||
Some(&(e, h)) if e > i => (e, h),
|
||||
_ if i < n => break_line(cx.text, &sg, i, width, para.hyphenate),
|
||||
_ if i < n => break_line(cx.text, &sg, i, width, para.hyphenate, para.burasagari),
|
||||
_ => (n, false),
|
||||
};
|
||||
let m = Metrics::max(&sg[i..end]).unwrap_or(pm);
|
||||
@@ -1038,6 +1059,21 @@ fn flow(cx: &mut Ctx<'_>, paras: &[Range<usize>], para: &ParaStyle, regions: Opt
|
||||
None => 0.0,
|
||||
};
|
||||
let w: f64 = sg[i..trimmed].iter().map(|g| g.adv).sum::<f64>() + hyphen.as_ref().map_or(0.0, |h| h.adv) - end_trim;
|
||||
// Burasagari: a comma or full stop ending an area type line hangs outside it (Standard:
|
||||
// when it doesn't fit; Forced: always). The rest of the line is aligned and justified
|
||||
// without it, and it follows the line's last character.
|
||||
let hang = (regions.is_some() && !rtl && hyphen.is_none() && trimmed > i + 1)
|
||||
.then(|| sg.get(trimmed - 1))
|
||||
.flatten()
|
||||
.filter(|g| hangs(g))
|
||||
.map(|g| g.adv - end_trim)
|
||||
.filter(|_| match para.burasagari {
|
||||
Burasagari::None => false,
|
||||
Burasagari::Standard => w > width + EPS,
|
||||
Burasagari::Forced => true,
|
||||
});
|
||||
let body = if hang.is_some() { trimmed - 1 } else { trimmed };
|
||||
let w = w - hang.unwrap_or(0.0);
|
||||
let (align, justify) = match para.justify {
|
||||
Justify::Auto => (if rtl { 2 } else { 0 }, false),
|
||||
Justify::Left => (0, false),
|
||||
@@ -1050,17 +1086,17 @@ fn flow(cx: &mut Ctx<'_>, paras: &[Range<usize>], para: &ParaStyle, regions: Opt
|
||||
};
|
||||
let justify = justify && regions.is_some();
|
||||
let (mut per_space, mut per_gap, mut per_cjk) = (0.0, 0.0, 0.0);
|
||||
let spaces = sg[i..trimmed].iter().filter(|g| g.is_space()).count();
|
||||
let spaces = sg[i..body].iter().filter(|g| g.is_space()).count();
|
||||
// Japanese (and Chinese) lines are justified between their characters (JLREQ 3.8): the
|
||||
// gaps next to a CJK character, not inside a Latin word or a tate-chu-yoko block.
|
||||
let cjk_gap = |j: usize| {
|
||||
j + 1 < trimmed
|
||||
j + 1 < body
|
||||
&& sg
|
||||
.get(j)
|
||||
.zip(sg.get(j + 1))
|
||||
.is_some_and(|(a, b)| !a.is_space() && !b.is_space() && !b.continues_tcy() && a.ch != '\t' && (is_cjk(a.ch) || is_cjk(b.ch)))
|
||||
};
|
||||
let cjk_gaps = (i..trimmed).filter(|&j| cjk_gap(j)).count();
|
||||
let cjk_gaps = (i..body).filter(|&j| cjk_gap(j)).count();
|
||||
if justify && (width - w).abs() > EPS {
|
||||
if cjk_gaps > 0 && width > w {
|
||||
// Spread over the CJK gaps and the word spaces alike.
|
||||
@@ -1069,9 +1105,9 @@ fn flow(cx: &mut Ctx<'_>, paras: &[Range<usize>], para: &ParaStyle, regions: Opt
|
||||
} else if spaces > 0 {
|
||||
// Composed lines may shrink word spaces (never below zero).
|
||||
per_space =
|
||||
((width - w) / spaces as f64).max(-sg[i..trimmed].iter().filter(|g| g.is_space()).map(|g| g.adv).fold(f64::MAX, f64::min));
|
||||
((width - w) / spaces as f64).max(-sg[i..body].iter().filter(|g| g.is_space()).map(|g| g.adv).fold(f64::MAX, f64::min));
|
||||
} else if para.justify == Justify::JustifyAll && width > w {
|
||||
let gaps = sg.get(i + 1..trimmed).map_or(0, |s| s.iter().filter(|g| !g.continues_tcy()).count());
|
||||
let gaps = sg.get(i + 1..body).map_or(0, |s| s.iter().filter(|g| !g.continues_tcy()).count());
|
||||
if gaps > 0 {
|
||||
per_gap = (width - w) / gaps as f64;
|
||||
}
|
||||
@@ -1116,14 +1152,14 @@ fn flow(cx: &mut Ctx<'_>, paras: &[Range<usize>], para: &ParaStyle, regions: Opt
|
||||
}
|
||||
if g.ch == '\t' {
|
||||
adv = tab_advance(¶.tabs, tab_origin, x, &sg[j + 1..trimmed.max(j + 1)]);
|
||||
} else if j < trimmed {
|
||||
} else if j < body {
|
||||
if g.is_space() {
|
||||
adv += per_space;
|
||||
} else if per_cjk != 0.0 {
|
||||
if cjk_gap(j) {
|
||||
adv += per_cjk;
|
||||
}
|
||||
} else if j + 1 < trimmed && sg.get(j + 1).is_some_and(|next| !next.continues_tcy()) {
|
||||
} else if j + 1 < body && sg.get(j + 1).is_some_and(|next| !next.continues_tcy()) {
|
||||
// Between glyphs, never inside a tate-chu-yoko block (one cell).
|
||||
adv += per_gap;
|
||||
}
|
||||
|
||||
@@ -585,3 +585,23 @@ fn character_alignment_lines_small_characters_up_with_the_largest_em_box() {
|
||||
assert!(near(place(CharAlign::EmBoxBottom), -0.12 * 20.0), "bottom: {}", place(CharAlign::EmBoxBottom));
|
||||
}
|
||||
}
|
||||
|
||||
/// Burasagari leaves Latin punctuation alone: in a measure of exactly "abcd", the full stop of
|
||||
/// "abcd. ef" doesn't hang with Standard or Forced (the line breaks as it does with None), and a
|
||||
/// Latin line ending in a full stop isn't shortened by Forced.
|
||||
#[test]
|
||||
fn burasagari_leaves_latin_commas_and_full_stops_inside_the_line() {
|
||||
use vectorcraft_doc::Burasagari;
|
||||
let measure = width(&point("abcd", style(20.0)));
|
||||
let lay = |text: &str, b: Burasagari| {
|
||||
let mut t = area(text, style(20.0), Rect::new(0.0, 0.0, measure + 0.01, 400.0), Justify::JustifyLeft);
|
||||
t.para.burasagari = b;
|
||||
layout(db(), &t).glyphs.iter().map(|g| (g.line, (g.origin.x * 100.0).round())).collect::<Vec<_>>()
|
||||
};
|
||||
for text in ["abcd. ef", "a b. efgh", "ab, cd, efgh"] {
|
||||
let none = lay(text, Burasagari::None);
|
||||
for b in [Burasagari::Standard, Burasagari::Forced] {
|
||||
assert_eq!(lay(text, b), none, "{text} {b:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -262,6 +262,22 @@ fn every_line_composer_is_more_even() {
|
||||
assert!(kp <= greedy + 1e-6, "every-line worst space {kp} vs greedy {greedy}");
|
||||
}
|
||||
|
||||
/// Burasagari (new type's Standard) leaves Latin paragraphs to the every-line composer: only a
|
||||
/// paragraph with a Japanese comma or full stop is composed line by line.
|
||||
#[test]
|
||||
fn burasagari_keeps_the_every_line_composer_for_latin_text() {
|
||||
// A measure where the two composers break the copy differently.
|
||||
let mut t = area(©.repeat(2), style(12.0), Rect::new(0.0, 0.0, 300.0, 4000.0), Justify::JustifyLeft);
|
||||
let ends = |t: &TextObject, c: Composer| {
|
||||
let l = layout_with(db(), t, &LayoutOptions { composer: c, ..Default::default() });
|
||||
l.lines.iter().map(|li| li.glyph_end).collect::<Vec<_>>()
|
||||
};
|
||||
let every_line = ends(&t, Composer::EveryLine);
|
||||
assert_ne!(every_line, ends(&t, Composer::SingleLine));
|
||||
t.para.burasagari = vectorcraft_doc::Burasagari::Standard;
|
||||
assert_eq!(ends(&t, Composer::EveryLine), every_line);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hyphenation_rules() {
|
||||
assert_eq!(hyphen::hyphenate_word("typography"), "typo-gra-phy");
|
||||
|
||||
@@ -406,3 +406,66 @@ fn mojikumi_sets_an_opening_bracket_flush_at_the_start_of_a_wrapped_line() {
|
||||
assert!((first.glyphs[0].advance - 20.0).abs() < 0.01, "vertical {vertical_type}: {}", first.glyphs[0].advance);
|
||||
}
|
||||
}
|
||||
|
||||
/// Burasagari: in a measure of exactly five ems, a comma that would be the sixth character goes to
|
||||
/// the next line with the character before it (None) or hangs outside the line (Standard, Forced);
|
||||
/// a full stop that is the fifth character stays inside (None, Standard) or hangs while the four
|
||||
/// before it fill the measure (Forced). Closing brackets don't hang. Horizontal and vertical, with
|
||||
/// Line-end Punctuation Half Width (the hanging mark is half width). Needs a font with full-width
|
||||
/// Japanese punctuation (skipped without one).
|
||||
#[test]
|
||||
fn burasagari_hangs_a_comma_or_full_stop_outside_the_line() {
|
||||
use vectorcraft_doc::{Burasagari, Mojikumi};
|
||||
// 20 pt type, justified, in a frame five ems along the lines.
|
||||
let lay = |text: &str, b: Burasagari, vertical_type: bool| {
|
||||
let mut t = TextObject::point(Point::ZERO, text, CharStyle { size: 20.0, ..CharStyle::default() });
|
||||
t.xf = Affine::IDENTITY;
|
||||
t.vertical = vertical_type;
|
||||
t.para.mojikumi = Mojikumi::LineEndHalf;
|
||||
t.para.justify = Justify::JustifyLeft;
|
||||
t.para.burasagari = b;
|
||||
let frame = if vertical_type { Rect::new(0.0, 0.0, 200.0, 100.0) } else { Rect::new(0.0, 0.0, 100.0, 200.0) };
|
||||
t.kind = TextKind::Area { frame: PathData::from_bezpath(&frame.to_path(0.1)) };
|
||||
layout(FontDb::global(), &t)
|
||||
};
|
||||
if (lay("一、二", Burasagari::None, false).glyphs[1].advance - 20.0).abs() > 2.0 {
|
||||
return; // no font with full-width punctuation here
|
||||
}
|
||||
let advances = |l: &TextLayout, n: usize| l.glyphs.iter().take(n).map(|g| (g.advance * 100.0).round() / 100.0).collect::<Vec<_>>();
|
||||
for vertical_type in [false, true] {
|
||||
// 、 as the sixth character.
|
||||
let none = lay("一二三四五、六七", Burasagari::None, vertical_type);
|
||||
assert_ne!(none.glyphs[4].line, none.glyphs[0].line, "vertical {vertical_type}: 五、 go to the next line");
|
||||
for b in [Burasagari::Standard, Burasagari::Forced] {
|
||||
let l = lay("一二三四五、六七", b, vertical_type);
|
||||
assert_eq!(l.glyphs[5].line, l.glyphs[0].line, "vertical {vertical_type} {b:?}: 、 hangs");
|
||||
assert_eq!(advances(&l, 6), [20.0, 20.0, 20.0, 20.0, 20.0, 10.0], "vertical {vertical_type} {b:?}: five ems inside, half an em outside");
|
||||
assert_eq!(l.glyphs[6].line, l.glyphs[0].line + 1, "vertical {vertical_type} {b:?}");
|
||||
}
|
||||
// 。 as the fifth character: it fits.
|
||||
for b in [Burasagari::None, Burasagari::Standard] {
|
||||
let l = lay("一二三四。六七", b, vertical_type);
|
||||
assert_eq!(l.glyphs[5].line, l.glyphs[0].line + 1, "vertical {vertical_type} {b:?}");
|
||||
// Four ems and a half: the half em left is spread between the five characters.
|
||||
assert_eq!(advances(&l, 5), [22.5, 22.5, 22.5, 22.5, 10.0], "vertical {vertical_type} {b:?}: 。 inside");
|
||||
}
|
||||
let forced = lay("一二三四。六七", Burasagari::Forced, vertical_type);
|
||||
let a = advances(&forced, 5);
|
||||
assert!((a[..4].iter().sum::<f64>() - 100.0).abs() < 0.05, "vertical {vertical_type}: the four fill the measure: {a:?}");
|
||||
assert_eq!(a[4], 10.0, "vertical {vertical_type}");
|
||||
assert_eq!(forced.glyphs[4].line, forced.glyphs[0].line);
|
||||
// A closing bracket doesn't hang: 」 as the sixth character goes on with 五.
|
||||
for b in [Burasagari::Standard, Burasagari::Forced] {
|
||||
let l = lay("一二三四五」六七", b, vertical_type);
|
||||
assert_ne!(l.glyphs[4].line, l.glyphs[0].line, "vertical {vertical_type} {b:?}");
|
||||
}
|
||||
// The full-width comma and full stop hang too.
|
||||
for mark in [',', '.'] {
|
||||
let l = lay(&format!("一二三四五{mark}六七"), Burasagari::Standard, vertical_type);
|
||||
assert_eq!(l.glyphs[5].line, l.glyphs[0].line, "vertical {vertical_type}: {mark} hangs");
|
||||
}
|
||||
}
|
||||
// Horizontal: the hanging mark starts at the frame's edge.
|
||||
let l = lay("一二三四五、六七", Burasagari::Standard, false);
|
||||
assert!((l.glyphs[5].origin.x - 100.0).abs() < 0.01, "{:?}", l.glyphs[5].origin);
|
||||
}
|
||||
@@ -3005,6 +3005,8 @@
|
||||
# Leading model (Paragraph panel menu, East Asian options).
|
||||
Top-to-Top Leading Interlineado de superior a superior
|
||||
Bottom-to-Bottom Leading Interlineado de inferior a inferior
|
||||
Burasagari Burasagari
|
||||
Force Forzar
|
||||
|
||||
# About › Contributors / Models (credits.rs)
|
||||
About Acerca de
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -15,6 +15,8 @@
|
||||
Ascender アセンダ
|
||||
Baseline ベースライン
|
||||
Bottom-to-Bottom Leading 欧文ベースライン基準の行送り
|
||||
Burasagari ぶら下がり
|
||||
Force 強制
|
||||
Center 中央
|
||||
Character Alignment 文字揃え
|
||||
Connect Selected End Points 選択した端点を連結
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -249,6 +249,8 @@
|
||||
Bottom 下
|
||||
Bottom in Front 下方置前
|
||||
Bottom-to-Bottom Leading 基线至基线行距
|
||||
Burasagari 标点悬挂
|
||||
Force 强制
|
||||
Bottom: 下:
|
||||
Bounds 边界
|
||||
Bounds (Fill Proportionally) 边界 (按比例填满)
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -260,6 +260,8 @@
|
||||
Bottom 下
|
||||
Bottom in Front 下方置前
|
||||
Bottom-to-Bottom Leading 基線至基線行距
|
||||
Burasagari 標點懸掛
|
||||
Force 強制
|
||||
Bottom: 下:
|
||||
Bounds 邊界
|
||||
Bounds (Fill Proportionally) 邊界 (按比例填滿)
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -162,6 +162,23 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if menu_item(ui, tl!("Bottom-to-Bottom Leading"), has, has && !top_to_top) {
|
||||
format(app, json!({"leadingModel": "romanBaseline"}));
|
||||
}
|
||||
// Hanging punctuation: a comma or full stop ending a line stands outside it.
|
||||
let hang = style.as_ref().map(|(_, p)| p.burasagari);
|
||||
ui.add_enabled_ui(has, |ui| {
|
||||
// Indented like the items beside it (their check column).
|
||||
ui.menu_button(format!(" {}", tl!("Burasagari")), |ui| {
|
||||
use vectorcraft_doc::Burasagari;
|
||||
for (label, b, key) in [
|
||||
(tl!("None"), Burasagari::None, "none"),
|
||||
(tl!("Regular"), Burasagari::Standard, "standard"),
|
||||
(tl!("Force"), Burasagari::Forced, "forced"),
|
||||
] {
|
||||
if menu_item(ui, label, true, hang == Some(b)) {
|
||||
format(app, json!({"burasagari": key}));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
ui.separator();
|
||||
menu_item(ui, tl!("Single-line Composer"), false, false);
|
||||
@@ -171,7 +188,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
para_cmd(app, "text.setStyle", json!({"justify": "auto"}));
|
||||
format(
|
||||
app,
|
||||
json!({"leftIndent": 0, "rightIndent": 0, "firstLineIndent": 0, "spaceBefore": 0, "spaceAfter": 0, "hyphenate": false, "direction": "auto", "leadingModel": "romanBaseline"}),
|
||||
json!({"leftIndent": 0, "rightIndent": 0, "firstLineIndent": 0, "spaceBefore": 0, "spaceAfter": 0, "hyphenate": false, "direction": "auto", "leadingModel": "romanBaseline", "burasagari": "standard"}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+14
@@ -2112,6 +2112,20 @@ Type and Convert To Point Type. Either keeps the text and its styles and is one
|
||||
{"name":"run_command","arguments":{"command":"type.convertToAreaType","params":{"ids":[42]}}}
|
||||
```
|
||||
|
||||
## Hanging punctuation (burasagari)
|
||||
|
||||
`text.setFormat {burasagari: "none"|"standard"|"forced"}` sets the Paragraph panel menu's Burasagari (None / Regular /
|
||||
Force) of the selected type (or `ids`): an East Asian comma or full stop ending an area type line (、。,., half-width
|
||||
、。) hangs outside the frame, Regular only when it doesn't fit, Force always (the rest of the line then fills the
|
||||
measure). Closing brackets and Latin punctuation don't hang; point type, right-to-left and hyphenated lines are left as
|
||||
they are. The hanging mark is as wide as its punctuation spacing makes it (`mojikumi`). New type (the Type tools,
|
||||
`text.create`) takes `standard`; documents from before it and imported text keep `none`, and `none` isn't saved. One
|
||||
undo step.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"text.setFormat","params":{"burasagari":"forced"}}}
|
||||
```
|
||||
|
||||
## Moving and flipping type on a path
|
||||
|
||||
Type on a path flows between a start and an end bracket, stored as fractions of its path's length.
|
||||
|
||||
Reference in new issue
Block a user