With View › Snap to Grid on, a whole object moved with the Selection,
Group Selection or Direct Selection tool landed on the grid by
whichever of its two edges or its center was nearest a gridline on
each axis. An object on the grid whose width is an odd number of grid
steps has its center half a step between gridlines, so a short drag
could pull it half a step off the grid: on a 20 pt grid, a 100 pt
square at (100, 100) dragged by (8, 8) moved by (10, 10).
A move now lands the nearer of the two edges on a gridline on each
axis, as the reference app's help describes, and the center no longer
snaps. The square above stays at (100, 100). This changes the rule #769
chose for #740. The plain move and the Shift-constrained one both use
grid_pull, so both change.
Docs: docs/target-app-parity.md (Drawing aids).
the_engine_follows_the_interface_language drew only English frames
after 6efe9df3e, because a Japanese frame set the drawing language for
every test running beside it. It set the engine's language by hand and
never checked that a Japanese frame hands the engine ja. Since #2570
each unit-test thread has its own drawing language.
The test again runs a frame with the interface preference set to
Japanese and checks that the engine gets ja, then runs a frame with
the preference back on auto and checks that it gets en. It also checks
that a thread started after the Japanese frame still draws in English.
With the drawing language process-wide, as before #2570, the test
fails, and the zoom hint test beside it showed its hint in Japanese in
12 of 200 runs.
Shrink Text to Fit scales area type down at layout time, and the canvas,
PDF export and the exports that write type as outlines draw it at that
size. SVG export wrote the live text at its stored font size, letter
spacing and baseline shift, so a viewer drew it larger than the canvas
does. Legacy .ai saves wrote the stored size, leading and baseline shift
into the text objects, DXF export took the TEXT heights from the stored
size, and the CSS Properties panel gave the stored font size, letter
spacing and line height.
The text crate's scaled, which the layout scales shrunk text with, is
now public. SVG export, legacy .ai saves and the CSS Properties panel
take the type's styles from it at the layout's fit scale, and DXF
export multiplies the TEXT heights by that scale. Positions and
textLength in SVG still come from the layout.
Docs: docs/target-app-parity.md.
object.isolate on a Shaper Group isolates the group's source group, but
the hidden and locked check ran on the Shaper Group itself, so a source
group hidden or locked with object.setProps was still isolated. The
check now runs on the object that isolation mode enters.
A click with the Selection tool does not select a hidden or locked
object, an object in a hidden or locked group or layer, or a guide path
while Lock Guides is on, but object.isolate isolated each of them. Once
a locked or hidden group, or a group on a locked or hidden layer, was
isolated, a Selection tool drag moved the member under the pointer. The
Layers panel's Enter Isolation Mode sent the same command for a locked
or hidden layer or group.
object.isolate now refuses a hidden or locked object, or one in a
hidden or locked group or layer, with "the object is hidden or locked"
(the text of layer.target), and a guide path while Lock Guides is on
with "guides are locked". Isolation mode stays off, and the selection
stays as it is.
Docs: docs/mcp.md.
Nine catalogs named a blend's spine with two or more terms across the
rows for the spine commands and the Replace Spine message. Bulgarian
used four (оста, гръбна линия, скелет, гръбнак), and Ukrainian, Greek,
Finnish, Japanese, Korean, Brazilian Portuguese and both Chinese
catalogs used two each.
Each of those catalogs now uses the term its rows used most often:
оста in Bulgarian, вісь in Ukrainian, κεντρική καμπύλη in Greek,
selkäranka in Finnish, スパイン in Japanese, 스파인 in Korean, coluna in
Brazilian Portuguese, 脊线 in Simplified Chinese and 脊線 in Traditional
Chinese. 19 rows change, and no English string changes. A new i18n test
checks that every row naming the spine in the 19 catalogs uses its
catalog's term.
The reference app adds a layer while opening the New Layer dialog with
Ctrl+Alt+L on Windows and Option+Command+L on macOS. VectorCraft opened
that dialog from an Alt-click on the Layers panel's New Layer button,
from Help › Search Commands… and from the panel menu's New Layer…
item. It had no key for it, and a Cmd+Alt+L press ran New Layer as
Cmd+L does.
ui.newLayer (New Layer Options…) now has the default Cmd+Alt+L, which
is Ctrl+Alt+L on Windows and Linux. The key opens Layer Options for a
new layer, as the Alt-click does: OK adds the layer above the current
layer, and Cancel adds none. Cmd+L still adds a layer without the
dialog. Keyboard Shortcuts now lists the command with its key.
Docs: docs/mcp.md, docs/ui-parity.md, docs/target-app-parity.md.
A Blob Brush stroke followed the pointer whatever modifier keys were
held. The reference app's shortcut list has Shift constrain the Blob
Brush path horizontally or vertically.
With Shift held during a drag, each point of a Blob Brush stroke now
goes on the horizontal or vertical line through the press point that
is nearer in angle to the pointer, at 90-degree steps from the
Constrain Angle preference, and keeps its distance from the press
point. When the pointer crosses to the other line, the points dragged
since Shift went down move to it, each on its own side of the press
point. Each point reads the Shift state of its own event, so the
points dragged while Shift is up follow the pointer. The stroke still
runs path.blob once on release.
Docs: docs/target-app-parity.md.
Optical Margin Alignment and Roman Hanging Punctuation hung punctuation
in point and area type only. Type on a path kept its first and last
glyphs inside its brackets with either option on.
When Optical Margin Alignment is on, or the first paragraph has Roman
Hanging Punctuation on, the glyphs at the ends of left-to-right type on
a path now hang outside its brackets by the same amounts, measured
along the path. The type hangs at the start bracket when its line is
placed there, hangs at the end bracket when the line is placed there,
and hangs at both when it is centered. Glyphs that hang past the end
bracket do not make the type overflow. On an open path a hang stops at
the path's end, and on a closed path the two hangs together take at
most the stretch between the end and start brackets. SVG export writes
type on a path that hangs as outlines, with a warning.
Docs: docs/mcp.md, docs/type-parity.md, docs/target-app-parity.md.
The reference app justifies Arabic text with kashidas, set per paragraph
with the Paragraph panel's Insert Kashidas list (None, Short, Medium,
Long, Stylistic). VectorCraft had no kashidas, so a justified Arabic
line was spaced out at its word spaces like a Latin one.
The Paragraph panel, with the Indic options shown, now has Insert
Kashidas. On a justified horizontal area type line that is shorter
than its measure, each Arabic word with joined letters gets one kashida
between its last pair of them, drawn as tatweels (U+0640) in the font
of the letter after it. The kashidas share the line's room equally,
each at most a quarter of an em (Short), half an em (Medium) or an em
(Long); a Stylistic kashida is a row of the font's own tatweels, up to
an em. The word spaces take the room the kashidas leave, so the line
keeps its width, and line breaks do not change. The letter after a
kashida advances over it, so the caret before that letter, a selection
of it and a click on it take the kashida in. None, the default, leaves
lines as they were.
In horizontal type that is not on a path, the caret in a letter drawn
with several glyphs (a letter with a vowel mark) is now placed by the
whole letter, as hit testing already measured it, instead of by its
first glyph.
text.setFormat takes kashida. The setting is saved with the paragraph
only when it is not None, paragraph styles carry it, and Reset Panel
sets it back to None.
Docs: docs/mcp.md, docs/target-app-parity.md.
The composer offered a break after a dash, a Thai word or a space even when
a space came next, so it could start the next line with that space. A
leading space makes the next line wider, and the composer could score that
break better. With mixed type sizes now composed in rectangular area type,
Thai text whose words come from a Thai font broke as "สวัสดีครับ" and
" ภาษาไทยง่าย". The composer now offers only the break after the spaces,
as the single-line composer ends its lines.
A paragraph with one character in a different type size was broken
with the Single-line Composer, even when Every-line was chosen: the
composer gave up whenever the paragraph's glyphs did not all have the
same line metrics (ascent, descent, leading, cap height, x height and
em box top).
In rectangular area type without text wrap, rows and columns included,
the width of a line does not depend on how far down the frame it is.
There the Every-line Composer now composes a paragraph whose
characters differ in line metrics (type size, font, leading or
vertical scale, or an inline graphic taller than the text), and each
line takes its height from its own text, as with the Single-line
Composer. Area type of another shape, or with text wrap, still breaks
such a paragraph with the Single-line Composer.
Docs: docs/mcp.md, docs/target-app-parity.md.
Using a library's character or paragraph style, and scrubbing a
numeric field, closed their undo step with Session::end_undo_group,
which closes the open undo group of every document. An undo group open
in another document, such as one begun with edit.undoGroup.begin for
an MCP command_batch with atomic, was closed too, and that document's
next edits became undo steps of their own. When the style failed to
apply, or Escape ended the scrub, the other document's group was
canceled as well and the edits made in it were undone. Inside an undo
group already open in the same document, a scrub closed that group,
Escape undid the edits made in it before the drag, and a style that
failed to apply left the style it had added.
Session::begin_undo_scope and end_undo_scope make the edits in between
one undo step of the active document: the scope opens an undo group
there, or joins the one already open. Ending a scope closes only the
group it opened, in its own document, even when another document
became active in the meantime, and a canceled scope undoes only its
own edits, also inside a group that was already open. library.use and
the scrub use the scope, so Escape inside an open group also takes
back a scrub of the Transform panel's Shear field. edit.undoGroup.end
still closes the open group of every document.
When a cancel removes journal entries numbered below a recording's
start, the entries after them are numbered down. The recording's start
and the journal's hold kept the old number, so Stop Recording left out
as many of the recorded steps as there were entries removed, and the
hold no longer covered them. `Journal::remove` now moves the hold down
with the entries, and Stop Recording reads the start from the hold.
`Journal::replace` puts an entry in place of another and counts it at
its new size. A cancel uses it for the `document.activate` that takes
the place of a canceled group's entry that switched documents, so the
journal's size estimate does not keep the old entry's size.
A cancel cut the journal back to where the first canceled group began.
That also removed the commands run in other documents after it began,
which the cancel does not undo, so a replay of the journal lost them.
Each undo group now keeps the numbers of its own journal entries: the
commands that start and end in its document and the commands that edit
it. A cancel removes only those entries, and the later entries are
numbered down to close the gaps (`Journal::remove`). An entry of a
canceled group that also switched to another document, such as a batch
that draws and then activates another document, is replaced by a
`document.activate` of that document, so the commands after it replay
in the same document.
Session::end_undo_group with cancel cancels the open undo group of
every document. edit.undoGroup.end with cancel calls it, and so do a
field scrub ended with Escape and a library style that fails to apply.
The journal was then cut back to where the group of the last document
in the document list began, not to where the first of the canceled
groups began. When the group that began first belonged to an earlier
document, the journal kept the commands of that group's edits, which
the cancel had undone. After a scrub ended with Escape, which journals
no end command, replaying the journal drew those edits again.
Session::end_undo_group now cuts the journal back to the earliest start
of the groups it cancels.
An EPS or PostScript file that stops on a PostScript error named the
error, such as typecheck or rangecheck, with the operator and the
procedure, but did not say what the name means in any language.
The warning or error now gives the name as PostScript spells it,
followed by VectorCraft's own wording of what it means in the reader:
"a PostScript error (typecheck in `add`; a value has the wrong type for
the operator)". vectorcraft_eps::POSTSCRIPT_ERRORS lists the 14 names
the reader raises other than undefined, with their meanings, and a
test checks that each of those names has one. Each meaning and the
new message shapes have a row in every catalog. i18n::message now
translates placeholder values five levels deep instead of three, which
the status bar's line for an EPS file that can't be opened needs.
Docs: docs/mcp.md, docs/target-app-parity.md.
The reference app's graphs help describes adding totals to a column
design: type in the design holds a percent sign followed by two digits.
VectorCraft drew that type as typed, stretched with the design.
Type in a column design that holds a % followed by two digits now shows
the value of each column or stacked segment in place of the % and the
two digits. The first digit is the least number of places before the
decimal point, with zeros added in front (0 for as many as the value
needs), and the second is the number of places after it. Values are
rounded half away from zero. The type keeps its own size and moves to
where the design's copy puts the point it is set from. A repeating
design shows it once per column, where a whole copy ending at the value
end puts it, and Chop Design does not cut it. A sliding design moves it
with the part of the design on its side of the guide. Type inside a
group that clips or has its own opacity, blend mode, appearance or
opacity mask, or inside a blend, compound shape, envelope, repeat or
intertwine, stays in the design and shows the value in each copy. Type
inside a symbol stays as typed. The legend swatch leaves the totals
out.
Docs: docs/mcp.md, docs/target-app-parity.md.
The Tabs panel named its alignment buttons Left-, Center-, Right- and
Decimal-Justified Tab and labeled the position field X for every text.
The reference app's alignments for vertical text include Top-Justified
Tab and Bottom-Justified Tab, and its position field is Y for vertical
text.
In vertical type the Left-Justified button is now named Top-Justified
Tab and the Right-Justified button is named Bottom-Justified Tab, in
their tool tips and for screen readers, and the position field is
labeled Y. The reference page does not say which buttons these are;
VectorCraft's own mapping follows the line direction: a line of
vertical type runs from top to bottom, so a Left stop aligns the text
to the top and a Right stop aligns it to the bottom. Screen readers now
call every alignment button by its name; before, they read its glyph.
In horizontal type the tool tips and the X label do not change.
Docs: docs/target-app-parity.md.
object.objectsOnPath.attach took a path inside a compound path, blend,
envelope, repeat or Intertwine group, or a clipping path, as the path
and put it into the new Objects on Path group, so it was no longer a
part of its container. As objects, a blend's key path, a repeat's
source art, an envelope's content and a clipping path were put into
the new group the same way.
The Objects on Path tool highlighted a blend's key path or a clipping
path and attached the selection to it.
objects_on_path::held now finds the container that holds an object.
The command refuses such a path or object with a reason, and the tool
does not highlight it or attach to it. A member of a compound path
given as an object still attaches as its whole compound path. The four
new messages have rows in every complete-message catalog.
Docs: docs/mcp.md (Objects on Path).
Since #2664, swatch.new separates an RGB color into CMYK through the
color settings in a CMYK document unless keepModel is given. The Color
Guide, Color Themes and Recolor Artwork send keepModel when they save
colors they show, but the Color panel menu's Create New Swatch, the
Swatches panel and the Gradient panel did not. An RGB object's color
saved from them became a CMYK swatch, and a gradient's RGB stops
became CMYK stops.
These now send keepModel: Create New Swatch, an Alt-click on New
Swatch, the New Swatch dialog for a gradient, a Fill or Stroke proxy
or the Gradient panel's thumbnail dropped on the Swatches panel or
Alt-dropped on a swatch of its kind, and the Gradient panel's Add to
Swatches and Save to Swatches. A new swatch_paint_params returns
paint_params with keepModel: true added for an unlinked color or a
gradient. swatch.new without keepModel still converts RGB input.
Docs: docs/target-app-parity.md.
Object › Live Paint › Merge built the merged group anew and copied
only the bottom Live Paint group's name, opacity and blend mode, so
its lock, visibility, appearance, opacity mask, graphic style link and
other properties were lost. Make with a Live Paint group selected, and
a Live Paint Bucket click on a selected path beside a selected Live
Paint group, made a new group with a new id instead, and every face
took the fill of its paths again.
Merge now starts from the bottom Live Paint group, as Gap Options
does, and replaces only its faces, edges and paths and its Gap
Options. Make, and livePaint.fill with ids when the point is outside
that group's faces, add the other objects to the bottom Live Paint
group among them as Merge does.
Docs: docs/mcp.md, docs/target-app-parity.md.
An edge hidden on its own (for example with Object > Hide) while its
Live Paint group stays visible could still be highlighted, selected or
stroked, and a hidden edge nearer the pointer kept a visible one from
being picked.
lp_part_at now takes the nearest visible edge (visible_edge_near), inside
and outside the faces, for the hover, the Live Paint Selection click and
the Bucket's Shift-click. The Bucket's Shift-click sends nothing where no
edge is highlighted, and livePaint.strokeEdge also skips hidden edges, so
the Bucket strokes the edge it highlights.
With Select Fills off, or with Alt held, the Live Paint Selection tool
highlighted an edge near the pointer even when the pointer was outside
every face, but a click there looked only for a face under the pointer
and cleared the selection. Outside the faces, the highlight came from
the front-most Live Paint group with an edge in reach, not from the
nearest edge, and it included hidden groups. The Live Paint Bucket's
Shift highlight and Shift-click used the same search.
The hover, the Live Paint Selection click and the Bucket's Shift-click
now call one function. Outside every face it picks the edge nearest
the pointer, within 4 screen pixels, among the visible Live Paint
groups. A click selects or strokes the edge that the highlight shows.
This change follows up #3052.
When text.tabs.set refused a new X (past 100,000 pt), the panel kept
the stops it had changed and sorted, so later in the same frame the
ruler painted the refused position and highlighted another stop.
apply_moved now moves and sorts a copy of the stops and keeps it only
when the stops are set.
After "nothing in data set A was applied: no object is bound to a
variable it has a value for" showed in the status bar, choosing a data
set that did apply, or stepping to one, left that message in place.
`run_now` set the status from a command's `warning` and cleared it only
for `file.new`, and the Variables panel test cleared the status by hand
before its last check.
`dataset.select`, `dataset.next` and `dataset.prev` now clear the status
bar when they succeed, and the warning shows when no object is bound to
a variable the new data set has a value for. The test no longer clears
the status by hand and checks all three commands.
Pattern Brush Options' Scale and Art Brush Options' Width were number
fields only. The reference app's Brushes page documents a Scale slider
for pattern brushes and a Width slider for art brushes.
Each row now has a slider beside its field, as the Bristle Brush
Options sliders do. The slider covers the value's whole range, 1% to
10000% for Scale and 1% to 1000% for Width, in whole percents. Screen
readers name the slider as its row and read the field's number and
text.
Docs: docs/target-app-parity.md.
docs/control-protocol.md and the tool.options params text say that 0
turns the Pencil and Paintbrush options Edit selected paths within and
Close paths when ends are within off. A stroke that
started exactly on an end of a selected open path still continued that
path at 0, and a stroke that ended exactly where it started was still
closed at 0, because both checks compared the distance with <= 0.
At 0, a stroke that starts on an end of a selected open path now draws
a new path, as one that starts on the path away from its ends already
did, and a stroke that ends where it started stays open.
Docs: docs/target-app-parity.md.
A tab stop moved past another stop in the Tabs panel, by a drag on the
ruler or by a value typed in X, lost the selection. The panel sorts the
stops by position after the move but kept the selected index, so the
ruler highlighted another stop and the X, Leader and Align On fields
showed and changed that stop. With stops at 30 and 50 pt and the 50 pt
stop dragged to 10 pt, the panel selected the stop at 30 pt.
After a drag or an X value re-sorts the stops, the panel now selects
the moved stop at its new place, found by its position as a click that
adds a stop does. When text.tabs.set refuses the new position (an X
past 100,000 pt), the stops and the selection stay as they were.
Docs: docs/target-app-parity.md.
brush.new and brush.options refused a Hue Shift colorization given
without a key color ("invalid brush: missing field `key`"), and only
Brush Options picked a default. The reference app's Brushes page says
that by default the key color is the most prominent color in the art.
A Hue Shift colorization without a key now takes the solid color that
the art's fills and strokes use most, the first one found on a tie, or
black when the art has no solid color, as Brush Options already does.
Brush definitions read from a document get the same default.
brush.options replaced each field its params named, so an edit that
gave only the colorization method or only an art brush's start guide
was refused because the key or the end guide was missing. It now
merges colorization and an art brush's scale field by field.
Docs: docs/mcp.md, docs/target-app-parity.md.
Document Info had no Brushes category, and document.info refused
category "brushes". The reference app's Document Info panel menu lists
Brushes after Graphic Styles.
Document Info now has a Brushes category after Graphic Styles. For the
whole document it lists the Brushes panel's brushes. With Selection
Only it lists the brushes that the selected objects' strokes use, each
once. document.info gives the names as brushNames, the panel's flyout
offers the category, and the text report that Document Info › Save…
and File › Package write has a BRUSHES section.
Docs: docs/mcp.md, docs/target-app-parity.md.
The reference app's View menu has Show Live Paint Gaps, which
highlights the gaps in the selected Live Paint group based on the gap
options set for that group. VectorCraft had no such item.
View > Show Live Paint Gaps (view.livePaintGaps) turns the highlight
on or off, and the item reads Hide Live Paint Gaps while it is on.
While it is on and edges show, the canvas draws the gaps of each
selected Live Paint group up to its Paint Stops At size, the gaps that
livePaint.info lists. It draws them as the Gap Options preview does,
as lines in the group's Gap Preview Color. The setting works as View >
Hide Text Threads does: one setting covers every document, it is saved
with the interface state, and Hide Edges hides the gaps. It is off by
default.
Docs: docs/mcp.md, docs/target-app-parity.md.
The reference app's Brushes panel menu has Select All Unused, which
selects the brushes that are not used in the document. VectorCraft's
Brushes panel menu had no such item, and no command listed the unused
brushes.
brush.unused (Select All Unused) is a query that lists, in library
order, the brushes no stroke paints with. Strokes in the art and its
opacity masks, in symbols, graph designs, pattern tiles and graphic
styles count, and so do strokes in the art of a brush that is used.
The art of a brush that is not used does not count, so a brush whose
own art is the only art that paints with it is listed, and so is a
brush used only in the art of an unused brush. The Brushes panel
menu's Select All Unused selects these brushes, and Delete Brush and
Duplicate Brush then act on them.
Docs: docs/mcp.md, docs/target-app-parity.md.
Select > Object > Bristle Brush Strokes selected the objects whose
brush name contains "bristle". It missed objects with a bristle brush
of any other name, and it selected objects whose brush is of another
type with "Bristle" in its name.
select.object.bristleBrushStrokes now looks up the brush of each
object's topmost stroke in the document's brush library and selects the
object when the brush is a bristle brush, whatever its name.
Docs: docs/mcp.md, docs/target-app-parity.md.
The Artboards panel menu showed Convert to Artboards disabled whatever
was selected, and choosing it did nothing, although Object › Artboards
› Convert to Artboards runs artboard.convertToArtboards on the
selection.
The panel menu's item now uses the same check as Object › Artboards ›
Convert to Artboards, so it is enabled when something is selected, and
it runs the same command. Each selected object that has an area becomes
a new artboard of its geometric bounds and is removed, in one undo step.
Docs: docs/target-app-parity.md.
The reference app makes a document without opening the New Document
dialog with Ctrl+Alt+N on Windows and Option+Command+N on macOS.
VectorCraft had no default key for it.
file.newWithoutDialog (New Without Dialog, default Cmd+Alt+N) makes a
document with the settings New Document opens on: the first Recent
entry, or the Print category's Letter while Recent is empty, named
after the next untitled document. It fills the fields with the
dialog's own open, runs file.new with the params Create sends, and
does not show the dialog. A dialog that is already open stays open.
Keyboard Shortcuts lists it under File.
Docs: docs/mcp.md, docs/ui-parity.md, docs/target-app-parity.md.
The reference app adds a new fill with Ctrl+/ on Windows and Command+/
on macOS, and it adds a new stroke with Ctrl+Alt+/ on Windows and
Option+Command+/ on macOS. VectorCraft had Add New Fill and Add New
Stroke on the Appearance panel, with no default key for either.
appearance.addFill now has the default Cmd+/ and appearance.addStroke
has Cmd+Alt+/, which are Ctrl+/ and Ctrl+Alt+/ on Windows and Linux.
Like the panel's buttons and menu items, each key runs its command with
no params and adds the item to every selected object. Keyboard Shortcuts
lists both under Window › Appearance with their keys, and press_key
runs them for agents.
Docs: docs/mcp.md, docs/ui-parity.md, docs/target-app-parity.md.
craft-docpath follows a stored path only on this computer or on the
share the document is on, but its check is lexical and cannot see
mapped drives. fileio::document_path therefore followed a link stored
as Y:\img\logo.png, where Y: is mapped to \\server\other, from any
document: opening the document, links.check, Link Info and the
linked-file watch read or stamped the file on that server.
On Windows, document_path now reads the final path of the root folder
of a link's drive (std::fs::canonicalize). Windows opens that root
folder on the drive's server to answer, so the check still contacts
the server once per lookup; no file on it is read, stamped or watched.
When the final path names a share, the link is checked against the
document's path on its share, and a link on another share is refused
as on another computer, as a link stored with that share's own path
is. Links on this computer's drives and on the document's share are
followed as before, and a link on the document's own drive is not
looked up. A document opened as \\?\Z:\... is looked up on its share
as Z:\... is. Other systems are unchanged.
Docs: docs/mcp.md, docs/target-app-parity.md.
Preview in Browser wrote each preview into `VectorCraft Save for Web`,
and before each preview it removed every file and folder there that
was an hour old or older. Where the temporary folder is the shared
/tmp, another user could make that folder first, writable by everyone,
and the app wrote its previews inside it, in folders that user could
read and replace. Where the system follows other users' links in /tmp,
the folder could also be a link, and the app removed the old entries
of the folder it named.
A preview now uses that folder only when it is a folder and not a
link, and on Linux, macOS and FreeBSD only when the current user owns
it and no other user can write to it. On those systems the folder and
each preview's folder are made with mode 0700. The cleanup removes
only preview folders, on those systems only those the current user
owns. When the shared folder can't be used, the preview goes to a new
folder in the temporary folder. A later preview removes such folders
once they are an hour old.
Docs: docs/target-app-parity.md.
The version 7 .ai writer wrote the %_Bs color stops of every gradient
from ramp point 100 down to 0. The Adobe Illustrator File Format
Specification version 7.0 lists a linear gradient's stops in that order
(section 1.4.3). Its radial definition, "White & Purple Radial", lists
them from 10 to 100 (section 3.5.2). As reported on #2807, the
reference app reads both stops of #2687's radial gradient, written from
100 down to 0, at ramp point 100 and draws the circle in #1b2a6b. It
reads a radial gradient with three stops written that way in reverse
order.
The writer now lists a radial gradient's stops from the first to the
last. Each stop is written with its own midpoint to the next stop, and
section 3.5.2 says the last stop's midpoint is ignored. Linear
gradients are unchanged.
The desktop app reads the User Defined swatch and graphic style
libraries while it creates its window: set_user_dir runs
Libraries::rescan, which read and parsed every file in the folder
outside the panic guard that commands have. A parser bug that panicked
on one file stopped the app before its window appeared, at every
launch while the file stayed in the folder.
rescan now reads and parses each file inside guard::catch_panic and
skips a file whose reading panics, as it skips one that can't be read.
The other libraries are listed.
a_full_size_sheet_draws_only_what_is_in_view timed three frames of a
10,001 × 257 sheet and failed when they took 20 s or more, as they did
on a FreeBSD CI runner. The test now counts the buttons in the
screen-reader tree of the window. A graph of 40 categories and 40
series, whose 42 × 42 sheet is larger than the view both ways, draws
fewer buttons than its 41 × 41 filled cells, and a full-size sheet
draws the same number.
The renderer kept the art it made for an object's effects while the
object stayed the same. In the web build, type that a geometry effect
outlines, on its own, in a group or in a symbol's art, kept the
substitute font's outlines after its own font loaded. On the desktop,
Refresh Font List left them on a symbol instance whose art holds the
type. Objects with Revolve, Extrude & Bevel, Inflate or Rotate, and
objects and groups with a color adjustment, kept the colors they were
shaded or recolored with after the color settings changed. Live objects
already followed both.
The renderer now keeps that art with the inputs it keeps for the art
of live objects: the font generation when the art holds type, the art
of the symbols it shows, and the color settings. A temporary release
test timed the added key at 11 microseconds a frame for 400 type
objects with a drop shadow and 0.77 ms for 400 symbol instances of 100
paths each with a drop shadow, against warm frames of 5.3 and 72 ms.
Docs: docs/target-app-parity.md, docs/gaps.md.
The Variables panel menu called import_datasets and export_datasets
directly, so their file dialogs were asked for outside an entry and
shown in line. macOS shows no dialog in line, so the two items did
nothing there. On Linux the dialog held the window until it closed.
Both items now go through picks::button, as the Libraries panel
menu's Import Library… and Export Library… do. When the dialog
answers, the item runs again with the picked path. Errors still go
to the status bar.
variables_panel_data_sets_ask_off_the_ui_thread clicks both items
with dialogs shown as sheets, as on macOS, imports a CSV and exports the
data sets to the path the dialog answers.
document.pdfInfo read thumbnail with as_u64, so a value such as -1,
1.5, "1" or true gave a reply without a thumbnail and no error. This
happened for a PDF and for a PostScript file.
A thumbnail that is present and not null must now be a whole number
from 1 to the page count. Any other value is the existing "thumbnail:
no page {n} (the PDF has {count})" error, or "(the file has 1)" for
PostScript, with the value as given. An absent or null thumbnail still
gives no thumbnail.
VectorCraft had no Objects on Path. A click with the Objects on Path
tool attaches the selected objects to the path clicked. The tool is in
Edit Toolbar's Modify section and the Blend tool's flyout, and Object ›
Objects on Path › Attach and the context menu with two or more objects
selected choose it. The objects and the path go into an Objects on
Path group with the path at its bottom.
- object.objectsOnPath.attach orders the objects column by column from
left to right and top to bottom in each column. The first one sits
on the path's first anchor, the last one sits on the end of an open
path, and the rest are spread evenly by length; a closed path takes
them all the way round. Each object's attachment point (Pivot, one
of nine, default the center of its bounds without strokes) sits on
the path, and the object is turned to the path there. An object that
was rotated by an angle other than a multiple of 90° is first turned
to the nearest multiple of 90°. Rotate tilts every object by that
many degrees counter-clockwise. Symbol instances are measured by
their art. Guides, compound paths, type, paths without length and
graphs are refused with a reason.
- After every edit the objects go back on the path: editing the path
moves them, an object moved on its own goes back, and objects moved
into or out of the group in the Layers panel join the end of the
path or leave it. Scaling, shearing or reflecting the whole group
keeps each object's scale, shear or reflection, and the objects are
spread evenly along the transformed path again. Undo takes the path
and the objects back in one step, and a copied group stays live on
its own path.
- object.objectsOnPath.options changes Pivot and Rotate, and
object.objectsOnPath.expand leaves a plain group. Both act on the
groups that `ids` names, or on the groups of the selection when
`ids` is not given.
- The native file keeps the group live; SVG, PDF and the other exports
write plain art. The tool highlights the path under the pointer, has
a cursor entry, and Escape during a press cancels the click.
Part of #2545
Docs: docs/mcp.md, docs/target-app-parity.md.
Double-clicking the Live Paint Selection tool did nothing, and the tool
always selected faces and edges and highlighted them in red at 4 px.
It now opens Live Paint Selection Tool Options with Select Fills,
Select Strokes, Color and Width, the options the reference app's help
page on selecting objects lists.
- With Select Fills off, a click selects the edge nearest the pointer,
as Alt-click does. With Select Strokes off, Alt-click selects the
face under the pointer, as a click does. With both off, a click
selects no face or edge.
- Color (a layer color or a custom color) and Width (1 to 20 px) set
the highlight on the face or edge under the pointer.
- The defaults are the earlier behavior: both selections on, red
(#ff2a2a) and 4 px. The options are tool options (tool.setOption
{tool: "livePaintSelection"}): they last across tool switches and
are saved with the preferences.
Docs: docs/mcp.md, docs/target-app-parity.md.
The params text of ui.layersPanelOptions and the doc comment on
PanelOptions::locate_selected said that, with the option on, the panel
opens the rows around any object selected outside it and scrolls to its
row. When Show Layers Only, a search or the filter leaves the object
without a row, the panel opens no rows for it and scrolls as with the
option off. Both texts now describe that case, as docs/mcp.md does.
With Locate Selected Objects on, choosing a saved selection in the panel
menu's Save Selection submenu opened the rows around the recalled object
and scrolled to it. A selection changed with the panel's own menu is not
located, and the submenu's recall now follows that rule as the other
panel menu items do.
With Locate Selected Objects on, an object selected on the canvas had the
layers and groups around it opened even when Show Layers Only, a search or
the filter left it without a row. The panel did not scroll to it, but the
rows stayed open, so they showed once the panel listed every row again.
The panel now opens the rows around the object only when the object has a
row once they are open. Otherwise it opens nothing and scrolls as with the
option off. The Layers panel entries in docs/mcp.md and
docs/target-app-parity.md describe this.
By default the Layers panel opens the layers around a row and scrolls
the row into view when its highlighted rows change, for example after
a click in the panel, a new layer or Locate Object. When no row is
highlighted and a selection makes another layer or sublayer the
active one, the panel does the same for that layer's row. Selecting
art on the canvas or by a command highlights no row, so by default
the panel does not open the layer or sublayer that holds the art or
the groups around it, and does not scroll to the art's row.
Panel Options gets Locate Selected Objects, off by default and kept
with the UI state. With it on, when the last selected object changes
on the canvas or by a command, the panel opens the layers and groups
around it and scrolls to its row, as after Locate Object, but does not
highlight the row. When the selection also changes the active layer,
the panel scrolls to the object's row instead of the layer's row.
When Show Layers Only, a search or the filter leaves the object
without a row, the panel scrolls as it does with the option off. A change made with the panel's own
rows, buttons or menu is not located, and a change made during a drag
is located when the drag ends. Locate Object is unchanged.
On a layer of 5,000 groups of 10 paths with every row open, the first
panel frame after a selection change took a median of 1.6 ms with the
option on and took a median of 1.4 ms with it off, in a release build.
Docs: docs/mcp.md (locateSelected), docs/target-app-parity.md.
A tab stop placed or dragged on the Tabs panel's ruler always landed
at a fractional position, and the ruler was numbered in inches with a
tick every 18 pt whatever the document's units. The reference app
snaps stops to the ruler units with Snap To Unit in the panel menu or
with Shift held during a drag, and numbers its tab ruler in the
document's units.
The ruler is now numbered in the General unit with the canvas rulers'
label step at the ruler's scale, and has a tick every tenth of a step.
The panel menu has Snap to Unit, checked while it is on. With it on,
or with Shift held, a stop placed by a click or dragged on the ruler
lands on the nearest tick, through Unit::snap_to_ruler_tick as a
Shift-dragged ruler guide does. A snapped click that lands on an
existing stop's tick selects that stop and records no undo step. The
stops still change through text.tabs.set. The reference app's page
names Shift for a drag only; Shift also snapping a click, and a
snapped click onto a stop's tick selecting that stop, are VectorCraft's
own rules.
Docs: docs/target-app-parity.md.
Extrude & Bevel and Inflate built one solid from the path, painted with
the object's top fill or, without a fill, its top stroke. A filled and
stroked object lost its stroke, and a stroked open path was extruded as
the area it would enclose if it were closed.
Each visible stroke now builds a solid from its outline in its own
paint, beside the solid the top fill builds from the path, as Rotate
already turns each stroke's outline. When the fill and the strokes are
opaque with normal blending, the faces of all the solids are drawn in
one back-to-front order; otherwise each solid is drawn on its own, in
the order of the appearance. Under a light that is not white, a surface
drawn as one composite or with an opacity mask is also drawn on its
own. In that one order a flat cap turned toward the viewer is drawn
after every other face, untwisted solids included, so the stroke's
front cap covers the fill's walls inside it. The solids share one set
of bounds, and with Perspective one eye sees every inflated solid. A
stroke whose outline is past the 2,048-point limit is left out.
Inflate's cells cut by a hole now pass only the nearby contours to the
boolean. An inflated, outlined 72 pt "VectorCraft" takes about 8 ms
instead of 34 ms, and 66 ms with a 3 pt stroke.
Docs: docs/three-d.md (Fills and strokes), docs/target-app-parity.md.
Choosing a data set in the Variables panel, stepping through data sets
with the panel's arrows, and running `dataset.select`, `dataset.next`
and `dataset.prev` applied nothing and showed no message when no object
was bound to a variable that the data set has a value for. This
happens, for example, after Import Data Sets… defines new variables
without binding them.
The app now shows "nothing in data set A was applied: no object is
bound to a variable it has a value for" in the status bar, and the three
commands add it to the reply as `warning`. A data set with a value for
a bound variable gives no warning even when every object bound to it is
locked; `skipped` counts those objects. What applying a data set does
is unchanged.
Docs: docs/mcp.md, docs/target-app-parity.md.
The reference app's help says Smart Guides cannot be used while Snap to
Grid or Pixel Preview is on, even with the menu command selected. The
Selection tool's move already leaves Smart Guides out under Snap to
Grid, but the canvas still drew the Object Highlighting outline under
the pointer with either view on.
The hover outline now draws only with Smart Guides and Object
Highlighting on and with Snap to Grid and Pixel Preview off.
A .ai saved without PDF compatibility has only a placeholder page in
its PDF part. document.open reads such a file from its editing data,
but with `pages` given it failed with "the file holds only a
placeholder page", even for "1" on a file of one page.
When `pages` names every page of the file, document.open now reads
the editing data as it does without `pages`.
Docs: docs/mcp.md.
Preview in Browser wrote every page to one folder,
VectorCraft Save for Web in the system's temporary folder. A second
preview of a document with the same title, from the same app or from
another one running, overwrote the first preview's page, and any image
file of the same name (PNG-8 and PNG-24 both write .png), before the
browser read them. The same overwrite made
dialogs::tests_save_for_web::preview_in_browser_opens_the_page fail now
and then, when another Save for Web test in the same binary, or a test
run in another checkout on the machine, previewed Untitled-1 at the
same time.
Each preview now goes to a new folder inside that one, named after the
process id and a count of the process's previews. A name that an
earlier process with the same id left there is skipped for the next
count. The page keeps its name and its images folder. A preview first
removes the entries of VectorCraft Save for Web that were last changed
an hour ago or earlier, which also clears the files the single folder
held before.
Docs: docs/target-app-parity.md.
Saving a version 7 .ai wrote the title, spot color names and gradient
names as the UTF-8 bytes of their characters: a spot color named Café
was written (Caf\303\251) in the header's custom colors and in each x.
Type and, since #2722, layer names are written in Windows-1252, the
encoding the file installs with TE.
These names now go through #2722's Windows-1252 path, so Café is
written (Caf\351). A character outside Windows-1252 is written as ?,
with one warning for all of these names. A spot color keeps one name
in the header, in each x and X and in gradient stops, and a gradient
keeps one name in its definition and in each Bg. Two spot colors or
two gradients whose names become the same that way are numbered (? 2).
Docs: docs/file-format-parity.md.
An image in a .ai's editing data whose operands had no matrix, fewer
than 11 numbers, or a matrix of other than six numbers was left out
with no warning. So was one whose matrix maps it onto a line or a
point. The reader's warnings did not mention such an image, while the
reader warned "an image VectorCraft can't read was left out" for an
image whose size or bit depth it can't use.
Each of those exits now adds the same warning, as does the one where
the decoded pixels don't make an image.
A PDF or PDF-compatible .ai saved with Preserve Editing carries the
native document as an embedded file, with a hash of its pages at the
end of the file's /Desc. The import read /Desc with from_utf8_lossy and
looked for "hash ". /Desc is a PDF text string, which may be written as
UTF-16BE with a byte order mark (ISO 32000-1 7.9.2.2). Written that
way it holds no UTF-8 "hash ", so the import found no hash, took the
editing data as intact and reopened the native document with no
warning, even after another app had changed the pages.
/Desc is now read with text_string, the importer's decoder for text
strings, so the hash is found in UTF-16BE, UTF-8 and PDFDocEncoding.
Changed pages open as artwork with the "changed in another app"
warning. The embedded file's /UF and /F names are decoded the same
way, so editing data named only in UTF-16BE is found.
A /Desc without a hash is still trusted, as before.
alongtheblue-sketch took 0.12.0's version 7 output of the SVGs in #2692
and #2830, edited it by hand and opened it in the reference app
(https://github.com/storytold/vectorcraft/pull/2801#issuecomment-6244853548).
Writing the image lines as #2801 does, with the bracketed matrix and
the 60-digit data lines, did not change the result there. With one
line, F, added after %AI5_EndRaster, the reference app opened the image
in its clipping group and the art after it in both files.
The writer now writes F after every %AI5_EndRaster: clipped, grouped
and loose images, flattened images and images in pattern tiles. No path
is open at that point, so the line paints nothing. VectorCraft's reader
opens the files with the same objects as before, and the file's
PostScript prolog draws the same page.
The errors of the parameter checks that many commands share had no
@msg row in any catalog: "`{key}` must be a number, not {v}" from
check_numbers (object.move, object.rotate, object.setBounds and the
other callers), "missing integer `{key}`" from index_param in
cmd/appearance.rs (appearance, effect and freeform gradient commands),
the three object id errors of checked_ids_param and checked_id, and
the Unix seconds error of date_param. A template
matches only the whole text, so the row for "`{key}` must be a
number" did not match the longer check_numbers message. These errors
showed in English in the status bar, and "no such object id {value}"
matched the general "no such object {0}" row and showed half in
English. The scan in complete_languages_translate_every_message does
not read messages built with bad(), so that test did not report them.
Each of the 6 messages now has a row in the 18 catalogs that translate
every status and error message and in Bulgarian, Danish, Indonesian and
Dutch, and "missing number `{key}`" has a row in Danish and Indonesian.
No English string changes.
Docs: docs/target-app-parity.md.
path.curvatureEdit, path.removeAnchor, path.convertAnchor,
path.reshapeSegment, path.split, path.setHandle and path.insertAnchor
read subpath as an unsigned integer and fell back to 0 for any other
value. A subpath of 1.5, -1 or "1" edited subpath 0 and the command
reported success. path.split read anchor and segment with as_u64 too
and treated any other value as absent, so a bad anchor beside a good
segment split at the segment.
These commands now refuse a subpath that is not a whole number of 0 or
more with "missing integer `subpath`", the error path.curvatureEdit,
path.removeAnchor, path.convertAnchor and path.reshapeSegment already
give for a bad anchor or segment, and path.split refuses such an anchor
or segment too. Without a subpath, or with null, they edit subpath 0 as
before. The document and the undo history stay as they were after each
error. The params text of the six commands that listed subpath as
required now marks it optional.
Docs: docs/mcp.md, docs/target-app-parity.md.
craft-docpath follows a network path only on the share the document is
on, and its caller contract asks for a document opened from a mapped
drive to be passed by its UNC path. fileio::document_path passed the
drive path as given, so a document opened as Z:\jobs\doc.vectorcraft
treated a link stored as \\server\share\img\logo.png, on the share
behind Z:, as on another computer: the image was missing, and
document.open, links.check and links.info gave refused: "the path is on
another computer".
On Windows, when a stored path is refused as on another computer and
the document is on a drive, document_path now reads the final path of
the document's folder (std::fs::canonicalize). When that names a share
(\\?\UNC\server\share\...), the path is checked again against the
document's path on that share. Links on other computers stay refused,
and paths that were followed resolve as before. Opening a document,
Link Info, links.check and the linked-file watch all use it. Other
systems are unchanged.
Docs: docs/mcp.md, docs/target-app-parity.md.
Transform Each, Rasterize, Create Object Mosaic, Create Trim Marks and
Rearrange All Artboards read their optional numbers with f64_or, which
falls back to a default for a value that is not a JSON number.
object.transformEach {"scaleH": "50"} left the selected objects at
their size and reported success. Rasterize did the same with ppi,
Create Object Mosaic did the same with width, height, spacingX and
spacingY, Create Trim Marks did the same with offset, length, bleed and
weight, and Rearrange All Artboards did the same with spacing. Crop
Image cropped to the artboard when rect was a string or another value
that is not an array.
These commands now fail with "`<key>` must be a number, not <value>"
for a value that is not a number or null, as the transform commands
do, and change nothing. Transform Each checks scaleH, scaleV, moveH,
moveV and rotate. Crop Image refuses a rect that is not an array of
four numbers with "rect must be [x, y, width, height]", the message it
already gave for an array of the wrong length; a null rect crops to the
artboard, as a missing one does.
Docs: docs/mcp.md, docs/target-app-parity.md.
document.pdfInfo refused PostScript (an EPS, a .ps file, or a .ai in
PostScript form), mostly with "this is a PostScript file ..., not a
PDF", although document.open and file.place read the same file
through the EPS reader as one page.
It now answers {pages: 1, needsPassword: false, pageInfo: [{width,
height, rotation: 0, boxes: {}}]}. The page is the file's bounding box
(%%HiResBoundingBox when it has one, a US Letter page without one),
read from its comments without running its program. thumbnail: 1 is a
PNG of what that page prints, read by the EPS reader without any
editing data the file carries. A program whose header is only %! is
read as PostScript, as clipboard.importEps reads it.
Docs: docs/mcp.md, docs/target-app-parity.md.
The PostScript interpreter that reads EPS and PostScript .ai files
accepts a FlateDecode or LZWDecode filter with Predictor 2 at
BitsPerComponent 1, 2, 4, 8 and 16, but undid the horizontal
differencing only at 8 bits. At the other depths the filter gave back
the stored differences, and the image drew them instead of its
samples.
The predictor is now undone at every depth it accepts, as ISO 32000-1
§7.4.4.4 describes: each component adds the same component of the
pixel to its left, within its bit width, and each row starts again.
16-bit components add as big-endian 16-bit numbers. The padding bits
at the end of a row stay as they are, and 8-bit data takes the same
path as before. The PNG predictors are unchanged.
Docs: docs/target-app-parity.md.
While a Type tool edited text, a click on blank canvas ended the
editing and also placed new point type at the click. Area Type and
Type on a Path did the same with a click on a path.
Such a click now only ends the editing. The typing session is
committed, empty point type that a click placed is discarded as before,
and the Type tool in use stays active; the next click makes new type.
The same applies when the editing began with a double-click from a
selection tool. A click on other type still edits it at once, and a
drag that draws an area-type frame still makes the frame.
In number fields, Up/Down and a wheel notch added a whole step and kept
the fraction, so 5.1 mm stepped up to 6.1 mm. A plain step now goes to
the next whole unit in its direction (5.1 goes up to 6 and down to 5; 5
goes up to 6 and down to 4), and a Ctrl/Cmd step goes to the next tenth,
in the field's own unit. Repeated presses or notches of one size add or
take one step each, and presses and notches are applied in the order
they came. A Shift step still adds or takes ten and keeps the fraction.
The number fields that widgets.rs draws for the panels, the Control bar
and the dialogs (unit, percent, degree and count fields) all use this
step (widgets::step_value).
The Evidence list in docs/target-app-parity.md had two or three
versions of several items.
Seven items now have one line each: the Actions panel, Editing type,
Releasing ruler guides, the Layers panel, choosing the Artboard tool,
Image Trace Flat Logo mode and Create Strokes. Each kept line holds
every number, path and pull request reference of the lines it
replaces. The Layers panel line gains the #2260 note on layers dropped
under the last row. The Create Strokes line keeps the newer of its two
statements, which no longer lists the skeleton graph among the
remaining work.
Open pull requests edit the lines of seven other items, or the lines
beside them (Preferences that act, Initial live Revolve, Live effects
with previewing dialogs, Type, Masks and clipping, SVG/SVGZ, and EPS
and DXF), and those items are unchanged.
The blend, envelope and gradient mesh commands read their optional
numbers with f64_or or as_f64, which fall back to a default for a value
that is not a JSON number. object.blend.make {"steps": "8"} made a
Smooth Color blend and reported success. Blend Options did the same
with steps, distance and value. Make with Warp, Reset with Warp and
Envelope Options did the same with bend, h and v (and their long
names). Make with Mesh, Reset with Mesh and Create Gradient Mesh did
the same with rows, cols and columns. Envelope Options did the same
with fidelity, Create Gradient Mesh did the same with highlight, and
Set Mesh Point Color did the same with opacity.
These commands now fail with "`<key>` must be a number, not <value>"
for a value that is not a number or null, as the transform commands
do, and change nothing. Envelope Options refuses the warp keys whether
or not a warp envelope is selected. Create Gradient Mesh refuses an
`at` that is not exactly two numbers with "`at` must be [x, y]".
Before, it took the first two entries of a longer array, and for any
other value it made a mesh with no lines through the point.
Docs: docs/mcp.md, docs/target-app-parity.md.
Dragging a user slice's edge or corner handle with the Slice Selection
tool never snapped, while drawing a slice with the Slice tool snaps to
the grid, to whole pixels, to the art and to the ruler guides.
The handle drag now gathers its targets when the handle is pressed and
snaps what the handle moves through PointSnap::snap_scale, as the
Selection tool's bounding-box handles do: to the nearest gridline with
Snap to Grid, to whole pixels with Snap to Pixel, to the art and the
artboards with Smart Guides, and with Smart Guides off to the art's
anchors and the ruler guides with Snap to Point. Then a shown, unlocked
ruler guide within 5 px moves the side or corner onto it, as the Slice
tool moves a drawn slice's corner onto a guide. Shift still keeps the
proportions, and Alt still resizes about the center.
Docs: docs/target-app-parity.md.
A move that Shift kept level, upright or on a diagonal left that line
when it snapped. With View › Snap to Grid on, a level Shift-drag also
moved the selection up or down onto a gridline, and Snap to Pixel,
Smart Guides and Snap to Point moved it off the line in the same way.
Each axis snapped on its own after Shift had constrained the move.
With the Selection tool, and with Group Selection and Direct Selection
moving whole objects, a Shift-constrained move now slides along its
line until it snaps. With Snap to Grid or Snap to Pixel, the move
slides along the line by the shorter of the two distances that put it
on a gridline or a whole pixel on either axis. The bounds line up with
the other objects, or space evenly with them, only on the axes the line
runs along, and the grabbed point stops where the line passes an anchor
or crosses a ruler guide. A level or upright move snaps along its own
axis only. Moves without Shift snap as before.
Docs: docs/target-app-parity.md.
PathData::bounds measured each segment by its start, end and extrema,
as kurbo's bounding_box does. For a curve with a handle at about 1e308
in size, or an infinite one, kurbo's extrema computation overflows and
drops the extreme. The box then held only the anchors. The canvas check
after an edit reads that box for the selected objects, so path.create
kept a curve with handles at 1e308.
The new segment_bounds measures a segment by its control points when
one of them has an x or y that is infinite or larger in size than
f64::MAX / 16, and by its extrema otherwise, bit for bit as before.
curve_bounds joins those boxes as kurbo does, and PathData::bounds uses
it, so such a curve's box holds the whole curve and the canvas check
refuses an edit that leaves one selected.
Docs: docs/mcp.md, docs/target-app-parity.md.
The History panel menu had no Clear History, and VectorCraft had no
command that removes the undo and redo steps. In the reference app,
Clear History in the History panel menu removes the states after the
selected state, and it removes the states before it while their
changes remain in the document.
edit.clearHistory (Clear History) removes every step to undo and to
redo in the active document and returns how many it removed. The
document and the selection stay as they are, and clearing is not an
undo step. Typing or a drag still in progress becomes a step first, as
Undo makes it one, so its change stays in the document. The command is
disabled when there is no step to clear, and it waits while an input
method composes, as Undo does.
The History panel menu has Clear History after Step Forward. It runs
the command and is disabled when there is no step to clear.
Docs: docs/mcp.md, docs/target-app-parity.md.
A .ai can keep its variables and data sets in a variable library in
its editing data. VectorCraft doesn't read that library, and such a
file opened with an empty Variables panel and no warning about it.
The reader now counts the library's variables and data sets in the
editing data's /Document dictionary, and opening the file adds a
warning: "the file's variables and data sets weren't read (2
variables, 3 data sets): the Variables panel doesn't list them". The
same warning appears when the layers can't be read and the file opens
from its PDF part. A PDF that opens from the editing data it carries
gets the warning too. A library with no variables and no data sets
gives no warning.
Docs: docs/target-app-parity.md.
The Export for Screens dialog built its Example line from its fields as
typed, so it often showed a name the export does not write. It used the
artboard's name with its spaces (Artboard 1.png where the export writes
Artboard-1.png) and the first artboard whatever the range, kept a
prefix, a suffix or a device name such as CON as typed, used the scale
text as the row folder instead of the row's own folder or its size,
kept a vector row's @2x suffix, left out the folder that Create
Sub-folders adds, and read a counter set as text as 1.
The line now comes from the export's own naming. The naming part of
document.exportForScreens moves into a Naming type that the export
uses for every file, and fileio::screen_first_file returns the path of
the first file the export writes for the given params, without encoding
or writing anything. The dialog passes it the params it exports with,
without the destination, so the line shows before a folder is chosen
and is left out while the settings choose nothing to export. The Start
at field shows the counter the export uses when it was set as text.
Docs: docs/target-app-parity.md.
The engine built the PDF options without a creation date, so the
writer dated every PDF and PDF-compatible .ai file with the time it
was written. A document created on 2020-01-01 (File › File Info
shows the date) exported as a PDF with today's date in /CreationDate
and xmp:CreateDate. SVG export with the metadata option already
writes the document's own date.
The options now carry the document's File Info created date and the
time of writing as the modification date. A document without a
creation date is dated with the time of writing, as before, so PDF/X
files still get their required date. The PDF writer writes one date
for both, so the export then sets /ModDate, xmp:ModifyDate and the
PDF/A history dates to the time of writing; /CreationDate and
xmp:CreateDate keep the document's date. The page information mark
and the embedded editing data's modification date keep the time of
writing.
Docs: docs/mcp.md (File Info), docs/target-app-parity.md.
Object > Expand and Object > Create Gradient Mesh mixed a freeform
gradient's points in display RGB and turned the result back into the
points' model with the profile-free formula. CMYK inks changed: points
of C50 M20 Y0 K10 gave mesh points of C45 M24 Y0 K22. RGB points were
stored as display sRGB, so with a working RGB space other than sRGB
the mesh showed other colors. Points colored along CMYK stops, a point
paint.freeform.splitLine added between CMYK points and a point
paint.freeform.addPoint added without a color came out as RGB.
Points that share a color model now mix in it: the C, M, Y and K
values mix as inks, and Gray, RGB and Lab components mix as they are.
Points in different models still mix in display RGB. Object > Expand
and Object > Create Gradient Mesh store the result in the working RGB
space, and in a CMYK document they store it as CMYK through the color
settings, as paint.setFill stores an RGB color. Freeform::recolor and
split_line mix with the new Color::lerp_keep_model, which
vectorcraft_doc::blend::lerp_color now calls.
Docs: docs/mcp.md, docs/target-app-parity.md.
The FreeBSD job fails
fx::tests::a_drag_renders_raster_effects_within_the_drag_budget at
fx.rs:2822 with 1 previewed instead of 2.
pixels_do_not_depend_on_the_thread_count called set_default_threads
with 0, 1, 4 and 8. The override is process-wide, so a Renderer::new()
in a test running at the same time took that count. With 0 threads a
drag renders the drop shadow at full resolution, as
RenderOptions::dragging documents, so stats.previewed counts only the
filtered content (1 instead of 2).
The thread-count test now sets each renderer's threads. The drag test
gives its renderers 3 threads, so it also passes where default_threads()
is 0 (one CPU, or VECTORCRAFT_RENDER_THREADS=0).
On Linux the save dialog is the XDG desktop portal's, shown through
rfd. rfd sends no `current_filter` and reads only the file name from
the portal's answer, and the GTK file chooser keeps the typed name
when another file type is picked. With "PDF-compatible .ai" picked,
`poster` typed for a new document was saved as a native
`poster.vectorcraft`, and so was the suggested
`Untitled-1.vectorcraft` left as it was.
When a save offers two or more file types (Save As, Save a Copy, Save
as Template and the first Save of a new document), the app now calls
the portal's SaveFile itself over D-Bus. The dialog starts on the
suggested file's type, and its answer names the type picked. A name
typed without an extension, or the suggested name left as it was,
gets that type's extension. A name typed with another save format's
extension, other than the suggested file's (for Save as Template, a
template format's), is saved in that format, and any other extension
gets the picked type's added. The dialog keeps the window as its
parent on X11 and Wayland, and rfd shows the dialog when the portal
can't. The same code runs on the BSDs; Windows, macOS and the web
build keep their dialogs.
Docs: docs/target-app-parity.md.
pattern.options refused a new name that another pattern had, but not
one that a color swatch, a gradient swatch, a second swatch of the
pattern or a color group had. The rename then gave the pattern's swatch
that name, so two swatches, or a swatch and a color group, had one
name. The rename also gave the new name to every swatch that had the
old one, a color swatch included, and art linked to that color swatch
kept a link to a name no swatch had. Make and Save a Copy numbered a
pattern's or a swatch's name, but not a color group's.
pattern.options now refuses a name that a swatch or a color group has,
with "a swatch or color group named `<name>` exists". It takes a name
that only the pattern's own swatches have, such as a name Swatch
Options gave the pattern's swatch, when none of them still has the old
name. A rename gives the new name only to the pattern's swatches that
had the old one. Make and Save a Copy number a color group's name as
they number a swatch's. The new message has a row in the 17 catalogs
that translate every message (COMPLETE_MESSAGES) and in bg, da, id and
nl.
Docs: docs/mcp.md, docs/target-app-parity.md.
file.saveAsTemplate with format ait and aiVersion: "7" wrote the
legacy PostScript .ai of version 7 under the .ait name: for a
one-artboard test document, a 7,556-byte file starting %!PS-Adobe-3.0.
The web's Save as Template dialog showed the .ai Version menu for
.ait, whose Legacy (version 7) choice set aiVersion 7.
aiVersion 7 for a .ait is now an error, "aiVersion 7 writes legacy .ai
files; a .ait template is always PDF-compatible", and nothing is
written. The 17 catalogs that translate every message have a row for
it. The dialog shows no Version row for .ait, and aiVersion "pdf"
still writes the PDF-compatible template.
Docs: docs/mcp.md, docs/target-app-parity.md, docs/file-format-parity.md
(two stale rows for .ai and .ait removed).
document.export, document.serialize, document.exportSelection and
Export for Screens rows answered "PDF-compatible .ait template files
can be opened but not written" for a .ait, although
file.saveAsTemplate writes them. The CLI's convert and run --export
call document.export, so vectorcraft-cli convert in.pdf out.ait
failed with that error.
The error now reads "PDF-compatible .ait template files are written
by file.saveAsTemplate", with a row in the 17 catalogs that translate
status messages and in bg, da, id and nl. convert and run --export
write a .ait with file.saveAsTemplate, which writes every artboard
with the .ai options. convert passes --replace-lossy and its .ai flags
on, and refuses --artboard and --range for a .ait.
Docs: docs/mcp.md, docs/target-app-parity.md.
In a CMYK document, paint.setFill stores an RGB color as CMYK through
the color settings, but the other commands that take a color stored it
as RGB: text.create, text.createInPath, text.setStyle and
text.setRangeStyle colors, a shapeBuilder.merge hex fill,
paint.editGradient stops, freeform gradient points, gradient mesh
points, the Symbol Stainer's color, and swatch.new {colors},
swatch.newGroup, swatch.editGroup and swatch.edit. The Mesh tool sent
the fill as a hex value, so a CMYK fill came in as RGB.
These commands now store such a color through paint::new_color, as
paint.setFill does, and take keepModel to keep RGB (shapeBuilder.merge
inside its fill object). A color the swatch
or gradient already has keeps its model, so stops sent back with a
moved offset keep their colors (the Gradient panel's ramp and the
Gradient tool send keepModel), and a stop that names a process
swatch takes the swatch's color as it is, in paint.setFill gradients
too. Where the app copies an existing color, it keeps the color's
model: the Color panel's sliders for gradient stops and freeform
points, a swatch put on a stop or a point (the stop popover, Alt-click
in the Swatches panel, a drop on the Gradient panel's ramp), the fill
the Mesh tool gives a new point or the Symbol Stainer applies, and the
colors the Color Guide, Color Themes and Recolor Artwork save as
swatches.
swatch.new {color, mode} converted RGB into CMYK before converting it
into mode, so the New Swatch dialog in RGB mode stored #00ff00 as
R101 G176 B62 in a CMYK document. With mode, the color is now
converted once.
Docs: docs/mcp.md, docs/target-app-parity.md.
The History panel and Edit › Undo and Redo did not translate the steps
that apply or redefine a character or paragraph style, or redefine a
graphic style. The engine built their names with format! ("Apply Body",
"Redefine Body", "Redefine Graphic Style “S”"), and the panel looks a
name up as a whole, so no catalog row matched a name with a style's
name in it. A name cannot be matched against template rows instead: a
character style named Brush gives "Apply Brush", which is also the name
of the brush step.
The engine now names these steps with Session::edit_named, and each
such step keeps its template and the style's name
(HistoryEntry::named) through undo and redo. The History panel and
Edit › Undo and Redo show the template's row with the name in it
(panels::history::entry_name). The name stays as the document holds
it, and the Normal styles' names are shown in the interface language.
Every complete catalog has rows for Apply {name} and Redefine {name},
and Redefine Graphic Style “{name}” had one.
every_undo_label_is_translated also requires a row for each template,
and the only step name it allows to be built with format! is the
Liquify tools' "{} Tool".
Docs: docs/target-app-parity.md.
A marquee measured an Intertwine group and a repeat by their bounding
box. A marquee in the gap between two members of an Intertwine group,
or between two copies of a repeat, selected the object, although a
click there misses and a plain group needs a marquee to meet one of its
members. Direct and Group Selection's marquees also selected the
Intertwine group whole beside the anchor points of its members.
The marquee now touches an Intertwine group by its members, as it
touches a plain group, and it touches a repeat by the copies the repeat
draws, as clicks hit them. Direct and Group Selection's marquees select
the anchor points of an Intertwine group's members, and Group
Selection's E marquee selects the members inside it. Blends, and
envelopes outside Edit Contents, keep their box, as clicks on them do.
Docs: docs/target-app-parity.md.
A Shaper Group keeps its original shapes as its first child, at no
opacity, and clicks do not pick them outside Construction mode. The
Lasso, the Add Anchor Point, Delete Anchor Point, Anchor Point and
Scissors tools still picked their anchor points and segments, also
where the group erased the face, and Smart Guides, Snap to Point and
the transform tools' reference point snapped to them. The Lasso and
the anchor tools also picked art outside the isolated group or object
in isolation mode.
The Lasso and the anchor tools now visit only the art a click can pick:
the isolated object in isolation mode, and a Shaper Group without its
original shapes unless they are isolated (Construction mode). Smart
Guides and Snap to Point leave the original shapes out of their
targets, and Spacing Guides leave out their bounds when a face moves.
The transform tools' reference point snaps to an original shape only
when that shape is selected, or under the pointer in Construction
mode. The Shaper Group test document moves into the shared test
helpers.
Docs: docs/shaper.md, docs/target-app-parity.md.
The Appearance panel's inline effect editor still showed eight choice
parameters as text fields holding the stored value: Revolve's edge,
Extrude & Bevel's bevel, every Warp style's orientation, Rasterize's
Color Model, Background and Anti-aliasing, and the channel of Levels
and Curves. Their dialogs built these lists in their own bodies, so
effect::choices did not hold them. Nothing checked a channel's value:
a typo was stored, and Levels or Curves drew it as RGB.
effect::own_choices now holds these lists, each label as its dialog
passes it to its dropdown or radio buttons. The dialogs read the lists
from it (Rasterize's Anti-aliasing from the AntiAlias list it is built
from), and the inline editor shows each one as a dropdown with the
dialog's translated labels; Rasterize's Color Model reads CMYK or RGB
by the document's mode. effect.apply and effect.setParams refuse a
Levels or Curves channel other than rgb, red, green or blue (r, g, b
and any letter case are read too) with "`channel` must be one of rgb,
red, green, blue", and the document and its undo history stay as they
were.
Docs: docs/mcp.md (effect params), docs/target-app-parity.md.
The color and channel sliders gave screen readers a name but no value.
Each caller maps its own range onto the thumb's position, so a screen
reader announced the Color panel's G slider, Transform Each's Scale or
the Navigator's Zoom without the setting the field beside it shows.
color_slider_valued and channel_slider_valued take an optional
SliderValue: the number and text of the field beside the slider and
the slider's range. The accessibility node gets the number, the text
and the range, which extends to a number past the slider's end.
color_slider_named and channel_slider_named keep their arguments and
report no value. Every caller outside the tests uses the new variants:
the Color panel, Swatch Options, the Color Picker, the Navigator, the
Character panel's variable font axes and the dialog sliders.
form::slider_rail_named reports the number it edits, and the harmony
wheel's Brightness slider, which has no field, reports its brightness
in percent.
Docs: docs/target-app-parity.md.
Shift+Enter (Shift+Return) in the Pattern Options panel's Name field and
in the name fields that a double-click opens in the Layers, Artboards
and Asset Export panels ended the editing, as Enter does. These fields
draw their own text box and do not use the shared field widgets that
#2431 changed.
- Each of them now calls widgets::own_field_shift_enter, built on
#2431's shift_enter_keeps_focus and after_shift_enter. Shift+Enter
applies the name as Enter does and the field keeps the focus. In the
next frame the field shows the name as applied, all selected, so
typing replaces it.
- Enter, Escape and a click elsewhere work as before.
Docs: docs/target-app-parity.md.
With Direct Selection, the Pen or the anchor tools, a group, compound
path or clip group selected whole drew the anchors of its paths hollow,
the look of unselected anchors, while a path selected whole drew its
anchors filled. After two Group Selection clicks selected a path and
then its group, the group's hollow anchors covered the path's filled
ones.
With these tools, the selection overlay now draws the anchors of the
paths inside a group, compound path or clip group selected whole
filled, as it draws the anchors of a path selected whole. Direction
handles still show only on a path that is selected itself, and Direct
Selection drags only those.
Docs: docs/target-app-parity.md.
In type that holds right-to-left characters, Left and Right move the
caret through the visual caret stops (`caret_horizontal`). A cluster
drawn with no advance, such as a zero width non-joiner that the font
shapes as a glyph of its own, puts two stops at one x. A step kept the
stop with the lower byte, and the next step passed over the other one,
so the arrow keys never reached it. In Persian میخواهم laid out on
macOS, the caret could not be put between the non-joiner and خ with
the arrow keys.
Stops at one x are now visited one at a time in byte order. Going
right, the caret takes them in increasing order on left-to-right
glyphs and in decreasing order on right-to-left glyphs; going left,
it takes them in the reverse order. The end of a soft-wrapped line,
which the caret shows on the next line, is not one of those stops.
Where no two stops share an x, a step goes where it went before.