With Envelope Options > Distort Appearance on, VectorCraft turned the strokes
of type in an envelope into filled outlines and bent them with the art, so a
stroke got thinner where the grid squeezes the type and thicker where it
stretches it. The drawn art of users' .ai files keeps those strokes as stroked
paths at the type's own weights, and the ring they paint is as wide wherever
the grid squeezes or stretches the type. An envelope of stroked type in such a
grid therefore didn't draw as its drawn art and kept it instead of opening live.
Type's strokes (its characters' and those on its own appearance) now paint
along the bent glyph outlines at their own weight, with Distort Appearance on
or off, also in an envelope inside an envelope:
- doc: `map_node` maps the groups that hold type's glyph outlines with
`Hooks::type_appearance` in place of `Hooks::appearance`, for type in the
envelope's contents and for type that a live object in them (an inner
envelope) outlined (`Warper::type_outlines`). Such a group's opacity mask
is mapped with the ordinary hooks, so it bends as other art does. Groups
marked as type's outlines in the contents themselves (art an earlier Expand
made) stay plain art.
- effects: `bake_type_appearance`, `bake_appearance` with strokes kept as
strokes: geometry effects on type's fills and strokes still bake in and bend.
- doc: `max_outset` counts type's character strokes as type's own visual
bounds do (`char_stroke_outset`). It left them out, though they already
painted past an envelope's grid where the glyphs reach its edge; this also
grows the visual bounds of blends and repeats of stroked type and the reach
of symbol art holding it.
Paths' strokes, geometry effects, opacity masks, linear gradients and pattern
fills bend as before. The canvas, hit testing, effect bounds, Expand, every
export and the .ai save take the evaluation from the same hooks. Expand of an
envelope of stroked type gives the bent outlines with the strokes on them.
This changes how VectorCraft's own envelopes of stroked type draw.
Tests: envelope_distort::type_strokes_paint_at_their_own_weight_in_a_squeezed_envelope,
a_stroke_on_types_appearance_paints_at_its_own_weight_in_a_squeezed_envelope,
type_strokes_paint_at_their_own_weight_in_an_envelope_inside_an_envelope,
a_paths_stroke_still_bends_in_a_squeezed_envelope,
an_opacity_mask_on_type_bends_as_other_art,
the_visual_bounds_of_an_envelope_hold_its_types_strokes,
the_visual_bounds_of_a_repeat_of_stroked_type_grow_by_half_the_stroke,
expanding_an_envelope_of_stroked_type_keeps_the_stroke_at_its_weight,
bake::tests::type_pieces_bake_their_geometry_effects_and_keep_their_strokes and
tests_native::an_envelope_of_stroked_type_whose_drawn_art_strokes_the_bent_outlines_at_the_weight_opens_live.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At the Type tool's caret, kerning goes on the character before the caret.
At the start of the text there is none, so Alt+arrows, the Kerning field
and Reset Tracking and Kerning did nothing; at a later paragraph's start
the kerning landed on the invisible line break.
CharStyle::kerning_before (kerningBefore) is the space before a character
that starts a line by itself: the first character of the text, of a
paragraph, or after a forced line break. Shaping gives its cluster the
space and draws it that much later along the line (right to left the
space is at its right; in vertical type it moves down the column). The
caret at such a line's start sets it (edit::caret_kerning_target), in
type.step, type.resetTracking and the Character panel; text.setRangeStyle
takes it. SVG export starts the line where the character is drawn. The
Eyedropper and character styles leave it alone.
Type › Dynamic Text (`type.dynamicText`, also on the Contextual Task Bar
for type on a path) scales the text by one factor so it fills the span
between its start and end brackets, and the layout fits it again after
each edit to the text, the path or a bracket. The first character's
size moves in 0.1 pt steps within 0.1 to 1,296 pt. Turning it off, or
threading the type, keeps the sizes it was drawn at.
- The model saves `dynamicText` only when on.
- SVG, the legacy .ai writer and the .ai editing data write the drawn
sizes through `vectorcraft_text::drawn`; the editing data warns that
the setting is not written.
- Shrink Text to Fit and Dynamic Text step the size the Character panel
shows when a size measures the cap height, x-height or ICF box.
Docs: docs/mcp.md, docs/type-parity.md, docs/target-app-parity.md.
Part of #4134.
A centred or right-aligned line of tracked type was measured with the
tracking after its last character, so it sat half that tracking
(centred) or the whole tracking (right-aligned) left of where the page
of users' .ai files draws it. That page aligns a line as wide as its
characters without that tracking.
Shaping now records the tracking each glyph's advance holds
(SGlyph::tracking), and a line that isn't justified leaves its last
character's out of the width it is aligned by. The last character is
the last one the line draws before a paragraph's end, a forced line
break or the spaces a wrap leaves; spaces or a tab typed before a
paragraph's end or a forced line break keep the letter's tracking, as
the page does. Forced line breaks and wraps aren't in the files: they
follow the rule so that turning a wrap into a paragraph or a forced line
break keeps the line in place. Hyphenated lines, lines whose last
character hangs, vertical and right-to-left lines and type on a path
keep it. The tracking stays in the glyph's advance, so the caret,
selections, line breaking and Fit Headline are unchanged.
LineInfo::trailing gives the amount. Right-aligned area type sets the
text after its last tab the same way.
What aligns by line widths follows. Convert To Area Type makes its
frame end where the lines are aligned, with room for the tracking, so
tracked type converted either way stays in place. Affinity import lines
up differently aligned paragraphs of artistic text by the aligned
widths. SVG import moves a centred or right-aligned letter-spaced line
back by its share of the last letter spacing, so it stays where a reader
draws it and SVG export, which anchors lines at the end of their
advances, reads back unchanged.
Centred and right-aligned tracked type lays out differently than before
in every document.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In horizontal type, tracking and manual kerning are now thousandths of
the em as the Horizontal Scale draws it: at 50%, a tracking of 100 adds
0.05 em after each character. They were added at the full em while the
glyph advances took the scale, so narrow tracked type drifted from the
page of users' .ai files letter by letter. That page writes such type's
character spacing as the tracking itself in a text space as wide as the
scale, which PDF scales by it (ISO 32000-1 9.4.4). Vertical type keeps
the full em: its advances down the column don't all take the scale.
vectorcraft_text::spacing_scale gives the factor. Shaping, inline
graphics and the Optical pairs use it, and so do the conversions from
points back to thousandths of an em: the automatic kerning the Character
panel reads and Touch Type's limits (glyph_em), Combine Point Text, and
PDF import, whose tracking is now the file's Tc. Scale Text Attributes
now keeps tracked type where a horizontal stretch drew it.
SVG export writes letter-spacing in thousandths of the font size it
writes (the vertical scale), so uniformly scaled type keeps its spacing
in a viewer.
Type with both a Horizontal Scale other than 100% and tracking or manual
kerning lays out differently than before, in every document.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Direct Selection showed and dragged the anchors of type on a path's path
(#3980) but not their direction handles: handle_anchors read only a
path's own data, and the canvas draws only a path's handles.
- text.setPathHandle moves a handle of type on a path's path, in
document space, as path.setHandle moves a path's; the brackets keep
their fractions and the text reflows. One undo step.
- Direct Selection draws the handles of selected type on a path and
drags them with it (Alt: one side, Shift: 45 degree steps).
The custom dictionary's words in a list, with an Entry field and Add, Delete
and Change, applied on Done, as the public Check and correct spelling page
describes it. The menu item moves from the Spelling submenu to the Edit menu,
where the page puts it.
A right-click on type has a Spelling submenu with Auto Spell Check, as the
public Check and correct spelling page describes. A submenu that holds only
toggles shows their check marks, even in the context menu.
A closed font family field stayed ready for the wheel as long as it kept
its place and drew in each pass. Coverage was checked only when a pointer
move or wheel turn arrived. When a window, modal or popup covered a still
pointer and went away with no input in between, the next plain wheel
turn changed the selected type's font.
The field now stops being ready, and a wheel stream it held ends, in any
pass where the last pointer position is over the field but on another
layer. The pointer has to move onto the field again before the wheel
steps the font.
Since #5331, a closed font family field in Character, Properties or the
Control bar took every plain vertical wheel turn while the pointer was
over it. When a panel scrolled the field under a still pointer, the
field took the rest of the turns: the panel stopped scrolling and each
turn changed the selected type's font.
The field now takes the wheel only when the pointer was moved onto it
after the field last moved, and the wheel stream began there. A stream
that began anywhere else, a sideways scroll included, stays there until
0.5 s pass with no wheel input. A stream that leaves its field does not
return to it. It leaves when the pointer moves off the field or out of
the window, when the field moves or misses a layout pass, and when a
turn arrives while an overlay covers the field. Trackpad momentum stays
with the stream it follows and never starts one on a field. The rule and
its 0.5 s are VectorCraft's own.
Wheel turns the field does not take stay in the input in their order.
Moving the pointer onto the field and turning the wheel still steps the
font, as #5253 asks, in floating panel windows too. When the pointer was
moved onto the field while a window or overlay covered it, the pointer
has to move again after the cover goes away before the wheel steps the
font.
The Measure and Measure & Inspect tools ignored View › Snap to Glyph:
their press and end points snapped only to targets that hold no glyph
guides.
With Snap to Glyph, Smart Guides and Alignment Guides on, and Snap to
Grid and Snap to Pixel off, a drag's press and end land within the
snapping tolerance on the guides of live type that the Character
panel's options allow, except Anchor Point. A Shift-drag's end slides
along its 45° step to them, and the guide shows during the drag. With
Snap to Glyph off, the tools snap to the same targets as before and
show no guide. The targets are kept while the document, the window and
the snapping options stay the same, so a drag lays out the type once.
A mark glyph's vertical translation started a new draw_glyphs batch, so
krilla received the cluster's source range again and mapped its
characters twice. Copying or searching the exported PDF then found
doubled letters for Arabic harakat in any font, and for every dotted
letter in fonts that draw dots as zero-advance mark glyphs (Noto Sans,
Naskh and Kufi Arabic).
Keep compatible glyphs on the same layout line in one batch and carry
their relative vertical placement as per-glyph y_offset; horizontal
placement is unchanged (next_x - x advances). Font, size, linear
transform, line/column, paint run, list marker and hyphen boundaries
still split batches. Krilla keeps owning cluster mappings and
ActualText. No importer change.
Regressions decode the content stream and ToUnicode maps: one batch per
layout line, no duplicated cluster assignment, one ActualText per
multi-glyph cluster, contours within 0.05 pt of outline export; mixed
scripts, vertical columns, path rotations and PDF/X variants covered.
The reader round-trip stays an ignored test until the importer consumes
ActualText (#6165).
Addresses #6309.
docs/mcp.md: vectorcraft-cli's mcp and run sessions keep the words in the file too; only without preferences do they last until the app closes. docs/type-parity.md: the custom dictionary commands are done and only the dialogs and Auto Spell Check remain, with the evidence at the end of the row.
text.spelling.addWord, removeWord and changeWord are Check Spelling's Add
and Edit Custom Dictionary's Add, Delete and Change; text.spelling.dictionary
lists the words. They live in craft-proof, which every language's check
reads. The desktop app and vectorcraft-cli keep them in
`Custom Dictionary.txt` next to the preferences (none without preferences);
elsewhere they last until the app closes.
text.spelling.ignoreAll is Ignore All: the active document's checks skip the
word while it is open; not saved, not undoable, and not a change to the
document's revision.
Behaviour: https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/check-and-correct-spelling.html
Language per character rows in docs/type-parity.md: spelling is no longer listed as not yet done, and the hyphenation note names the languages that still lack it.
Switching documents, or windows of a document, deactivates the Type tool, which clears the tool its double-click came from, so Escape after a later edit goes to the Selection tool. The field's comment and the two checklist rows now say so.
Only a double-click on type from another tool records the tool Escape and
Cmd+Enter return to, and the Type tool keeps it until another tool is
chosen. Choosing the Type tool from the toolbar records none, so Escape
goes to the Selection tool. The type and UI checklists and the doc
comments on prev_tool, end_editing and claims_key now say so.
docs/type-parity.md held four copies of the point, area and path type
row, three of them with older Escape wording. They are one row again,
with the Type Orientation sentence that only one copy had.
Two problems in the Character Styles panel and its options dialog.
Opening the options took the panel menu's item, which is easy to miss; a double-click on a style's
row did nothing beyond applying the style a second time. A double-click on a user character style's
row now opens its Character Style Options (`ui.charStyleOptions`), as the menu item does. A single
click still applies the style, the second click of the double-click does not apply it again (the
Normal style's included), and with Shift, Cmd/Ctrl or Alt held the double-click only follows the
click rules. The built-in Normal style opens no dialog. Double-clicking a paragraph style's row similarly opens
its options, which are the rename dialog the Paragraph Styles panel menu opens (`ui.paramDialog`
running `paraStyle.rename`): paragraph styles have no full options window yet.
The dialog then grew with its content: the General section lists every attribute of the style, so
the window covered the screen and its buttons could sit below it. The content now scrolls both ways
inside a window at most 420 points tall (less on a short screen) and never wider than the points
left beside the section list, with one scroll offset per section. The dialog keeps a fixed width of
720 points, enough for the widest section's controls, in every section. The content's height is the
space the footer leaves, measured from the footer drawn last frame, so a large interface scale's
taller footer is reserved too and OK and Cancel stay visible. The section list stays outside the
scroll area, so scrolling a long section never moves the navigation; the section is read after the
list handles a click, so a newly picked section gets its own scroll offset in the same frame.
Tests:
`panels::text_styles::tests::double_clicking_a_character_style_row_opens_its_options` selects text,
double-clicks a character style's row and checks the dialog opens with the style's name, one apply
ran from the first click and the second click applied nothing; a paragraph style's row opens none.
`double_clicking_the_normal_style_opens_no_options` checks the Normal style opens none and is
applied once.
`dialogs::char_style_options::tests::the_sections_scroll_in_a_window_shorter_than_the_general_list`
draws the dialog on a 900 by 560 point screen and checks OK and Cancel stay on it, the dialog keeps
its 720-point width in two sections, and the General list is taller than its scroll area.
`every_section_and_the_buttons_fit_the_smallest_window` checks the app's minimum 800 by 500 point
window in all five sections: every section label, OK and Cancel are on the screen.
`a_scrolled_section_keeps_its_offset_after_switching_away_and_back` scrolls General, clicks Basic
Character Formats and then General again, and checks General''s content returns to the same place:
the section is read after the list handles the click, so no other section stores its offset.
`every_section_and_the_buttons_fit_the_smallest_window_at_twice_the_scale` checks the same in the
400 by 250 points the minimum window holds at 2x UI scale, before and after a wheel scrolls the
content.
Docs: type-parity (a panel row and the revision history), ui-parity (a panel row and the revision
history), target-app-parity (two evidence bullets and the revision history) and roadmap (the
revision history), with the status lines updated.
The type parity row no longer says the NumLock-on path was tried on
Windows: neither path has been run on a Windows machine yet. The comment
on the NumLock-off egui keys says Clear comes as the top-row 5, and the
test comment names the keys 0151 and 0234 actually use.
Follow-up to #5939. With NumLock off the keypad sends Insert, End, arrows,
Clear, Home and Page Up/Down; Windows still composes the code from them,
but the app also moved the caret, so the character landed elsewhere. With
Alt held, those keys without the extended-key flag now count as digits and
are taken out of the frame; the separate navigation keys keep their meaning.
Escape and Cmd+Enter (Ctrl+Enter on Windows and Linux) end text editing
and switch back to the tool that double-clicked into the type, or to the
Selection tool when there was none (`TypeTool::end_editing`). The
evidence lines in docs/type-parity.md, docs/ui-parity.md and
docs/target-app-parity.md, and the doc comments on `prev_tool` and
`claims_key`, said they always switch to the Selection tool.
- text.spelling.check gives craft-proof one paragraph at a time, as
check_text_with expects: its language detection reads a paragraph, so a
Vietnamese paragraph no longer leaves the English ones after it unchecked,
and the check stops at the paragraph where the 10,000-word list fills up
instead of proofing the rest of the text first.
- Given `ids` are checked in paint order, as the result promises, not in the
order they were passed.
- Docs: English words come from the first dictionary craft-proof finds
(dictionaries folder Hunspell, SCOWL from craft-i18n, system Hunspell),
with the Moby word list as the last fallback, not from Moby alone.
- Doc comments on the command functions and test helpers.
- text.spelling.check checks a given `ids` as edit.findReplace does
(checked_ids_param): a non-array, a negative, non-numeric or unknown id is
an error instead of a check of the whole document. Null still means every
visible, unlocked type object.
- The byte-offset test puts an em dash, not "café", before the unknown word,
so it passes whichever dictionaries craft-proof is built with (CI builds
with craft-i18n's, where "café" is unknown).
- Docs: the spell check engine in target-app-parity.md (Type advanced rows
and evidence), gaps.md (G4), roadmap.md (M7) and the Language per
character rows of type-parity.md, with status lines and revision history.
The Edit > Spelling items have been placeholders with nothing behind them. This adds the
engine half of Check Spelling on craft-proof, the shared spelling engine in craft-libs (at the
revision the other craft-libs crates already pin), so no word list is copied here:
- text.spelling.check {ids?} lists the words the dictionaries don't know in every visible,
unlocked type object (or ids), each run checked in its Character panel Language (English
when it has none; nothing in a language without a dictionary), as {id, start, end, word,
lang}. start/end are byte offsets in the object's text, the ones text.editRange takes, so a
word is changed with text.editRange as one undo step. At most 10,000 words, with `more`.
- text.spelling.suggest {word, max?, lang?} gives the dictionary's suggestions, asked for one
word at a time as craft-proof recommends.
English comes from the public-domain Moby word list built into craft-proof. Its dependencies
(log, miniz_oxide, toml, unicode-normalization) are already in the lockfile. The Check
Spelling dialog, Auto Spell Check and Edit Custom Dictionary follow in later PRs.