mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 21:56:24 +00:00
Type > Dynamic Text (type.dynamicText) now also works on area type as Block. Each line as typed (each paragraph, and each forced line break in one) is set to the frame's width, less the inset, the column gutters and the paragraph's indents, by changing its font size only. Lines are not broken again. The first character's size moves in 0.1 pt steps within 0.1 to 1,296 pt, and the line's other characters keep their proportions. The sizes are written into the characters and fitted again after every edit to the text or the frame, in the same undo step. - Word Gap (0 to 1,000 % of a word space, default 100) sets every paragraph's Desired word spacing; Line Gap (-1,296 to 1,296 pt, default 0) is added to each character's size for its leading. Both are VectorCraft's own ranges and defaults, saved in the area options only when they differ from the defaults. - Area Type Options gains a Block fit (text.areaOptions fit "block", with wordGap and lineGap), and the Properties panel's Paragraph section shows a Dynamic Text checkbox with Word Gap and Line Gap rows. - Block replaces Auto Size and Shrink Text to Fit. Off keeps the sizes. Threaded area type is refused, as for type on a path. - Empty lines keep their size, a frame with no width changes nothing, vertical type fits each line to the frame's height, and right-to-left lines and list markers are fitted like other lines. A refit runs at most four extra layouts. 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, 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)]
|
||
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;
|