Spine::at_length computed tangents from flattened polyline chords, which
are piecewise constant and jump discretely between chords on curved
spines. Steps oriented to the spine with Align to Path stuttered at
large step counts.
Fix: store the original PathData in Spine and compute the tangent from
the original cubic at the true arc-length position via ParamCurveDeriv,
proportionally mapping flattened arc-length s onto each segment's curve
arclength using anchor_length values, instead of using piecewise-constant
chord directions.
Updated segment-selection condition in Spine::at_length to skip segments
whose flattened end is not greater than start before checking whether s is
within the segment. Preserve the existing final-segment fallback so
at_length(0.0) selects a nonzero segment and uses its tangent.
Added zero-derivative fallback tangent for blend-step sampling to keep
Align to Path from passing a zero tangent to atan2.
Added missing docstrings for Spine::new and Spine::length to satisfy
docstring coverage requirements.
Co-authored-by: Craft Bot <bot@storytold.dev>
Spine::at_length computed tangents from flattened polyline chords, which
are piecewise constant and jump discretely between chords on curved
spines. Steps oriented to the spine with Align to Path stuttered at
large step counts.
Fix: store the original PathData in Spine and compute the tangent from
the original cubic at the true arc-length position via ParamCurveDeriv,
proportionally mapping flattened arc-length s onto each segment's curve
arclength using anchor_length values, instead of using piecewise-constant
chord directions.
Updated segment-selection condition in Spine::at_length to skip segments
whose flattened end is not greater than start before checking whether s is
within the segment. Preserve the existing final-segment fallback so
at_length(0.0) selects a nonzero segment and uses its tangent.
Added missing docstrings for Spine::new and Spine::length to satisfy
docstring coverage requirements.
Co-authored-by: Craft Bot <bot@storytold.dev>
Spine::at_length computed tangents from flattened polyline chords, which
are piecewise constant and jump discretely between chords on curved
spines. Steps oriented to the spine with Align to Path stuttered at
large step counts.
Fix: store the original PathData in Spine and compute the tangent from
the original cubic at the true arc-length position via ParamCurveDeriv,
proportionally mapping flattened arc-length s onto each segment's curve
arclength using anchor_length values, instead of using piecewise-constant
chord directions.
Added missing docstrings for Spine::new and Spine::length to satisfy
docstring coverage requirements.
Co-authored-by: Craft Bot <bot@storytold.dev>
Spine::at_length computed tangents from flattened polyline chords, which
are piecewise constant and jump discretely between chords on curved
spines. Steps oriented to the spine with Align to Path stuttered at
large step counts.
Fix: store the original PathData in Spine and compute the tangent from
the original cubic at the true arc-length position via ParamCurveDeriv,
proportionally mapping flattened arc-length s onto each segment's curve
arclength using anchor_length values, instead of using piecewise-constant
chord directions.
Reviewed-by: craft-house-rules
Co-authored-by: Craft Bot <bot@storytold.dev>
Spine::at_length computed tangents from flattened polyline chords, which
are piecewise constant and jump discretely between chords on curved
spines. Steps oriented to the spine with Align to Path stuttered at
large step counts.
Store the original PathData in Spine and compute the tangent from the
original cubic at the true arc-length position via ParamCurveDeriv,
instead of flattened chord directions.
Adds a regression test walking 200 samples along a curved spine and
asserting each tangent step stays under 0.1 radians.
A path that keeps its shape because the effect would pass the geometry
budget holds a group's own placed gradients in place. A hidden path in a
layer, which the layer's bounds leave out, no longer holds them, so the
layer's gradient moves with its visible art. The art's own gradients
follow the same rule.
A freeform gradient whose points were edited keeps its vector fitted to
the bounds, so it did not count as placed and its points stayed behind
under a Transform effect and Free Distort unless another placed gradient
was in the same appearance. Its points now move like a placed gradient.
Align to Path moved the whole line by the largest ascent and descent
among its characters. Each character now moves by its own font, size
and vertical scale. Ascender runs the top of the font's em box along
the path: the ideographic em box that the font's BASE table gives, or
in a Chinese, Japanese or Korean font the one that its typographic or
hhea box, a one-em box-drawing cross or its ideographs give; else an
em box centered on the cap height. Descender runs the bottom of the font's
bounding box along the path, and Center runs a line at half the height
of the top of the font's H along it. In vertical type, Ascender and
Descender move the characters half an em, and Center leaves them on
the path.
Existing documents with Align to Path set to Ascender, Descender or
Center lay their type out at a new distance from the path.
Found while testing crates/doc/src/assets.rs:43 (Document::next_asset_name)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A top object's corner test treats a straight segment as having its
handles a third of the way along it, so an anchor between two straight
segments is a corner only when it turns out of the outline. docs/mcp.md
and the doc comment now say so, and a new test covers an inward anchor
between straight segments. The bulging-curve corner test checks against
a fixed point instead of the same nearest-point routine the code uses.
Saved corners from an .ai file stay on the same anchors when the
envelope is reflected, as they already did when it was moved or
rotated. A reflected outline runs counter-clockwise; the corners were
then mapped as if the outline had been drawn the other way round, no
longer fit it, and were found again from the outline.
On a tie between two corner anchors, or two nearest outline points,
the one met first along the outline in the order it was drawn wins,
for outlines drawn counter-clockwise too.
The docs state the corner rule in full: a handle 0.01 pt long or
shorter makes an anchor a corner whichever way it turns; otherwise its
tangent unit vectors must differ by 0.01 or more in x or y and turn
out of the outline. Content spreads evenly along a segment without
handles.
A top-object envelope splits its outline into four sides at four corners
and maps the content onto the Coons patch of those sides. The sides
shared out the content by length, and the corners were the anchors or
curve points nearest the bounding box corners.
Each piece of a side between anchors and corners now takes a share of the
content's width or height in proportion to the length of its control
polygon, spread along the piece by its curve parameter. A straight
segment spreads it evenly by length. A rounded rectangle stays close to
the identity, and the content follows the curves of a side whose segments
differ in size.
Each corner is now the nearest anchor to its bounding box corner among
the sharp anchors that point out of the outline and lie within 47.5% of
the box's width and height from that corner. A handle shorter than
0.01 pt, or tangent unit vectors that differ by 0.01 or more in x or y,
makes an anchor sharp, and the first one along the outline wins a tie.
Without such an anchor, the corner is the outline's nearest point to the
box corner, moved onto the end anchor of its segment within 0.5% of the
segment's parameter. Envelopes opened from .ai files keep the corners the
file saves.
These are VectorCraft's own rules. Existing top-object envelopes redraw by
them. Gradient meshes made from a path keep their sides measured by length.
- crates/doc/src/live/mesh.rs: Coons::from_top_object and the corner
search, used for top-object envelopes.
- EnvelopeKind::TopObject gains an optional `corners` field (anchor
indices); the .ai importer fills it from the mesh's corners.
- Params text of object.envelope.makeWithTopObject, docs/mcp.md
(Envelopes) and a progress line in docs/target-app-parity.md.
Typed list detection gave every bulleted trigger the filled dot. Now
`*` and a space start a list with the filled dot (•), `+` with the
hollow circle (◦), `#` with the filled square (▪) and `>` with the dash
(–), each one of the existing Bullets presets. `1.`, `1)`, `A.` and `A)`
start the same numbered lists as before.
Only the list a typed trigger starts changes. Lists already in a
document keep their bullets, pasted triggers start no list, and Undo
and Redo of a typed list work as before.
Docs: docs/mcp.md (typed list paragraph), docs/target-app-parity.md.
The legacy reader skipped `bindrepeat Tg` (section 15.3.4 of the version 7
file format specification). It now reads the binding punctuation bit:
1 and 3 set the paragraph's Burasagari to Force, and 0 and 2 set it to
none, which is VectorCraft's own mapping. The value is modal, lasts into
the next text object, and an operand other than 0 to 3 leaves it as it
was. The repeat character, `TG`, `Xu` and `Tu` are still skipped.
docs/mcp.md and the `ParaStyle::burasagari` doc comment name the legacy
`.ai` case as the exception to imported text keeping `none`.
Add Used Colors, New Color Group from artwork and Select All Unused
looked up each linked paint's swatch by searching every swatch. They now
build one map of swatch names first, with the built-in Registration
swatch, and the first swatch of each name as before.
New tests and test cases: New Color Group from artwork with repeated,
near-equal and -0.0 colors, a Registration link and names that need
numbering, and New Swatch and Edit Color Group with near-equal and
-0.0 colors; each checks the names, their order and one undo step.
New Swatch with several colors, New Color Group and Edit Color Group's
extra colors searched every swatch and group for each candidate name
of each new swatch. They now build the set of taken names once
(FreeSwatchNames, with take_new for New Swatch's default names and
insert for the group name and the group's own swatches) and keep it up
to date as they add swatches. The names they give are unchanged.
swatch.addUsedColors and swatch.unused searched lists inside loops over
colors and swatches, checking each color against every color or swatch
before it. They now keep the same ordered lists and use hash sets and
maps for membership and lookup. A color's key is its model and the bits
of its components with -0.0 read as 0.0, so the key agrees with `==`; a
color with a NaN component matches nothing, as before.
Add Used Colors names its new swatches through
Document::free_swatch_names, which collects the taken swatch and color
group names once and gives the same names free_swatch_name gives when the
swatches are added one at a time. The added swatches, their names, order
and links, the undo step, and the swatches Select All Unused lists are
unchanged.
An envelope mapped type from its laid-out box: ascent to descent for
point type, the frame for area type. The distorted glyphs then filled
less of the envelope than other art does, with room above the capitals
and below the baseline.
The envelope now maps type from the bounds of its glyph outlines,
underlines and strikethroughs when the type is on its own, in groups
and layers, or used as a clipping path. Inline graphics count by their
boxes. Rotated type and type in perspective map from their outlines as
they are drawn. Make with Mesh,
Make with Top Object and Reset with Mesh without Maintain Shape use
those bounds. Type that draws nothing keeps its laid-out box, and so
does type inside any other object, such as a blend, a repeat or a
symbol instance. Shrink Text to Fit type is measured as it is laid out
on the page.
vectorcraft-text's envelope_ink measures the type, and
register_envelope_ink registers it with the document crate. The engine
session, Renderer::new, live evaluation in vectorcraft-effects and the
.ai reader register it. The document crate keeps the measured boxes per
thread, by object and by a hash of the content, and measures again when
the type, the fonts, the hyphenation exceptions or the transform's
scale or rotation change.
This is VectorCraft's own rule. Envelopes of type made in VectorCraft
and in saved files map from the glyph outline bounds instead of the
type's laid-out box, so they redraw differently.
Smooth Color now counts the steps between each pair of keys from the
keys' colors and visible bounds, which changes the step count of most
Smooth Color blends. The counts are VectorCraft's own values:
- When the keys' solid fills or strokes differ (in groups, member by
member; in text, run by run), the count is 255 times the largest
change of one channel as a fraction of its range, a half step
rounding down, from 1 to 254. An RGB channel's range is 256 values,
CMYK and gray count in percent, and two tints of one swatch by their
tints. It is at most eight per point of the largest of the keys'
widths and heights and the distance between their centers. Black to
white was 128 steps and is now 254.
- When the colors match, a solid paint meets no paint, or a key holds
a gradient or a pattern, the count is the largest distance between
corresponding visible bounding-box edges divided by the keys' mean
width or height, rounded, less one, from 1 to 1000. Two 20 pt
squares 200 pt apart were 100 steps and are now 9.
Keys without bounds make the color count when their colors differ,
and 1 step otherwise.
A blend nested in another blend's keys counts toward the step limit
by its color steps when its colors differ, and as up to 1000 steps
when they match (256 before). An outer blend of 200 steps between two
such same-color Smooth Color blends now draws 99 steps.
A Smooth Color blend in an .ai file whose drawn steps follow this rule
now opens as a live blend.
Blend Options: https://web.archive.org/web/20241217212108/https://helpx.adobe.com/illustrator/using/blending-objects.html
Docs: docs/mcp.md, docs/target-app-parity.md.
A save wrote the path relative to the document only for linked images and
placed documents in the layers. Links inside symbols, graph designs,
patterns and opacity masks kept only their absolute path, so after the
document and its files moved to another folder together, those files were
not found.
- The save sets the relative path of every link the document holds.
- On open, a link inside that art whose file was found away from its path
takes the new path, as a link in the layers does, so the next save
writes the right relative path.
- Document::update_links_where changes only the links it picks and copies
only the nodes on the way to them.
Docs: docs/mcp.md (Linked images), docs/target-app-parity.md.
The Shape Builder, Live Paint and run_around tests in planar/tests.rs
measured elapsed time against a 5 s limit, and the large edit tests
(large_edits.rs, with the Lock and Hide All Artwork Above and perspective
Alt-drag tests that share `soon`) against 20 s. On a loaded machine they
failed without any change in the code.
- pathops: a test-only counter adds up the items `RectIndex::at` reads
and the segments `nearest_u` searches. The four planar tests assert a
bound on that count.
- doc: `tree_walks` counts, per thread, the walks of a document's tree
that lookups by id make. `soon` fails when a command walks the tree
more than 64 times, and keeps a 300 s wait for a hang. The tests that
named a 20 s limit are renamed.
Looking objects up one at a time in Delete or Offset Path, or scanning
the whole index or ring in the planar code, makes these tests fail.
A paste relinked every copy to the document's resources even when no
resource had another name or color and the objects linked no graphic
style. It now leaves those copies as they are.
Finding the pasted type looked each id up and compared every text object
with the ones already found. Text commands and the bounds refresh after
a paste now look all ids up in one walk and keep the found ids in a set.
Giving a pasted group new ids builds its map of old to new ids only for
an Objects on Path group, which is the only kind that reads it.
A pasted solid paint with no swatch link and a tint other than 1 now
keeps that tint when no relinking runs. Saved art carries a tint of 1 on
such a paint.
The width between two width points was a straight line, so a variable-width
stroke turned a corner at every width point.
- Each side of a width profile now follows a monotone cubic (Fritsch-Carlson)
through its points: no kink at a point, flat at a point wider or narrower
than both neighbours, never past either point of a span. Points on one line
stay on it and two-point tapers are unchanged; stacked points still step.
- widest() counts each span's wider left and wider right, since the two sides
needn't peak together.
- The outliner samples straight runs between width points closely enough to
follow the curve to the drawing tolerance (at most 64 samples per span and
4,096 per piece); straight spans get none.
- Tests that expected straight-line widths now expect the curve. The stroke
gradient coverage test uses Teardrop instead of Wave until #5964 is fixed.
When Distort Appearance is on and the size a `.ai` saves for an
envelope's contents (`AIDeformArtWidthKey`, `AIDeformArtHeightKey`)
matches the contents' bounds with strokes, the reader now maps the
contents from that stroked box. It used to map them from the geometric
bounds, half a stroke inside that box, so an envelope of stroked art
could differ from its drawn art and keep the drawn art with a note.
Envelope Options hold the box's margins as `boundsMargin`, shares of the
geometric bounds at the top, bottom, left and right. Native files write
them only when they are not zero, and `object.envelope.info` reports
them then. Leaving Edit Contents measures them again when the strokes
reach differently than when Edit Contents began, and the node holds
what they measured then until it leaves. New envelopes have no
margins, so their results do not change. Contents with no width or no
height, such as one straight line, get no margins.
Docs: docs/mcp.md, docs/target-app-parity.md.
The walk measured every object's visual box to leave out what is off
the page. A plain group or layer is measured as the union of its
members, and each member then measured itself again when it was
written, so an object in plain groups was measured once for each level
it sits at.
Measuring a plain group or layer now keeps its members' boxes in a
tree, and each member uses its box when it is written. A container
written whatever its box is (a PDF layer's group, placed symbol art) is
not measured. A knockout group's elements are measured for the group's
box and again for the list of elements on the page, and that second box
is used for the element and for its mask. Objects in a live container
(Blend, Envelope, Repeat, Intertwine) are still measured with the
container and again after it is expanded. Node::visual_of_members holds
the formula that Node::visual also uses, so the boxes and every decision
are unchanged.
The output is byte for byte the same on one open path of 200,000
anchors, 20,000 rectangles in 200 groups of 100, and 5,000 point type
objects, and on a document of plain, see-through, clipping and knockout
groups on and off the page, each exported with default settings, with
uncompressed text and with PDF layers.
The editing copy of a .ai or .ait file that VectorCraft saves or exports
now has `%_<n> /Int (CropAreaActive) ,` after the ArtboardArray in its
recorded part, and marks that artboard as the selected one. The app
records its view's active artboard before each command
(DocState::active_artboard; an index past the last artboard is the last,
as in the Artboards panel); without a view, the artboard the document
opened on (Document::opened_artboard) is written, else the first. A .ai
file read from its editing copy opens on that artboard. Before, the copy
named no active artboard and always marked the first as selected.
Docs: docs/mcp.md, docs/file-format-parity.md, the editingData option text.
Bulgarian was not among the text languages, so Bulgarian text could not
be tagged and fonts drew their Russian-style Cyrillic. Bulgarian (bg)
is now in the language list (Character panel, character styles,
text.setFormat by name or tag, the Hyphenation preference's default
language), with CLDR's quotes, „…“ at either level. Shaped as bg, the
fonts' locl forms for BGR apply: in the bundled Source Sans 3 Italic,
в, д, ж, з, к, л and ю take their Bulgarian forms.
Fixes#5945
A linked 1-bit image mask from a .ai file keeps its fill color as a
colorize paint, so the image takes the color once its file is relinked.
Until then the image shows the missing-link placeholder, and the canvas
and PDF export colorized that placeholder too: its light tile turned
nearly transparent and its red cross took the fill color.
ImageBlob::takes_colorize is false for a preview whose pixels are not
grayscale. While it is false, the canvas draws the image and PDF export
writes it as it is, printed separations separate it as an uncolorized
image, and transparency flattening (EPS, PDF 1.3 and PDF/X-1a export,
Flatten Transparency) does not count its paint as transparency. The
paint stays on the image, and a grayscale preview is still colorized.
SVG and EPS export already write only grayscale images colorized.
VectorCraft now reads `CropAreaActive`, an `/Int` in the dictionary of
the editing data that holds the `ArtboardArray`, as the index of the
active artboard in that array. The reader ignored it before, and every
file opened on its first artboard.
The opened document starts with that artboard active, so the view the
document opens at and the Artboards panel use it. The index counts
every entry of the array, so an artboard that is left out (one with no
area) does not shift it. When the entry is missing, is not an `/Int`,
or names an artboard that does not open, the first artboard is active,
as before. A file that comes in as its PDF part, with its pages placed
where its artboards are, opens on the page of the active artboard, also
when the reading stops before the artboards. The same reader serves the
editing data of EPS and PDF files.
The active artboard is kept on the opened document only; native files
do not save it.
A linked image read from a version 7 `.ai` file can keep its path in
the classic Mac OS form, with colons between folders
(`Macintosh HD:Images:photo.tif`). The link's name was the whole path,
so the Links panel showed it as one long name, and Relink to Folder,
the search of the document's folder and Find Linked Files looked for a
file with that whole string as its name.
A path with a colon and no `/` or `\` is now read as a classic Mac OS
path. Its name is the part after the last colon, and `links.info` gives
the rest as `location`. Relink to Folder, the document-folder search and
`links.findFiles` look for that name, and Find Linked Files compares the
path's folders with the folders of the files it finds. The link keeps
the path as stored. Paths with a `/` or `\` keep those as separators,
so a file name may still hold a colon (`/Users/a/x:y.png`).
Docs: docs/mcp.md (Links), docs/file-format-parity.md (version 7
linked images).