Commit Graph
414 Commits
Author SHA1 Message Date
BFlat 239152b6c8 Merge pull request #7303 from dotCooCoo/fix/slanted-seam-tolerance
Area type: lines stop at a gap between slanted contours however narrow
2026-11-14 07:32:55 +07:00
BFlat f6e84df143 Merge pull request #7306 from dotCooCoo/fix/path-type-rtl-fit
Type on a path: overflowing right-to-left type shows the longest start that fits
2026-11-14 06:26:48 +07:00
BFlat efba8bfd89 Merge pull request #7295 from dotCooCoo/fix/negative-wrap-offset
Text Wrap: a negative Offset lets text run into the wrap object
2026-11-14 06:26:39 +07:00
dotCooCoo 0fc37a0ddf Merge main into fix/slanted-seam-tolerance 2026-11-13 15:17:12 -08:00
BFlat 795f01782f Merge pull request #7272 from dotCooCoo/fix/invert-wrap-one-pass
Text Wrap: Invert Wrap intersects a line's spans in one pass
2026-11-14 05:41:20 +07:00
BFlat 64b4cb1ad6 Merge pull request #7300 from dotCooCoo/fix/composite-fonts-per-document
Type: documents that define one composite font name differently each draw in their own
2026-11-14 05:40:49 +07:00
BFlat c135ba1f9b Merge pull request #7279 from dotCooCoo/fix/path-type-probe-wider
Type: threaded type on a path aligned Justify All is measured by paragraphs
2026-11-14 05:40:38 +07:00
BFlat 55fe8b62d4 Merge pull request #7278 from dotCooCoo/fix/rtl-tab-line-hyphenation
Type: right-to-left lines with tabs are measured as they are drawn
2026-11-14 05:39:52 +07:00
dotCooCoo 77e4dceb9d Type on a path: overflowing right-to-left type shows the longest start that fits
Right-to-left type on a path that overflows searched for the start of its
text that fits with a binary search, which assumed that a longer start
never fits once a shorter one does not. A letter tracked to a negative
advance breaks that: with steps of 6, 6, -3 and 6 points in a 10 point
span, the first three letters fit while the first two do not, and the
search showed only the first letter.

- Without hanging punctuation, and with no spacing or on a path of one
  straight segment, every start is measured in O(n log n): each glyph
  keeps its place in the whole text's visual order, except the trailing
  whitespace and controls that UAX #9 rule L1 sets to the paragraph's
  level.
- With hanging punctuation, or spacing on a curve, when a letter has a
  negative advance, a start is a candidate only when its glyphs, in its
  own visual order and each at the least step it can take, reach no
  farther than the path's length. This lower bound holds with negative
  advances.
- At most 32 candidates are arranged from the longest. When none of
  them fits, a binary search over the rest shows a start that fits,
  which is not always the longest.
- With hanging punctuation, or spacing on a curve, text with no negative
  advance keeps the binary search.

Right-aligned and centered type on a curve with Spacing was placed using
the width its glyphs have at the start bracket, while the steps were
measured where the line was placed, so a line that fits could end past
the end bracket and lose glyphs. It is now placed using the width of the
steps it has where it is placed.
2026-11-13 14:04:05 -08:00
dotCooCoo 08f7ce6670 Area type: lines stop at a gap between slanted contours however narrow
Two contours of an area-type frame now count as meeting along a slanted
edge only when their edges lie exactly on one line, checked with an exact
cross product, and the intervals on either side of a seam join only
where both edges cross the middle of the band at the same x, found along
the same line. Before, both checks allowed a few units in the last place
of the coordinates, which near the edge of the canvas (x = 3,999,000)
is wider than a real gap of one to four units, so a line ran across the
gap outside the frame.

An edge divided by an anchor exactly on it still joins, including where
the divided edge's own crossing would round to another x.
2026-11-13 13:42:05 -08:00
dotCooCoo 4bdfc44b26 Type: documents that define one composite font name differently each draw in their own
A document's composite fonts were lent by writing them into the font
database's shared map, and a second lend of another definition of the same
name was skipped while the first was held. A document drawn in a tiled or
floating window while the front document drew, a document placed in one
that defines the name differently, and an export made while another
document is in front could draw their type in the other document's
definition. Registering the front document's fonts during a render could
also change the definition mid-render.

Lending now applies on the lending thread only: each guard pushes the
document's definitions onto a per-thread stack that lookups read before the
database's own fonts, latest loan first, and removes them when it drops,
also when the thread unwinds. The guard is neither Send nor Sync. Lending no
longer changes the database or its generation; while a thread lends
definitions other than the database's, FontDb::generation gives a number of
its own for them, so kept layouts and render caches made in one document's
definitions are not used for another's.
2026-11-13 13:24:38 -08:00
dotCooCoo 395dd5b294 Text Wrap: a negative Offset lets text run into the wrap object
The help page says a wrap object's Offset may be positive or negative
(https://help.adobe.com/archive/en_US/illustrator/cs4/illustrator_cs4_help.pdf#page=307).
Text Wrap Options, the commands, the MCP tool and .ai import already
kept a negative value, but layout set it to 0.

- Layout keeps the signed offset, within -1000 to 1000 pt like the
  commands and .ai import (a value that is not a number counts as 0).
- Around a wrap object, a negative offset shrinks the object by that
  distance from each edge, holes included, so lines beside, above and
  below it run that far into it. A shape narrower than twice the
  distance no longer moves text aside. A line may get less room than
  the exact shrunk shape leaves, for example where several edges come
  close to one piece of the band, or where a band holds more than 64
  anchor heights. It never gets less room than an offset of 0 leaves.
- With Invert Wrap, a negative offset keeps the text that far inside
  the object.
- Zero and positive offsets up to 1000 pt lay out as before. A larger
  positive offset lays out as 1000 pt.
- The params text of object.textWrap.make and object.textWrap.options
  and the MCP text_wrap offset description give the range and what a
  negative value does.

Docs: docs/target-app-parity.md.
2026-11-13 13:19:43 -08:00
dotCooCoo 25857b2bce Type: right-to-left lines with tabs are measured as they are drawn
A right-to-left line draws the text before a tab after it, but its width
was measured in the order of its text. A right-aligned line such as
"אב גד<tab>הו זח" with a stop at 150 pt in a 300 pt frame ended 5.59 pt
past the frame's right edge, and right, center and decimal stops aligned
the text before the tab instead of the text drawn after it.

- The line's width is measured in drawing order, each tab up to its stop
  from where the glyphs drawn before it end. Right, Auto and centered
  lines are aligned by that width. Stops are still measured from the
  left edge.
- Right, center and decimal stops align the text drawn after the tab.
- Area type breaks a line with a tab where it fits as drawn, and wraps a
  line that fits only in the order of its text. Once the line holds a
  tab, it reads on while the least it can be drawn at fits, then ends at
  the latest break that fits as drawn: up to 256 breaks are measured
  from the latest back, and earlier breaks without a hyphen are
  bisected. Hyphenation points are measured as drawn the same way, so
  the line hyphenates at a point that fits.
- Only lines of horizontal paragraphs with right-to-left text keep a
  list of their break candidates.
- A tab that ends a wrapped right-to-left line is drawn before the line
  at its own width instead of pushing the line to the right.

Docs: docs/target-app-parity.md.
2026-11-13 12:40:40 -08:00
dotCooCoo ab1b7a974a Type: threaded type on a path aligned Justify All is measured by paragraphs
Type on a path whose first paragraph is aligned Justify All places its line
from the start bracket, the same as Left, so a threaded story is now measured
with the paragraphs it needs rather than the whole rest. A 1,500-paragraph
story through 40 paths aligned Justify All was laid out over 2,135,202 bytes
while measuring and is now laid out over 20,583. The frames take the same
text as before.

Center, Right, Auto right to left, hanging punctuation, Dynamic Text and
horizontal right-to-left text are still measured with the whole rest: a
centered or right-aligned line starts where the width of all its glyphs puts
it, and negative tracking can make an advance negative.

Tests compare every frame's share with the whole-rest share for Justify All
with spacing, on closed paths, with negative tracking and with punctuation
ending paragraphs.
2026-11-13 12:40:31 -08:00
dotCooCoo dd5a9e7cbc Text Wrap: Invert Wrap intersects a line's spans in one pass
With Invert Wrap, each piece of a line's band intersected the line's
spans so far with the spans the wrap shape holds over that piece by
comparing every pair of them. A wrap shape of many parts (a comb or a
zigzag) gives a line many spans, and with an offset the band is cut
into up to 128 pieces, so a dense shape made layout quadratic in the
number of parts for every line.

intersect_intervals now walks the two sorted lists once, stepping past
whichever interval ends first, and gives the same spans in the same
order. Tests compare it with the pairwise intersection on every list of
non-overlapping intervals with ends among seven points, check that two
interleaved lists of 3,000 and 2,000 intervals take at most 5,000
steps, and lay out a line inside a 500-tooth Invert Wrap comb with an
offset.
2026-11-13 12:12:22 -08:00
dotCooCoo fdedfb0e59 Merge main into feat/warichu 2026-11-13 11:59:22 -08:00
BFlat 34457735e5 Merge pull request #7231 from dotCooCoo/fix/threaded-type-measures-paths-and-aligned-frames
Type: threaded paths and frames aligned off the top measure the paragraphs they need
2026-11-14 02:39:29 +07:00
dotCooCoo b513c631f9 Type: threaded paths and frames aligned off the top measure the paragraphs they need
A threaded story measured each type-on-a-path frame, and each area frame
whose lines align to the center, the bottom or justify vertically, with
the whole rest of the story. A story of 1,500 one-line paragraphs through
40 such frames was laid out over about 2.1 million bytes while the frames
were measured.

- Area type without Shrink Text to Fit now takes the paragraph probe for
  every vertical alignment. A reflow pass that places less text is
  undone, so when the final layout stops before the last paragraph break
  measured, every pass placed the same lines as with the whole rest.
- Type on a path takes it when its line starts at the start bracket: no
  Dynamic Text, Optical Margin Alignment or Roman Hanging Punctuation,
  the first paragraph aligned Left, Justify Left or Auto, and vertical
  type or no right-to-left text in the rest of the story. The story is
  scanned for right-to-left text once per distribution.
- Shrink Text to Fit, the other path settings and a share that ends in
  the story's last paragraph are still measured with the whole rest.

The same 40 frames are now measured over 21,035 bytes (aligned to the
center) and 20,583 bytes (paths aligned Left). Each frame's share is
unchanged: the tests compare every slice with the whole-rest result for
both distribution entry points, across alignments, frame shapes, wraps,
path settings, right-to-left text, styles, lists and seeded stories.
2026-11-13 10:31:42 -08:00
dotCooCoo a55116c144 Type: Invert Wrap with an offset applies to a band of no height
A band whose top and bottom are equal lost its only piece when the repeated cut was removed, so the line kept the frame's whole width. Anchor cuts now apply only to a band with height.
2026-11-13 10:16:46 -08:00
dotCooCoo 1f13d1f8b7 Merge main into fix/area-type-band-coverage 2026-11-13 10:04:24 -08:00
dotCooCoo 6664a9bdb1 Type: a hairline gap between vertical edges of a frame still splits the line
Crossings of the middle of a line's band that lay within a few units of rounding of each other were joined as a seam, so two contours of an area type frame 1e-7 pt apart at x = 1e8 held one line across the gap between them. A vertical edge crosses the middle at its own x with no rounding, so a gap at one is no longer joined.
2026-11-13 09:55:08 -08:00
dotCooCoo ef331e8fad Type: Invert Wrap keeps a line where two parts of the shape bridge a gap together
With Invert Wrap and an offset, each piece of a line's band kept only the x that one part of the shape held within the offset of every height of the piece. Where the offsets of two parts met across a gap (one above and one below), the heights between them held neither part, and the line lost that x although the grown shape covered the whole band.

The band is now also cut at every height an offset above or below an anchor of the shape (when there are at most 64 of them), and the horizontal edges at the ends of a piece's window count, so the window of the piece between the meeting offsets reaches both parts.
2026-11-13 09:51:19 -08:00
BFlat ae33ab04cb Merge pull request #7181 from dotCooCoo/fix/area-type-inset-from-edge
Area type: Inset Spacing on a shaped frame is measured from the frame's edge
2026-11-14 00:48:54 +07:00
dotCooCoo 487da4b52a Type: area type lines stay inside a frame that pinches in within their band
A line in area type of a shape other than a rectangle was measured at five heights of its band, so an edge that pinched in between two of them (a notch tip at y = 47.5 in a 40..60 band) left the line running outside the frame. The frame's intervals over a band are now the intervals at its middle minus the x range of every edge within the band, which is exact for any number of vertices, holes and crossing contours.

Edges that contours meet along do not shorten a line: horizontal ones count by coverage, and collinear ones by overlap, and an edge that only touches the top or bottom of the band is left out. Rectangles keep their fast path.

Text wrap shapes were sampled at the same five heights, so a wrap object whose tip reached into a band between two of them let the line run across it. A line now keeps clear of every x at which a wrap shape, grown by its offset, lies anywhere in the band: the shape's intervals at the middle of the band plus every edge within it. With Invert Wrap, the band is cut into pieces a quarter of the offset tall (64 at most), and a line keeps to the x within the offset of the shape at the heights each piece's offset window holds, so it stays inside the grown shape over the whole band; an offset of 0 takes the shape's intervals over the whole band.

A frame less than 0.0002 pt tall no longer panics when a line is measured in it.
2026-11-13 09:15:01 -08:00
BFlat 4cad65c603 Merge pull request #7150 from dotCooCoo/fix/thread-frame-measure-needed-paragraphs
Type: a threaded story measures each frame with the paragraphs it needs
2026-11-13 23:15:01 +07:00
BFlat dad9116ef7 Merge pull request #7156 from dotCooCoo/fix/justify-after-last-tab
Justify: a line with a tab spreads the text after its last tab to the edge
2026-11-13 23:14:52 +07:00
BFlat 39940f5568 Merge pull request #7133 from dotCooCoo/fix/font-menu-scrollbar-test-flaky
Tests: the font menu scroll bar test lists fonts of its own
2026-11-13 23:14:43 +07:00
BFlat 2d91c860a7 Merge pull request #7087 from dotCooCoo/feat/every-line-composer-shaped-area
Type: the Every-line Composer sets mixed sizes in shaped and wrapped area type
2026-11-13 23:03:29 +07:00
dotCooCoo 5f45e39108 Area type: Inset Spacing on a shaped frame is measured from the frame's edge
On a shaped frame, Inset Spacing keeps text that far from the frame's
edge, measured from the edge itself rather than along the line. Before,
the inset was trimmed from each end of a line after the frame's edge was
found, so on a sloped edge the text came closer than the inset: a
triangle (0,0), (100,0), (0,100) with a 10 pt inset gave the band
40..60 the span 10..30, where a 10 pt clearance gives 10..25.86. Holes
keep the same clearance.

A rectangular frame keeps its inset from each side as before, a
column's inner edges keep it once, and the gap kept beside a wrap
object along the line is unchanged.
2026-11-13 07:58:15 -08:00
dotCooCoo 7f18643ada Type: measuring a threaded frame copies its own text once
Each measurement of a frame's share started from a copy of the whole frame, its own runs included, and then replaced the runs. The frame is now copied once without its own text, and each measurement copies that. A test counts the frame text copied to measure frames that already hold text.
2026-11-13 07:12:59 -08:00
dotCooCoo df975e6300 Justify: a line with a tab spreads the text after its last tab to the edge
A justified area type line counted a tab as a word space, so it gave
the tab a share of its room that no glyph drew, and the line ended
short of its measure. A word-tab-word line ended short too.

- The text up to a line's last tab keeps its spacing, each tab keeps
  its stop and its leader stays in its gap. The text after the last
  tab takes the line's room: word spacing, letter spacing and glyph
  scaling within their ranges, then its word spaces, or the gaps
  between its letters when it has none. Kashidas go in that text when
  it is one run.
- A tab separates two words, so Single Word Justification does not
  align a word-tab-word line.
- Right-to-left lines take the text drawn after the last tab drawn,
  measured as the line is drawn, and vertical lines work down the
  column. Point type keeps its natural width.

Docs: docs/target-app-parity.md, docs/type-parity.md.
2026-11-13 06:41:22 -08:00
dotCooCoo 6d0556e60f Type: a threaded story measures each frame with the paragraphs it needs
Distributing a story through threaded frames measured every frame but the
last with the whole rest of the story, and copied that rest several times
per frame, so a long story through many frames cost frames times story
length.

- The rest of the story is a borrowed view of the given runs (run index,
  byte offset, length). Each frame's share is copied once; the slices,
  styles of empty shares and the run left once the story is used up are
  the same as before.
- Area type without Shrink Text to Fit whose lines align to the top is
  first measured with the text through the end of the paragraph that
  reaches about twice the previous frame's share (at least 512 bytes),
  doubling while the story goes on. The share is taken when its text
  stops fitting before the last paragraph break measured; layout places
  lines in order, stops at the first that does not fit and shapes each
  paragraph on its own text. Type on a path, Shrink Text to Fit, other
  vertical alignments and a share ending in the story's last paragraph
  are measured with the whole rest.

Tests compare every slice with the earlier algorithm, kept in the tests,
for many kinds of frames, styles and paragraphs and 12 seeded stories.
A story of 1,500 paragraphs (55,892 bytes) through 40 frames was laid
out over 2,126,762 bytes while measuring and is now laid out over
21,035.
2026-11-13 06:31:37 -08:00
dotCooCoo eb2f3ba71a Tests: the font menu scroll bar test lists fonts of its own
font_menu::tests::the_scroll_bar_leaves_the_stars_clear starred every
family in the process-wide font database and listed that database, which
tests running alongside add fonts and composite fonts to. In full
parallel runs it sometimes failed with "8 families, no row stars drawn".
With the four bundled families alone it never reached the scroll bar
check, which ran only for seven or more families.

- The menu's header takes the font database it lists and hands it to
  the rows and the name-width measurement. The app passes the
  process-wide database as before.
- Each FontDb has an id of its own (FontDb::id), and the widest family
  name the menu keeps is stored with that id as well as the generation,
  so a width measured for one database is not used for another at the
  same generation.
- The test lists a database of the bundled fonts, stars its four
  families, opens on Inter, and runs the frames the scroll bar takes to
  appear. It checks that the rows overflow the list's scroll area, that
  a row star is drawn inside that area, and that each star stays left
  of the scroll bar's column (#2277).

With the list's scroll bar set to floating, the test fails with a star
under the bar. Without the id in the widest name's key,
each_font_database_has_its_own_widest_name fails.
2026-11-13 05:41:46 -08:00
dotCooCoo 130f914868 Type: a threaded frame's empty last paragraph keeps a format given to it
A paragraph format given to the empty paragraph after a frame's last
paragraph break was lost when the story flowed again: the paragraph is
shared with the next frame's first paragraph, and the next frame's old
style replaced it, in that frame and in the frames after it.

- The thread reflow also notes which frames' last paragraph changed
  style, and story_paras_restyled keeps that style against the next
  frame's unedited first paragraph, across unedited empty frames too.
  When the same edit formats the next frame's first paragraph, that
  format applies, as before.
- story_paras keeps its behavior for its other callers.
- Tests cover area type and type on a path on either side, a next frame
  holding text, three frames, undo and redo, a format that changes
  nothing, and numbered list counts.
2026-11-13 05:27:02 -08:00
dotCooCoo ba2a6fc1f3 Type: a further span beside a wrap object is measured across its line's height
Text wrap can split a line into spans that share a baseline. The spans
after the first were measured across the height of the line set in the
first one, so a taller line set in a later span could run into a wrap
object the shorter line stayed clear of. Each further span is now
measured again across the height of the line that goes in it. The line
gets the part that is free, and a span with no free part, or with a part
narrowed to less than the line's first word, is left empty. The
Every-line Composer and the line loop both place lines through
`Pen::place`, so they agree.

The Every-line Composer also keeps a line start whose line found no room
while a longer line from it would be taller. A taller line steps down
the frame by its own leading and can find room the shorter line's steps
missed.

Tests: a_further_span_is_measured_across_the_height_of_its_line,
composition_tries_a_longer_line_after_one_that_cannot_be_set.
2026-11-13 04:48:43 -08:00
dotCooCoo 6c773585fa Type: the Every-line Composer sets mixed sizes in shaped and wrapped area type
A paragraph whose characters differ in line metrics (type size, font,
leading, vertical scale, or an inline graphic taller than the text) was
broken with the Single-line Composer in area type of a shape other than
a rectangle, or with text wrap. The every-line composer now composes it
there too.

- The composer takes each line's width from a measure that follows
  where the lines above it went (`composer::Measure`,
  `compose_with`). The fixed widths by line number are one such measure.
- In a frame whose lines are not all as wide, each line the composer
  tries is placed from the pen the lines above it left, at its own
  tallest metrics, as the line loop places it (`layout/shaped.rs`).
  Starts with the same placement are shared, and each start keeps the
  placements it was tried with.
- A line too long for its room does not end the search from its start
  while a glyph after it could make the line taller and set it lower,
  where the frame may be wider.
- The line loop places a composed line in such a frame at the line's
  tallest metrics, so it gets the room the composer measured for it.
- The search for one paragraph stops after 1,000,000 lines tried or
  2,000,000 baseline steps. The searches of all the paragraphs of one
  frame share a budget of 2,000,000 lines and 4,000,000 steps, kept
  apart from the line loop's own steps. A paragraph whose search stops
  is broken line by line. A paragraph the frame has no room for is
  broken line by line too, and overflows as before.

Tests: crates/text/src/tests/combos.rs (a stepped frame, a frame that
widens, text wrap with two spans on a baseline, overflow, a long
paragraph) and crates/text/src/layout/wrapping_tests.rs (the composer
with a measure, and the shared search budget).
2026-11-13 01:24:33 -08:00
dotCooCoo f278364816 Type: a selection in a warichu block highlights its own block line
A selection inside a warichu block was highlighted across the whole
line of text, so selecting a character of the upper block line
highlighted the lower one too, and a selection from one block line to
the next spanned the wrong characters. Each block line now gets its own
band, out to halfway to the block lines next to it, with one highlight
over each run of selected characters drawn side by side on it. A
selection that runs past the line still covers the line break.

Glyphs now carry their block line, so the characters of one block line
stay together for the highlight and for a click whatever their own
Character Alignment.
2026-11-12 23:47:30 -08:00
dotCooCoo 06a283232e Type: no justification gap inside a right-to-left warichu block
Justification gaps follow the drawn order, and right to left the bidi
order draws a block's first glyph after the others. The glyph drawn
second then did not continue the block, so Full Justify and CJK
justification opened a gap inside it. Both gap rules now also skip a
pair whose glyphs belong to the same block.
2026-11-12 23:03:39 -08:00
dotCooCoo 199a9dbce3 Merge main into feat/warichu 2026-11-12 22:28:38 -08:00
BFlat 499903878e Merge pull request #7055 from dotCooCoo/feat/mojikumi-presets
Type: the Mojikumi Set menu offers the predefined spacing sets
2026-11-13 13:17:36 +07:00
dotCooCoo 36ab39a7f4 Type: Warichu blocks keep their places, clicks and line height in mixed text
- Insert Kashidas skips the characters of a warichu block.
- Blocks whose characters the bidi order draws in turn keep where each
  block starts.
- A click on a block line goes to that line's characters.
- Digits marked Warichu in vertical type on a path stay across the column.
- A block of several fonts gives its line the tallest ascent and the
  deepest descent among them.
- Warichu Settings on characters not marked Warichu do not split shaping.
- Warichu Settings leaves a value outside its range as it is unless it is
  edited.
2026-11-12 22:12:06 -08:00
dotCooCoo 226ac437b9 Type: Warichu stacks selected characters on lines inside their line
The Character panel menu has Warichu and Warichu Settings… with Show East
Asian Options. Warichu characters are set at a percentage of their size
and stacked on 1 to 6 lines inside their line: one above the other in
horizontal type and side by side in vertical type. Warichu Settings sets
Lines, Line Gap, Scale and Alignment.

- text.setFormat and text.setRangeStyle take warichu, warichuLines,
  warichuGap, warichuScale and warichuAlign; ui.warichuSettings opens
  the dialog.
- A stretch of warichu characters is one block that stays whole on one
  line. The canvas and every export draw the stacked lines; SVG, PDF and
  the .ai writers write such type as outlines, as they do ruby. Type on a
  path sets the characters at their own size.
- The attributes are saved natively, carried by character styles and
  translated in every complete catalog.

Docs: docs/mcp.md, docs/type-parity.md.
2026-11-12 21:13:21 -08:00
dotCooCoo 7b6b6af7b1 Type: the Mojikumi Set menu offers the predefined spacing sets
The Paragraph panel's Mojikumi Set lists None, YakumonoHankaku,
GyomatsuYakumonoHankaku, GyomatsuYakumonoZenkaku and YakumonoZenkaku,
then Line-end Punctuation Half Width, and text.setFormat {mojikumi}
takes yakumonoHankaku, gyomatsuYakumonoHankaku, gyomatsuYakumonoZenkaku
and yakumonoZenkaku besides none and lineEndHalf.

The spacing of each set is VectorCraft's own rule, built on the help
page's one-line description of each set:
- YakumonoHankaku sets every bracket, comma and full stop half width,
  takes a quarter em off each side of a middle dot, and puts a quarter
  em between Japanese and Latin letters or digits.
- GyomatsuYakumonoHankaku keeps punctuation full width inside a line,
  sets an opening bracket starting a line flush and a closing mark
  ending it half width, and takes the quarter em after a middle dot
  ending a line off.
- GyomatsuYakumonoZenkaku keeps punctuation full width and adds the
  quarter em between Japanese and Latin text.
- YakumonoZenkaku sets characters as None does.

Both composers and vertical type measure each line as it is set. A
right-to-left paragraph, whose line ends are measured untrimmed, also
draws them untrimmed, and a right-to-left line that ends between a
Japanese and a Latin character is measured without the quarter em the
set adds there. With a Desired glyph scaling other than 100%, the
space a set takes off before an opening bracket or middle dot, and the
Japanese-Latin space recorded at a line's end, scale with the glyph.

The sets save with the document and paragraph styles; new type and
Reset Panel keep Line-end Punctuation Half Width. SVG export lays type
with middle dots out again for its positioned spans. A .ai save
reports a paragraph with one of the new sets as left out.

Docs: docs/mcp.md, docs/target-app-parity.md, docs/type-parity.md,
docs/gaps.md.
2026-11-12 21:07:44 -08:00
jmverges f94eecd439 Merge remote-tracking branch 'upstream/main' into fix/ai-import-font-match
# Conflicts:
#	docs/file-format-parity.md
2026-11-13 01:55:35 +00:00
BFlat d0a8fc708f Merge pull request #7001 from dotCooCoo/fix/wasm-clippy-runtime-default
Updates: Runtime derives Default on wasm32, so wasm32 clippy passes
2026-11-13 08:29:29 +07:00
jmvergesandClaude Opus 5.5 bb50d438c8 .ai import: type from the text document takes the face of its exact PostScript name and recorded version (#6983)
Type read from a text document's font record was set in whichever installed
face first answered to the record's PostScript name (ignoring case and
punctuation), and its family was then swapped for any available family
spelled alike but for case and punctuation (`FontDb::find_family`): a record
naming `SampleInk`, the PostScript name of `Sample/Ink`, set its type in
`SAMPLE INK`. Copies of one family and style under other PostScript names or
versions were not told apart either, so the type could set at other widths
than the file's own page.

- `named_font` / `document_font` (crates/eps/src/import/text.rs) give the
  family and style of the installed face of exactly that PostScript name,
  else one that differs only in case or punctuation, and name its version
  when the family and style alone resolve to another installed copy.
- The font record's key 5, its version string (name ID 5), is read and picks
  that version among the faces of the name. Each record is resolved once.
- `FontDb::postscript_faces` lists every installed face a PostScript name
  answers to, loading their families; the import prefers among them one of
  exactly that name.
- Type a PostScript program sets (EPS import) and legacy .ai type take the
  face of their font's name the same way.
- The .ai save writes a static font's version (`/5`) when its PostScript name
  alone would give the reader another installed copy (another version, or the
  same name in another family), keeps the version in the style it compares,
  and checks that the type comes back in the same family, style and face,
  taking the record's version as the reader does (up to 256 bytes), so such
  type round-trips without a note.

Measured on 224 users' files (used locally, never committed): 16 files set
some type in another face; 12 render differently against their own page,
10 closer and 2 further by 0.03% of the page; envelopes of type that open
live go from 154 to 159, none fewer in any file; no file opens another way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 00:33:13 +00:00
dotCooCoo 7764da6ad8 Updates: Runtime derives Default on wasm32, so wasm32 clippy passes
`cargo clippy --target wasm32-unknown-unknown -p vectorcraft-ui-egui
-- -D warnings` stopped twice. In vectorcraft-text, MAX_FILE_SEARCHES
is used only by the system font search, which is not compiled for
wasm32, so it was dead code there. In vectorcraft-ui-egui, Runtime
holds only manual_window_open and status_message on wasm32, and the
hand-written Default set them to false and None, so clippy's
derivable_impls stopped the run.

- MAX_FILE_SEARCHES is compiled only for targets other than wasm32,
  like the SysFallback struct next to it.
- Runtime derives Default on wasm32, and the hand-written Default,
  which creates the updater, is compiled only for other targets.
2026-11-12 16:27:30 -08:00
dotCooCoo 705b688f2b Type: Insert Aki keeps a widened glyph's own width and an optical pair's letter
A glyph that justification widens to fill the line keeps its Insert Aki
before it at its width, and its own width, which ligature carets stand
in, now starts where the glyph is drawn. Optical kerning measures a pair
from the letter of a cluster rather than from a combining mark whose
advance holds the cluster's Aki.

The SVG test also checks centered and right-aligned lines with Insert Aki.
2026-11-12 15:48:12 -08:00
dotCooCoo b9946efb1e Merge main into feat/cjk-insert-aki 2026-11-12 12:46:09 -08:00
dotCooCoo 9d8df6f27f Type: Bunri-Kinshi finds a pair by its clusters in right-to-left justification
Single Word Justification adds no space inside an inseparable pair. A
right-to-left line draws the second half's cluster before the first half,
with the cluster's glyphs in logical order, so a dash followed by a
combining mark puts the mark next to the first dash. The pair is now found
from the two neighboring clusters instead of neighboring glyph indices.
2026-11-12 11:05:35 -08:00