mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 10:45:45 +00:00
Characters take two new attributes, `akiLeft` and `akiRight`: space before and after each character in reading order, in thousandths of an em from 0 to 1000, measured as tracking is. `text.setFormat`, `text.setRangeStyle` and character styles set them; values that are not numbers or are outside the range are refused, and a file holding one outside the range opens with the nearer end. Both are saved only when not 0. - Layout: Left is the space above a character in vertical type and on its right in right-to-left type. Wrapping counts the space, a line's end keeps it when aligned, and justification and Optical kerning leave it at its set width. A ligature, a letter with its marks and a tate-chu-yoko block take the values of their first character. - With Fractional Widths off or System Layout on, the space before and the space after a character are each set in whole points and added to its rounded width, so justification keeps them at that width. A tate-chu-yoko block rounds the space above and below it the same way. - Character panel: with Show East Asian Options on, Insert Aki Left and Insert Aki Right fields with 0, 250, 500 and 1000 in their menus; Reset Panel sets both to 0. - Character Style Options › Advanced Character Formats shows Insert Aki Left and Insert Aki Right, with the Character panel's 0, 250, 500 and 1000 in their menus; a typed value below 0 or past 1000 sets 0 or 1000, and Reset Panel unsets both. - SVG export: live text places each character the space moves, and type on a path with Insert Aki is written as outlines. Docs: docs/mcp.md, docs/target-app-parity.md.
152 lines
9.0 KiB
Rust
152 lines
9.0 KiB
Rust
//! Text objects (model only; layout and glyph outlines live in `vectorcraft-text`).
|
||
|
||
use serde::{Deserialize, Serialize};
|
||
use vectorcraft_color::{Color, Paint};
|
||
use vectorcraft_geom::{Affine, PathData, Point, Rect, Vec2};
|
||
|
||
use crate::appearance::{Appearance, AppearanceItem, Dash, FillLayer, LineCap, LineJoin, StrokeAlign, StrokeLayer};
|
||
|
||
mod area;
|
||
mod char_style;
|
||
mod composite;
|
||
mod object;
|
||
mod para_style;
|
||
mod runs;
|
||
mod tabs_lists;
|
||
mod wrap;
|
||
|
||
pub use area::{AreaFit, AreaOptions, PathAlign, PathEffect, TextFlow, TextWrap, VerticalAlign, WrapShape};
|
||
use char_style::yes;
|
||
pub use char_style::{CharAlign, CharPosition, CharStyle, Digits, FontHeightReference, GlyphChoice, MAX_AKI, ScriptMetrics};
|
||
pub use composite::{
|
||
COMPOSITE_BASELINE, COMPOSITE_FONTS_MAX, COMPOSITE_NAME_MAX, COMPOSITE_SCALE, COMPOSITE_SIZE, CharClass, CompositeFont, CompositePart,
|
||
CompositeParts,
|
||
};
|
||
pub use para_style::{
|
||
Burasagari, Composer, FirstBaseline, HyphenationOptions, JustificationOptions, Justify, Kashida, Kinsoku, KinsokuShoriType, LeadingModel,
|
||
Mojikumi, ParaDirection, ParaStyle, SingleWordJustify,
|
||
};
|
||
pub use runs::{INLINE_CHAR, INLINE_MAX_SCALE, InlineArt, RubyAlign, RubyPosition, RubySpan, TextKind, TextRun, TextStyleDef, TypeAntiAlias};
|
||
pub use tabs_lists::{
|
||
Bullet, DEFAULT_TAB_INTERVAL, LIST_HANGING_INDENT, LIST_LEVEL_INDENT, LIST_LEVELS, ListAlign, ListCounts, NumberStyle, ParaList, TabAlign,
|
||
TabStop,
|
||
};
|
||
pub use wrap::{resolve_wrap, wrap_work};
|
||
|
||
/// A text object. `runs` split into paragraphs at `\n`.
|
||
///
|
||
/// Paragraph attributes: `para` is paragraph 0's (and, while `paras` is empty, every
|
||
/// paragraph's). Invariant (kept by [`TextObject::normalize_paras`]): `paras` is either empty —
|
||
/// every paragraph uses `para` — or holds one style per paragraph with `paras[0] == para` and at
|
||
/// least two different styles. Readers that only know `para` see the first paragraph's style.
|
||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||
pub struct TextObject {
|
||
/// Top-to-bottom, right-to-left writing. Defaults to horizontal for old documents.
|
||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||
pub vertical: bool,
|
||
pub kind: TextKind,
|
||
/// Maps text space (origin = first baseline start for point type) to the document.
|
||
pub xf: Affine,
|
||
pub runs: Vec<TextRun>,
|
||
/// Ruby (furigana / phonetic guide) over byte ranges of the plain text; empty = none. Kept on
|
||
/// the object, so the runs (and their styles) stay the text itself.
|
||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||
pub ruby: Vec<RubySpan>,
|
||
#[serde(default = "ParaStyle::omitted")]
|
||
pub para: ParaStyle,
|
||
/// Per-paragraph attributes (see the type's docs); empty = every paragraph uses `para`.
|
||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||
pub paras: Vec<ParaStyle>,
|
||
/// Threaded text: the first paragraph continues from the frame before this one in its thread,
|
||
/// so its first line here has no first-line indent and no list marker. The engine sets it for
|
||
/// every frame of a thread when the story flows through them and when a file opens, and clears
|
||
/// it on a frame that leaves its thread and on a copy. Not saved.
|
||
#[serde(skip)]
|
||
pub first_para_continues: bool,
|
||
/// Area Type Options (area type only).
|
||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||
pub area: AreaOptions,
|
||
/// Type on a Path effect (type on a path only).
|
||
#[serde(default, rename = "pathEffect", skip_serializing_if = "crate::skip::is_default")]
|
||
pub path_effect: PathEffect,
|
||
/// Type on a Path Options › Align to Path (type on a path only).
|
||
#[serde(default, rename = "pathAlign", skip_serializing_if = "crate::skip::is_default")]
|
||
pub path_align: PathAlign,
|
||
/// Type on a Path Options › Spacing in points (type on a path only): glyphs are spaced as if
|
||
/// set this far above the path, which closes them up round the outside of a curve and opens
|
||
/// them up round the inside.
|
||
#[serde(default, rename = "pathSpacing", skip_serializing_if = "crate::skip::is_default")]
|
||
pub path_spacing: f64,
|
||
/// Type › Dynamic Text. On type on a path, the layout scales the text by one factor so it
|
||
/// fills the span between its start and end brackets, and fits it again after each edit; the
|
||
/// stored sizes keep their proportions, and the layout's `fit_scale` is the factor drawn. On
|
||
/// area type it is Block: after each edit the engine sets each line as typed to the frame's
|
||
/// width by its font size and writes the sizes into the characters, with the gaps of
|
||
/// [`AreaOptions::word_gap`] and [`AreaOptions::line_gap`]. Point type does not use it. Off for
|
||
/// new type and for documents from before it, and saved only when on.
|
||
#[serde(default, rename = "dynamicText", skip_serializing_if = "crate::skip::is_default")]
|
||
pub dynamic_text: bool,
|
||
/// Wrap objects above this area type, resolved by the engine after each edit (text space).
|
||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||
pub wrap: Vec<WrapShape>,
|
||
/// Type › Optical Margin Alignment: in every paragraph, Latin punctuation and the slanted
|
||
/// sides of letters such as A and W at a line's start or end hang outside the margin there
|
||
/// (point and area type, and type on a path at its brackets; see the layout's `optical_hangs`).
|
||
/// Off for new type and for documents from before it, and saved only when on.
|
||
#[serde(default, rename = "opticalMarginAlignment", skip_serializing_if = "crate::skip::is_default")]
|
||
pub optical_margin: bool,
|
||
/// Character panel › Anti-aliasing Method: how the type's edges are drawn as pixels. Each
|
||
/// frame of a thread keeps its own method. Sharp for new type and for documents from before it,
|
||
/// and saved only when it is another method.
|
||
#[serde(default, rename = "antiAlias", skip_serializing_if = "crate::skip::is_default")]
|
||
pub anti_alias: TypeAntiAlias,
|
||
/// Character panel menu › System Layout, copied from the document: when on, each character
|
||
/// that takes space advances by a whole number of points. Off by default, and saved only when
|
||
/// on. The document setting is [`crate::DocSetup::system_layout`].
|
||
#[serde(default, rename = "systemLayout", skip_serializing_if = "crate::skip::is_default")]
|
||
pub system_layout: bool,
|
||
/// Character panel menu › Fractional Widths: spacing between characters uses fractions of a
|
||
/// point. On for new type and for documents from before it, and saved only when off. Off, each
|
||
/// character that takes any space advances by a whole number of points, at least one, so small
|
||
/// type does not run together. The setting is the whole type object, not a range of characters.
|
||
/// <https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/turn-fractional-character-widths-off-or-on.html>
|
||
#[serde(default = "yes", rename = "fractionalWidths", skip_serializing_if = "crate::skip::is_true")]
|
||
pub fractional_widths: bool,
|
||
/// Threaded text: the numbered list paragraphs the story's earlier frames count before this
|
||
/// frame's first paragraph ([`ParaList::next_marker`]), so one count runs through the story.
|
||
/// The engine sets it whenever it re-flows a thread. Type that is not threaded has 0, and the
|
||
/// file saves the count only when it is not 0.
|
||
#[serde(default, rename = "listCount", skip_serializing_if = "crate::skip::is_default")]
|
||
pub list_count: u32,
|
||
/// Threaded text: as [`Self::list_count`], the counts the earlier frames carry into this frame
|
||
/// at list levels 2 on, level 2 first ([`ListCounts::from_parts`]). Empty when they are all 0,
|
||
/// and the file saves them only when not empty. A file's longer list opens with the first 8.
|
||
#[serde(default, rename = "listLevelCounts", skip_serializing_if = "Vec::is_empty", deserialize_with = "tabs_lists::read_level_counts")]
|
||
pub list_level_counts: Vec<u32>,
|
||
/// Cached layout bounds in text space, filled in by the layout engine (not serialized).
|
||
#[serde(skip)]
|
||
pub cached_bounds: Option<Rect>,
|
||
/// Cached baselines in text space, start to end, one per line that holds characters (a
|
||
/// vertical column's centre line), filled in with `cached_bounds` (not serialized).
|
||
#[serde(skip)]
|
||
pub cached_baselines: Vec<(Point, Point)>,
|
||
/// Cached ink of the glyphs in text space, underline and strikethrough bars included
|
||
/// (`vectorcraft_effects::preview::laid_out_ink`, `None` when nothing draws),
|
||
/// with the `cached_bounds` it was measured with: filled in with `cached_bounds` by the engine's
|
||
/// text edits, and stale once `cached_bounds` differs ([`Self::ink`]). Not serialized.
|
||
#[serde(skip)]
|
||
pub cached_ink: Option<(Rect, Option<Rect>)>,
|
||
}
|
||
|
||
/// Conversion from stored character attributes to document-space type controls.
|
||
/// Point size follows the transformed em-height axis; horizontal scale is the ratio of
|
||
/// the two transformed em axes. Rotation, reflection and shear remain in the affine.
|
||
#[derive(Clone, Copy, Debug)]
|
||
pub struct TextStyleScale {
|
||
pub points: f64,
|
||
pub horizontal: f64,
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests;
|