A .ai file that File > Export As, its dialog or a control request writes
to a path now names the view's active artboard, as document.export and
document.serialize do. The UI export records the view's artboard and
takes its document from fileio::export_doc, which applies
DocState::active_artboard to the export's copy; document.export and
document.serialize use the same function.
The app now records the view's active artboard before every command,
including the commands the UI handles itself, so a .ai copy from Save a
Timestamped Copy or Export All Open Documents names it too. Before,
these files named the artboard the document opened on, or the first.
The open document is unchanged.
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
Tab-separated rows with the same number of cells (quoted cells too)
become a group of point type, one per cell, in columns and rows over
0.5 pt gridlines; other text pastes as before. Fixes#5296.
The Eyedropper, the Live Paint check for paint to sample, the Appearance
panel's Characters chips and the colors the Libraries panel adds from
the selection found the paint behind the Fill or Stroke proxy through
Node::proxy_paint, which measures the object's geometric bounds. They
now read it with Node::proxy_paint_only.
- Eyedropper: the test for an empty fill reads the paint without
measuring. A sample measures the object only for a gradient or a
pattern, whose color depends on the point; a solid color gives its
color at once.
- Live Paint: the check for paint to sample reads the paint without
measuring.
- Appearance panel: the Characters row's two chips read the paint
without measuring.
- Libraries: adding a color or a pattern from the selection reads the
paint without measuring; a gradient is still measured for its
placement.
The paints and colors are the same as before. On an open path of
200,000 anchors, the Live Paint check and the Eyedropper's sample for a
point inside it take 0.004 ms together instead of 26.0 ms in a release
build.
Docs: docs/target-app-parity.md.
A 3D effect on a group or layer builds a solid from every path,
compound path and type in it, all turned about one shared axis, as the
public docs describe for groups. Fixes#4506.
perspective.grid.get reports define.name only while the grid still has
the preset's fields, as Define Grid's Preset menu shows [Custom]. Part of #5248.
The name remembered for a user library file stays only while the file still
holds the bytes that were written. A file a device name could make, which can
no longer be read, is kept too, and the save goes to a new file.
Co-authored-by: Adithya Shankaran <adikshan11@users.noreply.github.com>
After a restart an .ase file called COM¹- could have been saved as COM¹ or as
COM¹-, and the same goes for its numbered files. Such a file is kept until it is
saved here, so a library named like that gets a new file, once per run. Other
names go on replacing their own file.
An .ase file can't say whose it is, so without this COM¹ saved after COM¹- wrote
over it. The app now remembers while it runs. After a restart COM¹ gets a new
file, which costs a copy and loses nothing.
Find and Replace matched each style run on its own, and the whole-word
test looked only at the characters inside the run, so a run's end read
as a word boundary: with cat coloured red in catfish, a Whole Word
search for cat replaced it. The test now looks past the run's ends at
the characters next to it in the neighbouring runs. Matches still stay
inside one run, as the command documents.
Fixes#5901
all_texts walked every node of a visible, unlocked layer and checked
only each text object's own lock and visibility, so a text object that
was itself shown and unlocked inside a locked or hidden group was found
and replaced. It now stops at a hidden or locked group, as it stops at
a hidden or locked layer: Find and Replace and Find Next with no ids
leave that type alone, however deep the group is.
Fixes#5900
A library called COM¹ is saved as COM¹-, which can be the file of a library
really called COM¹-, and the numbered file one of them then gets can be asked
for by a third name. Saving one replaced the other. Each now gets a free file
name, and saving it again goes back to that file.
Each visible object with a URL (Attributes panel) is written as a Link
annotation with a URI action over its bounds on the page, trimmed to its
clipping groups. Hidden objects, non-printing layers and printed sheets get
none; PDF/X files leave them out with a warning. URIs are percent-encoded
to 7-bit ASCII.
A colorized image whose pixels are all black or white (or clear) is
written as a 1-bit image mask (ImageMask 1): its color as the fill, a 0
bit for each black pixel and a 1 bit for each white or clear one. The
legacy reader opens it colorized again with the same clear pixels, so
the engine no longer flattens or omits it. Other colorized images are
written with their color in their pixels, over white, with the note
for transparent images.
object.blend.spine.moveAnchor now refuses any supplied `with` (even an
empty list) together with `handle`. Direct Selection builds the blend
spine once when drawing the picked points and once per drag preview,
instead of once per picked point. Part of #5285.
When a frame's text ends with a paragraph break and the next frame is empty, restyling that empty frame was undone on reflow: an unedited empty frame after it gave the story's last paragraph its own style again. A restyled empty frame now keeps its style against unedited empty frames that follow it. Unedited empty frames before it, later frames with text and later restyled frames still give the paragraph their style, as before.
The remaining flags under crates/engine/src/cmd that read a boolean with
get(..).and_then(Value::as_bool).unwrap_or(..) now go through switch_or,
so a value such as "false" or 1 is an error instead of the default.
Omitting a flag or giving null keeps the old behavior.
- effect.move: copy
- aligned on the appearance.*, stroke.set, transparency.set and effect.*
commands, checked before any early return
- transparency.viewOpacityMask: on
- view.printTiling: on
- plugin.install: replace and allowHeavy, checked for every plug-in
- edit.clear: guides, and path.deleteAnchors' read of it
- text.create and text.createInPath: vertical (already refused by
check_switches, so their behavior is unchanged)
Loading a variable library deleted every variable of the document with its
bindings, including a text variable of the same name that the library
declares. Text made dynamic and then given a library's variable name lost its
binding, so selecting the library's data sets changed nothing. Such a
variable now stays bound, and the library's data sets change its text; other
variables are deleted as before.
The area an Intertwine member is drawn over another in was exactly the
overlap face, so its edge lay on the edge of the member below. Both edges
were anti-aliased, and the member below showed through as a thin line along
them. The area now reaches 2 pt past the face into the parts where only the
raised member paints (where drawing it again changes nothing), so its edge
lies inside that member.
Direct Selection kept only one picked point of a blend's spine. Shift-click
now adds a point of the same spine or takes it away, and dragging any picked
point moves them all. object.blend.spine.moveAnchor takes `with`, the other
points to move by as much, with the key objects on them, in one undo step.
The Join tool drew a segment between the open ends of two crossing paths
instead of trimming what overhangs the crossing. path.joinScrub now looks for
two crossing open subpaths whose overhanging ends the scrub runs over, cuts
both at the crossing and joins the kept parts there with a corner, in one undo
step. Without a crossing the endpoint join is unchanged.
The reader measured a symbol's definition only to find the page art an
instance covered, so every file with a placed symbol instance opened
with that instance as drawn art, or as its page.
- Each `%AI14_BeginSymbol` definition is read by a reader of its own into
a symbol of the document, with its art (in the symbol's space, y down),
the registration point its dictionary gives
(`AI13PatternRegistrationType`, 1 to 9 from the top left) and the
instances of other symbols in its art. A definition whose art isn't
read whole isn't kept: type, placed files, non-native art, pattern
fills, a gradient or an image the reader can't read or place, and
anything else the reader doesn't read. Its instances come in as the
page draws them, within the box of the art that is read. A definition
that holds an instance of a symbol the file doesn't have isn't kept
either, nor is a definition holding an instance of one of those, and
their instances aren't read. A definition after the first layer isn't
read.
- `%AI24_BeginSymbolList` orders the symbols as the Symbols panel lists
them. A name read twice is numbered ("Dot 2"), and instances name the
first symbol with the file's name.
- `/SymbolInstance` with `/SymbolRef` and `/RTransformMatrix` comes in as
a live instance, with the hide and lock set before it and the opacity,
blending mode, isolation and knockout of the `Xy` after it, as a
group's. An instance with appearance changes of its own
(`AI19SymbolAppearanceMap`), of a symbol that isn't read, or placed
only by `/Matrix` or `/Transform`, comes in as the page draws it.
- Symbol art takes the gradients and art styles read before it, and an
object in it linked to a listed graphic style keeps the link.
- In a symbol's art, the matrices of instances, gradients and images are
read in the canvas's space, y up, about the canvas point of the ruler
origin. Without `%AI3_TemplateBox` an image can't be placed, so the
symbol isn't kept.
- At most 4,096 definitions are read, and a symbol list names at most
16,384 symbols.
Tests: reader tests for each case above, and fuzz cases for damaged and
hostile symbol data.
links.findFiles hashes files up to MAX_HASHED (1 << 30 bytes) and compares larger ones by size only. The params text, the module doc comment and docs/mcp.md said 1 GB; they now say 1 GiB.
An art style with a Distort & Transform effect opened every object but
type as its drawn look. Objects other than type now open as themselves
with these effects live:
- Any object takes a Tweak that moves nothing and Pucker & Bloat at 0,
beside the effects it already took.
- A path, compound path or group takes Transform without copies, or a
Free Distort whose corners make a parallelogram, when it is the only
effect. Neither the style nor anything in the object may paint with a
gradient or pattern. Everything in the object must be a path, compound
path or group without a clip, an opacity mask or effects of its own.
When Transform scales, rotates or reflects, nothing in the object may
be stroked.
- An unstroked path or compound path whose segments are all straight
takes Twist when it is the only effect.
Other objects keep the drawn look, as do Zig Zag, Pucker & Bloat of
another amount, a Free Distort with other corners and Transform copies.
Docs: docs/file-format-parity.md, docs/target-app-parity.md.
Hovering looks up the bounds of every selected object, and each lookup
went through the memo key check, which compares the whole selection. That
made a hover quadratic in the selection: 1.5 s at 50,000 selected paths.
The walk now checks the key once and shares it across its lookups.
Part of #1280.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A .ai or EPS graphic style library holds graphic styles and their list
in its editing data but no layers, so it came in as its PDF part or
page with the default styles, and Other Library listed those. A file
read as its PDF part or page, because it has no layers or its layers
can't be used, now gets the styles of its list, in list order, as a
file read from its layers does: File > Open shows them in the Graphic
Styles panel and Other Library lists them in a library panel.
Style stroke weights and effects are scaled by the Large Canvas factor
of an EPS, and for a .ai whose editing data is in the units of its
pages by the pages' UserUnit or the declared Large Canvas scale.
Part of #3349
links.findFiles' params text and docs/mcp.md now say that the saved path's
parent-folder steps (.., or the empty components of a classic Mac :: run)
are resolved and that . components in a slash path are skipped before
folder names are compared.
A library colour kept only its values, so a spot colour came back as a
process colour once its document was closed or in a new one. The library
now keeps a spot colour's ink name and tint (in the .vclibrary file too);
library.use links the paint to the document's spot swatch of that name,
adding it when the document has none of that colour, in one undo step.
A spot swatch dropped on the Libraries panel brings its name and tint.
A saved link path such as `Disk:Projects:Art::photo.png` (classic Mac OS)
or `/old/Projects/Art/../photo.png` names a file in `Projects`. Ranking
the files found compared `Art` first, so a file under a folder named
`Art` came before one under `Projects`. The `::` runs and `..` components
are now resolved before the folder names are compared.
Drawing a new Pencil or Paintbrush stroke kept the earlier selection: the engine's
`insert_look` selects the new path only when `keepSelected` is on, and with it off left the
selection as it was. Illustrator starts such a stroke by dropping the selection, and the new
stroke is not selected with Keep selected off, so the command now clears the selection after
inserting a new path.
What stays as before: with Keep selected on a stroke selects only the path it draws (the
earlier selection is replaced); a stroke that continues or redraws a selected path with Keep
selected off deselects that path and keeps the rest of the selection. The Blob Brush keeps its
own Keep selected and Merge only with selection options.
`path.freehand`'s documentation, the engine test
`pencil_with_keep_selected_off_leaves_nothing_selected` and the Tool Options test
`the_new_path_is_selected_only_with_keep_selected_on` cover the three cases. Docs: roadmap,
target-app-parity and ui-parity carry the change.
- Extend vectorcraft-geom::shapes with clean-room open/pie/chord/arc ellipse and rectangle builders, per-corner radii, polygon/star corner rounding and deterministic vertex randomization, logarithmic/Archimedean spiral divergence and inner radius, and perspective/isometric 3D box face builders.
- Isolate 3D Box and customizable shape parameters as a VectorCraft add-on under crates/*/src/**/addons/:
- crates/tools/src/addons/box3d.rs (Box3dTool and CustomShape extras stored under Node::extras["vc-shape-tools"])
- crates/engine/src/cmd/addons/shapes.rs (addon.shapes.rectangle, addon.shapes.ellipse, addon.shapes.polygon, addon.shapes.star, addon.shapes.spiral, addon.shapes.box3d, and addon.shapes.configure)
- crates/ui-egui/src/addons/shapes.rs (Object > Plug-ins > 3D Box > 3D Box Tool and context-sensitive Control bar options)
- Keep crates/engine/src/cmd/create.rs, crates/tools/src/shape.rs, and crates/tools/src/draw2/family.rs byte-for-byte identical to main.
- Add translations for "3D Box" and "3D Box Tool" across all 26 .tsv catalogs in crates/ui-egui/src/i18n/.
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.
A tab definition in type of the legacy format (`numDots numTabs Xb`, a
`leader decimal tabType tabPosition Xe` for each stop, then `XB`) was
skipped, so every paragraph opened with only the default stops every
36 pt. The stops it describes now become the paragraph's tab stops
(left, center, right or decimal, at their positions), and they last
into the next paragraphs and text objects like the other paragraph
attributes. A definition with no stops, or whose descriptions were all
skipped, leaves the default ones. A description with an unknown type or
a position outside 0 to 100000 pt is skipped, and so is one past the
first 1024 of a definition. A text object's `To` ends a definition that
an earlier object left open.
Docs: docs/file-format-parity.md.
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.