Commit Graph
100 Commits
Author SHA1 Message Date
dotCooCoo 7930a0456b i18n: the History panel translates the undo step names the sources write as literals
The History panel looked up each step's name with tl!, which reads only
a plain catalog row. 95 of the 427 names that the engine, the tools and
the shell give their undo steps had no such row in at least one complete
catalog, and 83 had no row at all (Make Blend, New Guide, Pen, Reshape
Spine and others). A step named after an effect has the effect's menu
name without its ellipsis. For many effects, and for steps such as New
Layer, the catalogs hold that name only with the ellipsis. The panel
listed those steps in English.

Each complete catalog now has a row for every step name, written in the
catalog's own terms: 1,437 rows in the 19 catalogs. The History panel
reads a step's row, or else the row of the same name with an ellipsis,
without the ellipsis, and shows the name as recorded when the catalog
has neither. A new test reads the step names from the sources, as the
tl! and message tests read theirs, and requires a row for each in every
complete catalog.

Docs: docs/target-app-parity.md.
2026-10-17 17:22:15 +07:00
dotCooCoo b0e78cf3d8 Background save: dropped Puppet Warp pins, ended Edit Text and font scans leave the document saved
Selecting other art drops Puppet Warp pins, ending Edit Text clears the
flat view, and a font scan lays type out again. None of these makes an
undo step or counts as an edit to save, but each one replaced the
document. A background save that finished afterwards compared the
document with the one it wrote by pointer, so the document stayed
marked modified. Data Recovery also wrote a new copy of a document whose
pins had been dropped.

DocState now records the documents that these changes replaced, and
DocState::same_content counts them as having the same content. The
modified flag, the end of a background save and Data Recovery use it.
After Edit Text ends, an undo state that was the saved one stays the
saved one.
2026-10-17 17:13:03 +07:00
dotCooCoo 1401051603 Artboards: Shift-click in the Artboards panel selects a range of artboards
With the Artboard tool in use, a Shift-click on a row of the Artboards
panel selects the artboards from the last row clicked without Shift to
the clicked row, and the clicked row's artboard becomes the active one.
When the artboard of the last row clicked without Shift is no longer
selected, the range starts from the active artboard, and later
Shift-clicks start from there too. Ctrl/Cmd-click still adds or removes
one artboard, and a plain click selects one. The public Select
artboards page lists Shift-click in the Artboards panel for selecting
several artboards.

A Shift-click used to add or remove one artboard, as Ctrl/Cmd-click
does (#1139).

Docs: docs/target-app-parity.md.
2026-10-17 17:13:03 +07:00
dotCooCoo 14a3740e51 Export: PDF, EPS, SVG, EMF, WMF and image exports warn about type that doesn't fit its frame
Print and Print Preview name the area type and type on a path that
prints without the text past the end of its frame (#2308). The exports
that draw type gave no such warning: a PDF, EPS, SVG, EMF, WMF or
raster export of a frame too small for its text answered with no
warning about it.

These exports now add the same warning, with "isn't drawn in the
export" in place of "doesn't print". Each export counts the visible
objects that it draws. Those are the objects on the pages or artboards
it writes, or all of them in an EPS of the visible art and in an SVG
without chosen artboards. A PDF leaves out layers that don't print
unless includeNonPrinting or createLayers is set. The objects are found
with the walk that fonts::printed_type runs for Print, and typecmd.rs
builds the message with the code that builds Print's. A .ai file keeps
the whole text in its editing data and gives no warning.

Docs: docs/mcp.md (Fitting area type), docs/target-app-parity.md.
2026-10-17 17:13:03 +07:00
dotCooCoo 30c5bd3589 Guides: Lock Guides keeps guides deselected after Undo, Exit Isolation Mode and Paste
With View > Guides > Lock Guides on, Session::select drops the paths
made into guides from the selection (#1534), but commands that set the
selection another way skipped that filter and left a locked guide
selected, so the next move moved it. object.exitIsolation selected an
isolated guide path. Undo, Redo, a canceled undo group, a canceled
drag or dialog preview and a failed command.batch brought back locked
guide paths and ruler guides in the selection. Unlock All, Show All
and Paste selected the guide paths they unlocked, showed or pasted.
Locking the guides deselected them in the active document only.

The filter is now deselect_guides, which also clears the selected
ruler guides. Session::select and Session::edit run it while Lock
Guides is on, and so do object.exitIsolation, Undo, Redo,
Session::cancel_interaction, a canceled undo group and a failed batch.
Locking the guides deselects them in every open document. With Lock
Guides off, the selection is set as before.

Docs: docs/mcp.md.
2026-10-17 17:13:03 +07:00
dotCooCoo e09038041f Part of #2345: a marquee does not select a clip group by its hidden art
A marquee measured a clip group, and the objects on a layer with a
clipping mask, by all of their art, including the art the mask hides.
A marquee around a rectangle drawn next to a clip group also selected
the group when it crossed the group's hidden art. A click already
counts only the art inside the clipping path.

The marquee now takes a clip group, or an object on a clipping layer
or sublayer, only when it meets the area inside the clipping path, and
it measures the art there within that area's bounds. The same rule
applies to the objects that Direct and Group Selection select whole,
to Group Selection's E marquee, and in isolation mode when the clip
group or clipping layer itself, or one object directly inside it, is
isolated. The anchor points that Direct Selection's marquee picks are
unchanged.

Docs: docs/target-app-parity.md.
2026-10-17 17:13:03 +07:00
dotCooCoo 59f63e9161 Variables: Lock Variables stops variables from being defined, deleted or renamed
Nothing locked the variables: any command or panel button could delete
or rename a variable that scripts and data sets name.

variable.lock {locked?} (Lock Variables: a lock button in the Variables
panel's bottom bar and an item in its menu) sets a flag that is saved
with the document. Without `locked` it toggles, and a change is one
undo step. While the flag is set, variable.define, variable.delete,
variable.rename and the four variable.make…Dynamic commands are refused
with "variables are locked", and the panel disables New Variable…,
Variable Options…, Delete Variable and Make … Dynamic. Binding,
unbinding and the dataset commands still run. variable.list
reports `locked`. A file without the flag opens unlocked.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-17 17:13:03 +07:00
dotCooCoo 7bc9fd7220 Save: save panels for files other than documents offer the file type they write
Document Info's Save, Save Swatch Library and Save Graphic Style
Library to a file, the Libraries panel's Export Library…, the presets
dialogs' Export…, the CSS Properties export, Print to a PostScript
file and Keyboard Shortcuts › Export… passed no file type to the save
panel. On Windows the panel listed "All Files", and neither the Windows
nor the macOS panel added the extension to a name typed without one.

Each of these saves now passes the one file type it writes, as Save and
Export do for document formats. The Windows panel then lists it, and
the Windows and macOS panels add its extension. The labels are the ones
the open dialogs use for these files (Swatch libraries, Libraries, the
presets, Shortcut sets), the Window menu's Graphic Style Libraries, Text
for Document Info, CSS and PostScript.
2026-10-17 17:13:03 +07:00
dotCooCoo cec7d23d96 Part of #2460: a huge selection of type, images, symbols or meshes shows its bounds
A selection with more than 100,000 anchors shows its bounds instead of
every outline and anchor (#525). The count took in path anchors only.
Type, images, symbol instances, placed documents, gradient meshes and
envelopes counted nothing, although the selection overlay outlines each
of them: a box, an area type frame or a type path, a baseline marker
for point type, and the points and mesh lines of meshes and envelopes.
The PDF importer makes type objects from a page's text lines, one image
object per image and one gradient mesh per patch or triangle of a mesh
shading, so selecting all of a large PDF outlined each of them: 131,072
selected mesh pieces need 16.8 million vertices, a 336 MB vertex
buffer, past wgpu's 256 MiB buffer limit.

The count now takes in what the overlay draws for each kind. Paths
count their anchors. Area type and type on a path count the anchors of
their frame or path, and point type counts six: its box's corners and
the ends of its baseline marker. Images, symbol instances, placed
documents, and area type or type on a path in perspective count the
four corners of their box, and gradient meshes and envelopes count
their points and the ends of their mesh lines. The selection's outline,
the key object's outline and the hover highlight all use the count.

Docs: docs/target-app-parity.md.
2026-10-17 17:13:03 +07:00
dotCooCoo 24b6486ebd Artboards: the doc comment names the shared corner the first artboard holds
On an edge two artboards share, the artboard to the right of it or below
it holds a point, except at the end of the edge that lies on that
artboard's bottom or right edge, such as where both artboards' bottom
edges meet. There the first artboard holds it, or the last when
topmost. The doc comment and the test comments now say so, and the test
asserts that corner.
2026-10-17 17:13:02 +07:00
dotCooCoo d22ccebc9c Print: a Print Tiling origin on an artboard's right or bottom edge is measured from that artboard
The Print Tiling tool's press and print.tiling.set without `artboard`
decided which artboard a point is on with Rect::contains, which leaves
out the right and bottom edges. A press exactly on an artboard's right
or bottom edge picked no artboard when no other artboard holds that
point, so the drag did not snap to that artboard's edges. An origin
there was measured from the first printed artboard: with artboards at
0 to 300 pt and 300 to 600 pt, an origin on the second one's right edge
was saved as x 600 instead of 300.

Both now count the edges as Document::art_on_artboard does, within a
millionth of a point.
2026-10-17 17:13:02 +07:00
dotCooCoo 8f87ad53dd Artboards: art centered on an artboard's right or bottom edge is on that artboard
Rearrange All Artboards, Select › All on Active Artboard (for objects
with no area), Copy's source artboard for Paste in Place, Align to
Artboard without an index, Crop Image, the Crop Image tool, Place with
`at`, CSS positions and a filter plug-in's artboard decided whether a
point or an object's center is on an artboard with Rect::contains,
which leaves out the right and bottom edges. A line along an artboard's right or
bottom edge was on no artboard: Rearrange left it behind, and All on
Active Artboard did not select it. Document::art_on_artboard counts
art flush with those edges (#890).

They now count the edges as art_on_artboard does, within a millionth
of a point. On an edge two artboards share, the artboard to the right
of it or below it still holds the point. All on Active Artboard
selects a line on a shared edge from either artboard. A press exactly
on a right or bottom edge now picks that artboard, and the Artboard
tool's cursor shows Move there.
2026-10-17 16:38:43 +07:00
dotCooCoo 67b47c7262 Artboards: Rearrange and Select All on Active Artboard measure a symbol instance by its art
Rearrange and Select › All on Active Artboard measured a symbol
instance as its transform applied to the 20 × 20 square around the
symbol's origin, not by the symbol's art. An instance whose art lies
away from that square moved with, or was selected on, the artboard
the square was on. Convert to Artboards, Transform Each, Lock and
Hide › All Artwork Above, Paste and Place from Library at a point,
and the clipboard imports measured the instance the same way, as did
Copy and Cut when they recorded the source artboard and library.add
when it sized a graphic.

These commands now measure an instance by its art through
Document::symbol_boxes, as Document::art_on_artboard, Fit to Artwork
Bounds and the Align commands do. Clipboard::bounds measures with the
symbols the clipboard holds. clipboard.exportPng, clipboard.exportPdf
and the PNG and PDF that Copy offers other apps are now cropped to
the instance's art.

Docs: docs/mcp.md.
2026-10-17 16:38:43 +07:00
dotCooCoo 1a9509b776 Blends, meshes and envelopes: an index or count that is not a whole number is refused
The blend spine commands and the gradient mesh point commands read
their anchor, segment or index as a number and dropped its fraction,
so object.blend.spine.moveAnchor with anchor 1.5 moved point 1 and
reported success. object.envelope.setMeshPoint did the same and also
read a negative index as 0. Specified Steps in object.blend.make and
object.blend.options, and the rows and columns of object.mesh.create,
object.envelope.makeWithMesh and resetWithMesh, dropped their fraction
in the same way.

These commands now fail with "`<key>` must be a whole number" for a
fraction, and object.envelope.setMeshPoint fails with "index must be
≥ 0" for a negative index, as the other point commands do. Whole
numbers written as 4.0, as the Blend Options and Make with Mesh
dialogs send them, still work.

Docs: docs/target-app-parity.md.
2026-10-17 16:38:43 +07:00
dotCooCoo 5669e02331 Variables: a data set rewrites bound type on hidden layers
dataset.select skipped every bound object inside a hidden layer or
group. Type on a hidden layer kept the previous row's text, which
showed when the layer was shown again, and dataset.list and the
Variables panel reported every row as matching that type while the
layer stayed hidden.

dataset.select, dataset.next and dataset.prev now rewrite type and
apply the other values on hidden objects and inside hidden layers and
groups. Those objects stay hidden unless a visibility value shows
them. Objects that are locked or inside a locked layer or group are
still skipped and counted in `skipped`. dataset.list, dataset.capture
and dataset.update read hidden objects as well. Binding still refuses
art inside a hidden layer or group.

The values now go in the order the variables are defined. The passes
that applied visibility values before and after the other values are
removed.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-17 16:38:43 +07:00
dotCooCoo 4285ccdda3 Rotate View: a two-finger rotation on a Mac trackpad turns the view about the pointer
A two-finger rotation on a Mac trackpad did not turn the view. With
the new General preference Trackpad Gesture to Rotate View
(`trackpadRotateView`, on by default) and the pointer over the canvas,
the rotation turns the view about the pointer, keeping the document
point under it in place. The view does not turn until the rotation
reaches 5°; it then turns by the whole rotation and follows each later
rotation event. A rotation ends 0.25 s after its last event. A rotation
in the same frame as a pinch zooms and turns about the same point.

egui reports the gesture as `Event::Rotate`: egui-winit makes it from
winit's macOS rotation gesture, and eframe makes it from Safari's
gesture events in the web build. Rotation events that arrive while
fingers are on a touch screen do not turn the view. With the
preference off, the gesture does not turn the view.

Docs: docs/mcp.md, docs/target-app-parity.md, docs/hardware-parity.md.
2026-10-17 16:38:42 +07:00
dotCooCoo c5405479b9 Type: a huge selection of type on a path draws no per-object brackets
The Selection and Direct Selection tools drew the start, center and end
brackets of every selected type on a path, and hit-tested each of
them, however large the selection was: 32,768 selected type on a path
objects added 1,310,720 vertices to the frame's mesh, 40 for each
object. The PDF importer makes type on a path for curved text lines,
so selecting all of a large PDF can reach that size.

With more than 10,000 type on a path objects selected, the tools now
neither draw nor hit-test any bracket: a press where a bracket would
be does not start a bracket drag, and the pointer does not change to
the bracket cursor there. The tools draw 10 points for each object's
brackets, so 10,000 objects take 100,000, the same number as the
canvas's limit on the anchor points it outlines. The tools crate has
its own constant with the same value.

Docs: docs/target-app-parity.md.
2026-10-17 16:38:42 +07:00
dotCooCoo df16d21e84 Images: the too-large note counts the image's pixels as RGBA
File › Open, File › Place and the Links panel note an image that needs
more than the decode allowance (2 GiB, 512 MiB in the web build). The
note counted the bytes the decoder gives, one a pixel for an 8-bit or
1-bit gray image, so a 1-bit gray PNG of 23,171 × 23,170 pixels (537 MB
as gray, 2,147,488,280 bytes as RGBA) got no note, although sampling,
recoloring and Load Texture refuse it for its RGBA size.

The note now counts width × height × 4, or the decoder's bytes when
they are more, against the allowance, with the size computed in 64
bits. Such an image gets the note an oversized RGBA image gets. Images
within the allowance get no note, as before.
2026-10-17 16:38:42 +07:00
dotCooCoo 133fd73feb Rasterize, Flatten Transparency: commands that replace art with a rendering warn about an undecodable image
Object > Rasterize, Crop Image, Flatten Transparency, Expand
Appearance of raster effects and Vector Halftone draw the art with the
renderer and replace it with the result. When the art held an image
that could not be decoded, the renderer drew nothing for it, and the
result lacked the image with no message. A raster export of the same
art warns that "an image couldn't be decoded and is missing from the
picture".

These five commands now compare decode_failures before and after they
draw, as raster exports do, and their replies carry that text as
`warning`. They still make their result in one undo step. The app
shows a command's `warning` in the status bar: after a menu command,
after OK in a dialog with a preview, and after a tool runs a command
(Crop Image's Apply).

Docs: docs/mcp.md (Flatten Transparency, Expand Appearance, Document
Raster Effects Settings, Vector halftone).
2026-10-17 16:38:42 +07:00
dotCooCoo 8ee5f96da5 Part of #2422: a selected clip group shows its clipping path's outline, not its hidden art
A selected clip group drew the outline of everything inside it: the
clipping path, the whole rectangle of a clipped image, and the whole
outline and anchors of each clipped path. The Selection tool's
bounding box, which fits the clipping path, showed on top of them.
The hover highlight and the key object outline drew the same hidden
art.

The selection overlay, the hover highlight and the key object outline
now show only the clipping path of a clip group or clipping layer,
its first child. The overlay's limit on anchors counts only that path
too, so a clip group that hides a large trace still shows its
clipping path's anchors and hover highlight. The art inside still
shows its whole outline when it is selected on its own: with Edit
Contents, a Direct or Group Selection click, or in isolation mode.

Docs: docs/target-app-parity.md.
2026-10-17 16:38:42 +07:00
dotCooCoo e74b09c86d New Document and Document Setup: values out of range in a saved preset or a file are read in range
file.new takes a width and height up to 163,830 pt, 1 to 1,000
artboards, a spacing of 0 to 100,000 pt and a bleed of 0 to 72 pt a
side, but a saved or Recent New Document preset in the preferences
was read as it was stored. With a width of -5, file.new failed with
"width/height must be positive and within the canvas"; a spacing of
-50 overlapped the artboards, a preset with 5,000 artboards made 5,000
artboards, and a bleed of 100 reached the document. A .vectorcraft
file's Document Setup bleed, superscript and subscript proportions and
small caps size also opened as stored, and document.setup then failed
when its own report was passed back to it.

These values are now read as the nearer end of the range their
setter takes, and an artboard width or height of 0 or less is read as
1 pt. Presets and files with values in range read as before.

Docs: docs/mcp.md (Document Setup, New Document).
2026-10-17 16:38:42 +07:00
dotCooCoo d18135bfcd Object Mosaic: columns and rows that are not whole numbers are refused
object.createObjectMosaic read columns and rows with as_u64 and fell
back to 10 for anything else, so 2.5, -3 or "4" made a 10 x 10 mosaic
and reported success. Transform Each read reference and seed the same
way (a reference of 2.5 used the center point, and a seed of 7.5 used
the default seed), and Rearrange All Artboards cut 1.5 columns to 1
and read "1" as the default 2.

These five parameters now take a whole number, written 4 or 4.0 as the
Rearrange All Artboards dialog sends it. A fraction or a negative
number is refused with "`columns` must be a whole number", the message
object.slice.divide gives. A string or another value that is not a
number is refused with the check_numbers error. The command then
changes nothing. The existing ranges stay: 1 to 1000 mosaic tiles
across and down, a reference of at most 8, and 1 to the artboard
count of columns.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-17 16:38:42 +07:00
dotCooCoo 3adf5141e7 Fix #2276: the Eyedropper and RGB input give color-managed CMYK values in a process document
In a CMYK document, an RGB color from the Eyedropper, a hex value or
paint.setFill was stored as CMYK by the profile-free formula
(k = 1 - max(r, g, b)), not through the color settings. RGB 83/170/223
came in as C63 M24 Y0 K13. A CMYK image was sampled through the RGB its
decoder shows, so its inks came back only when the formula happened to
round-trip them.

paint_from now converts an RGB color into a CMYK document with
Cms::convert and the settings' intent, as Document Color Mode and Swatch
Options already do. This covers paint.setFill and paint.setStroke,
gradient stops, swatch.new {color} and the Eyedropper. With Generic CMYK,
83/170/223 now separates to C59 M12 Y3 K0, and #ff0000 now separates to
C0 M97 Y100 K0 instead of C0 M100 Y100 K0. keepModel: true still keeps
RGB. paint.sampleColor with a stop stores the color the same way.

The Eyedropper samples a CMYK image's ink amounts (a four-component
JPEG, or an 8- or 16-bit chunky CMYK TIFF) and returns them unchanged
(ImageBlob::sample_cmyk_object), also in an RGB document. It takes an
RGB image's sRGB pixel into the working RGB space, as the screen color
sampler does, so the picked color shows as the pixel when the working
space is not sRGB.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-17 16:38:42 +07:00
dotCooCoo 1651dadf41 .ai import: type from the layers opens without the PDF part's outlines warnings
A PDF-compatible .ai read through its editing data kept the PDF part's
warnings that text was kept as outlines ("text was converted to
outlines", the U+FFFD warning and the warning on glyphs that differ
from the installed font). The layers take from the page only text
nodes and the stroked paths around them, so filled outlined glyphs
don't reach the document, and type made from the text document holds
every character.

- The warnings are dropped when the document holds no glyphs kept as
  outlines. Stroked glyphs that come in with stroked type keep them.
- A text object that takes the page's type, next to glyphs the page has
  only as outlines, lacks those characters: a new warning counts such
  objects.
- vectorcraft_pdf exports the three warnings, is_outlines_note,
  is_glyph_outlines and holds_outlines.

Docs: docs/target-app-parity.md.
2026-10-17 16:38:41 +07:00
dotCooCoo baec72c0fe Appearance: the inline effect editor shows choice parameters as dropdowns with the dialog's labels
The Appearance panel's inline effect editor showed a parameter that
picks one of some values (Roughen's Points, Offset Path's Joins, the
menus of Smart Blur, Glass, Texturizer and De-Interlace) as a text
field holding the stored value, where the effect's dialog has a
dropdown. Its labels did not have the dialog's mappings ("Channel1"
and "Max Radius" for "Channel 1:" and "Max. Radius:"), and a plug-in
choice named mode whose value is a blend mode name showed the blend
mode list.

The editor now shows the other parameters that effect::choices lists
as dropdowns with the dialog's labels (Drop Shadow's Shade keeps its
radio buttons), translated in the "effect" context, and a pick sends
the dialog's value with effect.setParams. Glass's Texture menu ends
with Loaded Texture once a texture is loaded. A plug-in's choice lists
the plug-in's values, and the blend mode dropdown is only for our own
effects. The labels are form::humanize's without the colon. The dialog
and the panel both call form::choice_label and effect::glass_textures.

Docs: docs/target-app-parity.md.
2026-10-17 16:18:32 +07:00
dotCooCoo a12af589aa Rotate View: resize and rotate cursors turn with the view
In a rotated view the resize and rotate cursors were the ones for an
unturned view. With the view turned 90°, the right-middle handle of a
selection's bounding box shows at the middle of the box's bottom edge
and drags up and down, but its cursor was the left-right arrow. This
affected the Selection, Free Transform, Artboard, Slice Selection, Crop
Image, Perspective Grid and Perspective Selection tools, ruler guides,
and the spread handle of a freeform gradient.

Now each of these cursors turns with the view to the nearest 45°. The
Selection tool and the Perspective Selection tool turn the handle's
direction by the view's angle before they round it to a cursor.
ToolContext::view_angle reads the view's angle from the screen frame,
and Cursor::turned turns a resize or rotate cursor by it. The new
Cursor::rotate_about_in_view turns the pointer's direction from the
center by the view before it picks one of the rotate cursor's eight
directions. Cursor::rotate_about keeps its signature and behavior. At
0° every cursor is as it was.

Docs: docs/target-app-parity.md.
2026-10-17 16:18:28 +07:00
dotCooCoo b9309f7985 Graphs: Object › Graph › Column draws columns with graph designs
Object › Graph › Column… was a menu stub, so a graph's columns and bars
were always plain rectangles. The reference app fills them with a graph
design, vertically scaled, uniformly scaled, repeated with a value for
each copy, or stretched at a guide line.

graph.column (Column…) draws the columns, bars or stacked segments and
the legend swatch of the series picked with Group Selection with a
design saved by Object › Graph › Design. Vertically Scaled stretches the
design to each column's length at the column's width. Uniformly Scaled
scales it both ways to that length and centers it on the column.
Repeating stacks a copy for each Each Design Represents value out from
the zero line, and For Fractions chops the last copy at the value or
scales it down. Sliding keeps the part above the design's horizontal
guide line in proportion and stretches the part below. The design and
its options are saved with the graph, and the graph follows the design
when it is renamed or deleted. Columns drawn with a design cast no drop
shadow.

A marker or legend design made of one path is now drawn in a group of
its own. Painting a series with Group Selection then leaves the design's
paint alone, as it already did for designs of several objects.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-17 15:14:43 +07:00
dotCooCoo f9759afa30 Live Paint: Gap Options find and close gaps in Live Paint groups
Object > Live Paint > Gap Options was a stub, and paint leaked through
any gap between the paths of a Live Paint group. The reference app's
Gap Options stop paint at small gaps, close them with unpainted paths
and preview them on the canvas.

livePaint.gapOptions (Gap Options) sets a group's options: detect (Gap
Detection), size (small 3 pt, medium 6 pt, large 12 pt, or a custom
size over 0 up to 72 pt), color (the Gap Preview Color, #rrggbb or a
layer color's name) and closeGaps, which adds an unpainted path across
each gap to the group's paths. A gap runs from a loose end of a path
to the nearest point of any path within the size, unless the paths
already join the two close by. With detection on, the faces are made
again as if a line closed each gap, and each face or edge keeps the
look of the old one it lies in or on. The options are stored on the
group, so they are saved, copied and undone with it, and Expand drops
them. Merge keeps the bottom group's options when its Gap Detection is
on, or else turns Gap Detection on with Small when another merged
group has it on. When Gap Detection is off in every merged group,
Merge keeps it off. livePaint.info lists the options and the gaps up
to the size wide that no path closes.

The menu item opens a Gap Options dialog. With Preview on, the options
apply live and the gaps show on the canvas in the preview color, and
OK keeps them as one undo step. vectorcraft-pathops gains find_gaps
and live_paint_with_gaps.

Docs: docs/mcp.md, docs/target-app-parity.md.
i18n: the 19 complete catalogs have rows for the dialog's labels, and
bg's Gap Options menu label now names gaps instead of line spacing.
2026-10-17 15:14:43 +07:00
dotCooCoo be553fa3cf Window: the SVG Interactivity panel attaches JavaScript events and files for SVG export
Window › SVG Interactivity was a disabled menu item. VectorCraft could
not attach JavaScript to an object's events or link JavaScript files
for SVG export.

The SVG Interactivity panel lists 19 events, from onfocusin to
onscroll. Pick one, type its JavaScript and press Enter to add it to
the selected objects; the list shows their events. Remove Selected
Entry removes the picked event, and the panel menu's Clear Events
removes all of them. Link JavaScript Files… opens the JavaScript
Files dialog, which adds a path or URL to the document's list and
removes the selected one. Its Choose… button puts a picked file in
as a file:/// URL. The commands are svgInteractivity.info,
setEvent, removeEvent, clearEvents, addFile and removeFile, and
ui.javascriptFilesDialog. Each edit is one undo step. A document
links at most 64 files.

SVG export writes each event as an event attribute on the object's
element and each file as a <script xlink:href> element. With the SVG
Tiny 1.2 profile, export leaves the events out with a warning. SVG
import reads the listed event attributes and the script links back
as text. It never reads a linked file or runs a script, and it leaves
inline scripts and the links after the first 64 out with a warning.
The native format keeps both.

An unnamed <g> with data-* attributes of its own around a single
object now imports as a group. Before, import folded it into the
object, which then took the group's data-* or, when it had its own,
dropped the group's. An unnamed <g> with event attributes stays a
group in the same way. The SVG import fuzz tests now include event
attributes and <script> elements.

Docs: docs/mcp.md, docs/control-protocol.md, docs/target-app-parity.md,
docs/ui-parity.md.
2026-10-17 15:14:43 +07:00
dotCooCoo 8bcebf0e48 Artboards: Alt-click on New Artboard opens Artboard Options for the new artboard
An Alt-click (Option-click on macOS) on the Artboards panel's New
Artboard used to add an artboard at once, as a plain click does. It now
opens Artboard Options for the new artboard, filled in with the name and
size that artboard.new {} would give it, and adds nothing yet. OK adds
the artboard with the fields where artboard.new {} would place it (20 pt
to the right of the last artboard) in one undo step, and makes it the
active artboard; Cancel adds no artboard. A plain click and a row
dropped on the button work as before.

The new UI command ui.newArtboard ("New Artboard Options…") opens the
dialog. It is listed after ui.newLayer, and its label has a row in each
catalog.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-17 15:14:42 +07:00
dotCooCoo a6bac3298b Contextual Task Bar: the Artboard tool offers Resize, Duplicate, Rearrange All and Export
The Contextual Task Bar showed only with the selection tools and
selected objects. With the Artboard tool it never showed.

With the Artboard tool, the bar now sits under the selected artboards
and offers Resize, which opens Artboard Options for the active
artboard; Duplicate, which copies each selected artboard in one undo
step and selects the copies; Rearrange All, with two or more
artboards, which opens Rearrange All Artboards; and Export, which
opens Export for Screens with the selected artboards checked. The
objects' Fill, Recolor Artwork and Lock buttons are not shown with
the Artboard tool. "Resize" has a row in every catalog.

Docs: docs/target-app-parity.md.
2026-10-17 15:14:42 +07:00
dotCooCoo 0b6f3285ab Control bar: the Artboard tool shows the active artboard's preset, name, X, Y, W and H
With the Artboard tool in use, the Control bar showed Move and Scale
Artwork with Artboard and then the object controls (Fill, Stroke,
Opacity and the rest). The reference app's help lists the Control
panel, with the Artboard tool, among the places to add, delete,
rename, resize and position artboards, set their orientation and
reference point, and open Artboard Options.

While the tool is in use, the Control bar now shows the active
artboard's controls in place of the object controls: Preset, Portrait
and Landscape, New Artboard, Delete Artboard, the Name field, Artboard
Options, Rearrange All Artboards, the Transform link, Move and Scale
Artwork with Artboard, and the reference point with X, Y, W and H and
the Constrain Width and Height Proportions link. Each control runs the
command its Properties or Transform panel counterpart runs, and the
reference point is the one the Transform panel uses. In an 800-point
window the controls up to the Transform link fit, the two artwork
options show as their boxes with their names in tooltips, and the link
opens the position and size in a popover, as it does for objects.

Docs: docs/target-app-parity.md.
2026-10-17 15:14:42 +07:00
dotCooCoo d949c3d8b7 Type: Optical Margin Alignment hangs punctuation and letter edges outside the frame
Type › Optical Margin Alignment was a disabled menu item. The reference
app's Optical Margin Alignment hangs Roman punctuation and the edges of
letters such as W and A outside the margins of every paragraph of a
type object.

type.opticalMarginAlignment (Type › Optical Margin Alignment) turns it
on or off for the selected type in one undo step, and the menu item is
checked while the type has it on. In every left-to-right paragraph of
point and area type, quotation marks, hyphens, full stops and commas at
a line's start or end hang wholly outside the margin there. Asterisks,
tildes, ellipses, en and em dashes, colons and semicolons hang by half
their width, and A, V, W, Y, v, w and y hang by a tenth of theirs. A
punctuation mark followed by a quotation mark hangs with it.
Left-aligned lines hang at their left margin, right-aligned lines hang
at their right margin, and centered and justified lines hang at both.
Lines break where they did before, and the space that justification
adds between glyphs leaves out the gaps next to a hanging punctuation
mark.

The setting is off for new type and for documents from before it, and
the type object saves it only when it is on.

The Type menu's checks for Optical Margin Alignment, Type Orientation
and the Type on a Path effects now read the type inside a selected
group, as the commands do.

Docs: docs/mcp.md, docs/target-app-parity.md, docs/type-parity.md.
2026-10-17 15:14:42 +07:00
dotCooCoo 62fd793603 Expand: pattern fills expand into their tiles clipped by the shape
Object > Expand with Fill expanded gradient fills only. A pattern fill
stayed a live pattern, and object.expand.info did not report it, so the
dialog's Fill option was disabled for it. In the reference app, Expand
with Fill turns a pattern fill into the pattern's tiles, and the shape
becomes their clipping mask.

The Fill step now turns each visible pattern fill of a path or compound
path into a clip group shaped like the object, holding the tiles the
pattern lays over the object's box with fresh ids. The group isolates
the tiles' blending and takes the fill's opacity, blend mode and other
effects. Geometry effects of the fill or the object, such as Offset
Path, shape the group's outline and the box the tiles cover. A path in
the tile art with a random Transform effect is baked with its own id
first, and a path with a brush that varies at random (calligraphic or
scatter with Random variation, or bristle) is baked where the pattern
defines it first, so every tile matches the canvas. The object's other
fills and strokes stay on copies of it in paint order, and Expand stays
one undo step. Type with a pattern fill expands the same way once
Object outlines it, and object.expand.info reports pattern fills under
fill. It also reports gradient fills under a new gradient key, and the
dialog enables Expand Gradient To only when the selection has one. A
pattern fill stays as it is when it lays no tiles over the box, when a
group or type in its art has a random Transform effect, when the
pattern is scaled, rotated, reflected, stretched or sheared and its art
holds a symbol instance, and when the pattern is rotated, reflected,
stretched or sheared, or scaled while its art has geometry effects, and
its art has raster or 3D effects, or a geometry effect on a group or
type.

PatternDef gains instances_in_with, which places the tiles with a given
Scaling. Expand has stroke weights, effect distances and the tiles' own
pattern paints scale with the pattern's transform. When the pattern is
rotated, reflected, stretched or sheared, or scaled while its art has
geometry effects such as Roughen, the strokes and geometry effects of
the tiles' paths become filled art first (bake_appearance, and Outline
Stroke's conversion for brushed strokes), and their live shapes become
plain paths, in their opacity masks too. Tiles place their opacity
masks with them, unlinked ones included (place_art), which also fixes
unlinked masks in pattern tiles in PDF, EPS, EMF and WMF export and in
Distort Pattern Fills.

Faults in the shared code are fixed for every caller. assemble
dropped the effects of a sole part, so Expand lost a lone gradient
fill's own effects and Outline Stroke lost those of a stroke with a
gradient along or across it. Outline Stroke also dropped a brushed
stroke's raster effects from its brush art. Expand replaced an object
whose gradient fill could not be expanded with a plain copy; it now
leaves it as it was. Expand left the geometry effects of a gradient
fill, and with Object off those of the object, on the expanded art,
where they reshaped every strip; they now shape the clip group's
outline, and the gradient fits it, as for pattern fills.
bake_appearance dropped a stroke's raster effects from its filled
outline and filled the outline of a stroke with a gradient along or
across it with a gradient across the outline's box; it now keeps the
effects and makes the gradient meshes clipped to the outline, as
Outline Stroke does. Envelope Options > Distort Appearance uses it too.
PatternDef::offsets_covering overflowed on a region too large to count
in integers, as for a pattern scaled by 1e-20; it now gives no offsets.

Docs: docs/mcp.md, docs/control-protocol.md, docs/target-app-parity.md.
2026-10-17 15:14:42 +07:00
dotCooCoo f526671534 Save and Export ask before replacing a file that an added extension names
After the desktop save panel closes, the app adds the file type's
extension to a name typed without it (or with one the save does not
write). The panel asked only about the name as typed, so on Linux
typing `card` with `card.vectorcraft` in the folder wrote over
`card.vectorcraft` without a question. On Linux the save panel is the
XDG desktop portal, and its GTK and GNOME file choosers return the
name as typed. Windows and macOS add the extension themselves only
when the panel lists file types, and the panels of Document Info's
Save, the presets dialogs' Export…, the CSS Properties export, the
swatch and graphic style library saves and the Libraries panel's
export list none.

When the added extension names a file that exists, Save, Save As, Save
a Copy, Save as Template, the exports and the other writes through a
save panel now ask "“card.vectorcraft” already exists. Replace it?"
first (dialog `replaceFile`). OK runs the write again with the same
answer and replaces the file. Cancel writes nothing and shows the
dialog it was asked over again. The host reports whether a file exists
through a new `Services::exists`.

Docs: docs/control-protocol.md, docs/target-app-parity.md.
2026-10-17 15:14:42 +07:00
dotCooCoo 53100a458f Type: the Touch Type tool moves, scales and rotates single characters
The toolbar listed the Touch Type tool (Shift+T), but choosing it did
nothing. The reference app's Touch Type tool moves, scales and rotates
one character of live type by dragging the handles of a box around it.

A click with the tool now selects one character and shows a box around
it. Dragging the box or its lower-left handle moves the character along
its line and up or down. The upper-left handle scales it vertically,
the lower-right handle scales it horizontally, the upper-right handle
scales it proportionally, and the upper-middle handle rotates it. A
character rotated by up to about 84 degrees follows the pointer too.
Near a quarter turn, a drag moves it only along its line. Escape
cancels a drag, and otherwise lets go of the character.

Each drag is one text.touchType command and one undo step. It sets the
character's baseline shift, scales and rotation, and moves it along its
line by changing its own kerning and the kerning of the character
before it, within the kerning range. The characters after it stay
where they are. Where the font kerned the character with its
neighbors, the two kerning values also keep the character in place
when it is only raised, scaled or rotated. The characters after it
stay where they were, or move by the change in its width when it is
scaled. The first character of a line and a right-to-left character
move only up and down. The type stays editable.

Docs: docs/mcp.md, docs/target-app-parity.md, docs/type-parity.md.
2026-10-17 15:14:42 +07:00
dotCooCoo 0ed83870a4 Place: RTF files place as area type with their formatting
File > Place read text files only as plain `.txt`, and an RTF file
could not be placed. The reference app lists RTF among the formats it
places as text, keeping the file's font and style settings.

File > Place now reads RTF files, written from the Rich Text Format
Specification 1.9.1, and sets their text as area type the way a text
file is placed. Each run keeps its font from the font table, its size,
bold and italic, underline, strikethrough, all caps, small caps,
superscript or subscript (in Document Setup's proportions), baseline
shift and its color from the color table. Each paragraph keeps its
alignment, left, right and first-line indents, space before and after,
and direction. Table cells are separated by tabs and each table row is
a paragraph. Unicode characters, Windows-1252 and Mac Roman bytes and
the special-character control words are read, and of a \upr group's
ANSI and Unicode text, the Unicode text is read. Hidden text, pictures,
objects, headers, footers, notes, field instructions and other
destinations are left out, and unknown control words are skipped.

A file is read as RTF when it starts with {\rtf, whatever its name,
except a .txt file, which stays plain text. RTF files share the 4 MiB
limit of text files. Groups nested more than 512 deep share the
formatting of the deepest one read, the font and color tables keep at
most 4,096 entries, and a file with more than 100,000 paragraphs or
runs is refused. The Place dialog lists RTF files, and file.place,
file.place.info and file.place.queue report the format `rtf`.

The tests in tests_textimport.rs fail without the change, and
text_import_fuzz.rs places random RTF token streams.

Docs: docs/file-format-parity.md, docs/mcp.md,
docs/target-app-parity.md.
2026-10-17 15:14:41 +07:00
dotCooCoo e7b78e7150 Brushes: Shift-click and Ctrl/Cmd-click select several brushes, and Delete Brush deletes them
A Shift-click or Ctrl/Cmd-click on a brush in the Brushes panel used to
apply the brush, as a plain click does. It now selects brushes without
applying one. Shift-click adds the brushes from the last brush clicked
without Shift (else the highlighted brush) to the clicked one, in the
order the panel shows them. Ctrl/Cmd-click adds the clicked brush to the
highlighted brushes or removes it. The panel highlights the selected
brushes, and the bottom bar's Delete Brush and the panel menu's
Duplicate Brush and Delete Brush act on all of them. A plain click
works as before and ends the multi-selection, and so does selecting
other art, another document or another current brush.

brush.delete and brush.duplicate also take {names: [name...]}. Each
call is one undo step, and an unknown name fails before anything
changes. brush.duplicate answers {names} for a names list.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-17 15:14:41 +07:00
dotCooCoo 77f39084de Blend: Window › Blend opens a Blend panel with the blend's spacing, orientation and commands
The reference app has a Blend panel, opened with Window > Blend, that
makes and edits blends in one place. VectorCraft had the Blend tool,
Object › Blend and the Blend Options dialog, but no Blend panel.

Window › Blend opens a panel with Make, the selected blend's Spacing and
Orientation, Reverse Front to Back, Reverse Spine, Replace Spine, Expand
and Release. Each control runs its Object › Blend command, so a change
of the selected blend's spacing or orientation shows on the canvas at
once and is one undo step. With two or more objects selected and no
blend, Spacing and Orientation set the options new blends start with,
as Blend Options does. Make is dimmed until two or more objects are
selected. Spacing and Orientation are dimmed until a blend or two or
more objects are selected, and the other buttons are dimmed until a
blend is selected. The Blend Options dialog's orientation toggles now
have names for screen readers.

Docs: docs/target-app-parity.md, docs/ui-parity.md.
2026-10-17 15:14:41 +07:00
dotCooCoo ab1c386ca4 i18n: blend spine errors are translated in every complete language
The errors of the four blend spine commands (Move Spine Point, Reshape
Spine Segment, Add Spine Point and Delete Spine Point), which Direct
Selection, the Pen and the Anchor Point tool send, had no @msg row in
any catalog, so an error that reached the status bar, for example from
a Pen click, showed 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 13 messages now has a row in the 16 catalogs that translate
every status and error message, in Bulgarian and in Simplified Chinese.
The rows that name the spine use each catalog's term for it. No English
string changes.

Docs: docs/target-app-parity.md.
2026-10-17 15:14:41 +07:00
dotCooCoo 3f81c70ade .ai import: type keeps its own opacity and blend mode
A .ai or EPS opened from its editing data lost the opacity and blend
mode of its text objects. text_slot made the slot without the graphics
state's opacity and blend mode, which paths, meshes and images take, so
an Xy before a text object was lost. An Xy after it reached the slot,
but fill_slots then put type made from the text document in the slot's
place with only its visibility and name, and style_marker dropped the
transparency that a text object with an appearance carries on its look.

text_slot now takes the state's opacity and blend mode. fill_slots
gives made type the slot's opacity, blend mode, isolation and knockout.
The page's type gets them too, in place of its own transparency when
that is none, the slot's, or the slot's combined with that of the
objects around it; otherwise the page's type and the strokes around it
keep their own (a fill's, for example). When the type has transparency
and comes with other art (the strokes the page draws around it, or page
type that goes with it), a group of them takes the transparency. A text
object with an appearance takes the transparency from its look; where
the look draws more than the type and has transparency, the look and
the type go into a group that has it, and that group is not taken as
the look of a later object. The import fuzz's .ai tokens now include a
commented text object, 1 /FreeUndo , and a group's state marker
(6 () XW).

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-17 14:46:20 +07:00
dotCooCoo f3072fc748 Paths: Shift keeps a reshaped segment's handles perpendicular and equal in length
Shift pressed while dragging a segment had no effect on the curve:
Direct Selection's curved-segment and spine-segment drags, the Anchor
Point tool and the Pen with Alt sent the pointer offset alone, and the
segment bent freely. The reference app keeps the handles in the
perpendicular direction and of equal length while Shift is held.

path.reshapeSegment and object.blend.spine.reshapeSegment take
perpendicular (default false, which keeps today's behavior). With it,
both of the segment's handles stand at right angles to the line between
its anchors, on the same side and at the same length, and the point at
t lies as far from that line as the moved point does. A smooth anchor
at either end stays smooth, as it does in a drag without perpendicular,
and a segment whose anchors coincide bends as it does without
perpendicular. With Direct Selection, Shift pressed once a curved
segment's drag has started sends perpendicular; Shift held at the press
still toggles the path's selection, as before. Direct Selection's
spine-segment drag and the Anchor Point tool's segment drag send Shift
as perpendicular too, so the Pen with Alt does as well. A straight
segment dragged with Direct Selection already moves at 45° steps with
Shift.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-17 14:46:20 +07:00
dotCooCoo 0e26b258eb Fields: Shift+Enter applies a panel field's value and keeps the field active
Shift+Enter in a panel or Control bar field ended its editing, as Enter
does. The reference app's default keyboard shortcuts list "Apply value
and keep text box active" under Use panels and "Apply a value and keep
focus in edit field" under Transform panel, both Shift+Enter on Windows
and Shift+Return on macOS.

- A focused single-line field of the shared widgets (num_field,
  mixed_field, plain_field, range_field, spin_field, spin_field_auto,
  spin_plain, text_field, exact_text_field, opt_field and hex_field)
  takes Shift+Enter before its text box reads it, applies the text as
  Enter does and keeps the focus. In the next frame it shows the value
  as it now is, all selected, so typing replaces it.
- A text box of several lines keeps Shift+Enter as a line break.
- While a dialog is open, Shift+Enter applies the field's value and
  confirms the dialog, as before. The app sets widgets::set_dialog_open
  at the start of each frame.

Docs: docs/target-app-parity.md.
2026-10-17 14:46:20 +07:00
dotCooCoo d7026367f7 Paragraph: Reset Panel also resets the Hyphenation options
The Paragraph panel menu's Reset Panel turned Hyphenate off but left
the Hyphenation options: Words Longer Than, After First, Before Last,
Hyphen Limit, Hyphenation Zone and Hyphenate Capitalized Words. A
paragraph kept its custom hyphenation limits after a reset.

Reset Panel now also sets those six options to the values of
HyphenationOptions::default(), in the same text.setFormat call. The
reset paragraphs then save no hyphenation options.
2026-10-17 14:46:20 +07:00
dotCooCoo e37fbe3849 Effects: Smart Blur's Radius widens the edge search in Edge Only and Overlay Edge
Smart Blur's Edge Only and Overlay Edge modes marked a pixel when one
of its four nearest pixels differed from it by more than Threshold,
and Radius changed only the blur. The reference app's documentation
says that Radius sets the size of the area searched for dissimilar
pixels.

A pixel is now on an edge when one of the pixels the search reads
within Radius differs from it by more than Threshold and its
channels, alpha included, add up to at least that pixel's. The search
picks its neighbors as the Normal blur does, over the whole pixels of
Radius and within it, and always reads the four nearest pixels.
A color step gives a line about as wide as Radius on the side with
the larger sum, and a Radius of one pixel or less gives the same
edges as before. The edge modes' reach() includes the blurred copy
that a wide Radius reads.
On a 2,000 by 2,000 px image, Edge Only takes 0.49 s at Radius 10 and
0.34 s at Radius 100, and Normal takes 0.73 s and 0.39 s. Normal
draws the same pixels as before.

Docs: docs/target-app-parity.md.
2026-10-17 14:46:20 +07:00
dotCooCoo 08dc614938 File formats: PostScript (.ps) and .epsf files open and place
The EPS reader read PostScript by any name, but the Open and Place
dialogs hid .ps and .epsf files, and a .ps or .epsf file whose header
is only %! could not be opened or placed ("not a format VectorCraft
reads"). The reference app lists .eps, .epsf and .ps under Open and
Place.

- A read-only PostScript format (ps) joins the format table. A .ps
  file opens and places through the EPS reader and reports the
  ps format. document.export does not write .ps; file.print does.
- .epsf is an EPS extension.
- OPEN_EXTS and PLACE_EXTS list epsf and ps, so the dialogs, the CLI
  help and the MCP open_file description list them.
  UNASSOCIATED_EXTS lists them too: the macOS bundle declares neither.
- A .ps or .epsf file whose header is only %! now pastes as EPS when
  copied in a file manager, as one with a %!PS header already did, and
  clipboard.importEps reads PostScript whose header is only %!, which
  it rejected before.

Docs: docs/mcp.md, docs/file-format-parity.md (.ps row),
docs/target-app-parity.md.
2026-10-17 14:43:14 +07:00
dotCooCoo a070d83be3 EPS: images encoded with CCITTFaxDecode draw
The PostScript interpreter that reads EPS and PostScript files had no
CCITTFaxDecode filter. Reading a file whose image data went through it
stopped at an error naming the filter, and the file opened from a
fallback, such as its preview or the art drawn before the image, or
did not open.

CCITTFaxDecode now decodes CCITT Group 4 (ITU-T T.6) and Group 3
one-dimensional and mixed (ITU-T T.4) data to rows of 1-bit samples.
It reads K, Columns, Rows, EndOfBlock, EndOfLine, EncodedByteAlign and
BlackIs1 (PLRM 3rd ed., §3.13; ISO 32000-2, §7.4.6). With EndOfBlock
the data ends at its EOFB or RTC regardless of Rows, and the RTC of
mixed Group 3 data, six EOLs each with a tag bit, is found by the
filter. Damaged data keeps the rows decoded before it, and damage in
the first row is an ioerror. Columns outside 1 to 65,535 and Rows
below 0 are a rangecheck, and data that would decode to more than
256 MiB is refused. hayro-ccitt 0.3.0, already in Cargo.lock through
hayro-syntax, becomes a dependency of vectorcraft-eps.

The tests encode their images in the test, with the fax crate's
Group 4 encoder and T.4 run codes (a dev-dependency, already in
Cargo.lock through tiff), and fail without the change. The import fuzz
tests read a Group 4 and a Group 3 image coded by hand from the T.4
and T.6 tables, read the same file with random damage, and add the
filter and its parameters to the hostile programs.

Docs: docs/file-format-parity.md, docs/target-app-parity.md.
2026-10-17 14:43:14 +07:00
dotCooCoo b275eb04d0 Guides: a Cmd/Ctrl+Shift double-click releases the guide under the pointer
A double-click on a ruler guide with the Selection, Direct Selection or
Group Selection tool did nothing, whatever keys were held. A guide could
be released only with View › Guides › Release Guides.

With the Selection, Direct Selection or Group Selection tool, a
double-click with Cmd/Ctrl and Shift held now runs view.guides.release
on the ruler guide under the pointer (index) or, when no ruler guide
is there, on the guide path under it (ids). Nothing is released while
guides are hidden or locked, and a double-click without both keys does
what it did before.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-17 14:43:13 +07:00
dotCooCoo 4650ccbe26 Character: Reset Panel resets every attribute the panel shows except the font and size
The Character panel menu's Reset Panel reset tracking, leading,
kerning, baseline shift, the scales, rotation, underline,
strikethrough and All Caps, but left Small Caps, superscript or
subscript, Character Alignment, Proportional Metrics and Digits at
their custom values.

Reset Panel now also sets those five to their defaults in the same
text.setFormat call, with the values taken from CharStyle::default().
It resets them in the same characters as the other attributes: those
of the selected type, or, while the Type tool edits text, its
selection (all of that text at a caret). It resets them whether or
not Show East Asian Options and Show Indic Options (Preferences ›
Type) are on.
2026-10-17 14:43:13 +07:00
dotCooCoo 4f1e4979fc Layers: the panel menu's Save Selection submenu saves, lists and updates saved selections
The Layers panel menu had no saved-selection items, and the Select
menu was the only menu that listed saved selections. The reference
app's help also names a Save Selection menu in the Layers panel that
lists the saved selections and holds Save Selection, Update Selection
and Edit Selection.

The Layers panel menu now has a Save Selection submenu after Locate
Object. It holds Save Selection…, Update Selection and Edit Selection…,
which are enabled and run as in the Select menu, and below them the
active document's saved selections. Choosing a name runs select.recall
with that name, as the Select menu's list does.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-17 14:43:13 +07:00
dotCooCoo bde7140396 Images: sampling and recoloring decode images past 512 MB, up to 2 GB
rgba_within checked an image's size as RGBA against the allowance it
was given (2 GB on the desktop), then decoded the image within the
image crate's default limit of 512 MB. An image whose decoded pixels
take more than 512 MB, such as an RGBA image of 11,586 × 11,586 pixels,
was refused although the canvas draws it: the Eyedropper picked up
nothing from it, and Edit Colors, Recolor Artwork, print separations
and the color adjustment effects in export and Expand Appearance left
its pixels as they were.

The decoder's limit is now the allowance that rgba_within is given.
An image decoded as RGBA is no longer copied before it is used.
2026-10-17 13:52:06 +07:00
dotCooCoo cdb0bbe315 Artboards: Duplicate Artboards, Copy and Cut take every selected artboard
With several artboards selected, the Artboards panel menu's Duplicate
Artboards copied only the active one, and Copy and Cut with the
Artboard tool took only the tool's active artboard.

artboard.duplicate, artboard.copy and artboard.cut now take `indices`,
and without `index` or `indices` they act on the artboards the
Artboard tool has selected. Duplicate Artboards passes the selected
artboards and selects the copies. Duplicate and Cut are one undo step
each, and Paste adds every copied artboard back, with its art, in one
undo step. The copies keep their places relative to each other,
together right of the last artboard. A cut fails when it names every
artboard.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-17 13:52:06 +07:00
dotCooCoo 14de645441 Templates: Save as Template always writes a file with a template extension
file.saveAsTemplate kept a name whose extension named another format
and wrote the template into it: {path: "card.png"} wrote .vctemplate
JSON to card.png, and card.pdf typed in the desktop save panel got it
too. file.saveAs {format: "ait"} answered that document.export writes
other formats, but document.export does not write .ait either.

file.saveAsTemplate now replaces another format's extension (card.png
is saved as card.vctemplate, or card.ait for format: "ait") and adds
the extension to a name whose extension names no format. The desktop
panel adds the template's extension to any name that does not end in
a template extension (card.pdf.vctemplate), as Save As adds its own.
Save and Save As still refuse ait, and the error now names
file.saveAsTemplate.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-17 13:52:06 +07:00
dotCooCoo b9c3dd4519 Effects: effect.apply and effect.setParams refuse a color the effect can't draw
effect.apply and effect.setParams stored any string as the color of
Drop Shadow, Inner Glow, Outer Glow and Shift to Color. When the
effect could not read it, it drew its default color with no message,
so a typo such as #ff00 in the Drop Shadow dialog turned the shadow
black.

The commands now refuse such a color before they edit, with
"`color` must be a #rrggbb color", and the document and its undo
history stay as they were. Hex colors, the five named colors,
[r, g, b] and color objects still pass. The check runs in the
commands, so the app, command.batch steps and replayed journals get
it too. The effect dialog's OK shows the error and keeps the dialog
open, with Preview on or off. Saved documents open as before.

Docs: docs/mcp.md (effect params).
2026-10-17 13:52:06 +07:00
dotCooCoo b9d370aeff Render: every raster export warns about an undecodable image inside a pattern
A raster export warns that "an image couldn't be decoded and is
missing from the picture" when the count of failed decodes rose while
it drew. A pattern fill draws from a tile kept for each thread, and a
tile taken from that cache draws none of its art. Only the export that
made the tile counted the image, and later exports of the document gave
no warning although their pictures lacked the image too. The raster
kept for a blurred or filtered object had the same fault: the renderer
that draws pattern tiles keeps those rasters from one tile to the next,
so a tile drawn again took such an object from its raster and counted
nothing.

A kept tile and a kept effect raster now store how many decodes failed
on their thread while they were made, and each draw from them adds that
number to the count again.
2026-10-17 13:52:06 +07:00
dotCooCoo 69a90f6033 i18n: file filters, preset names and mark styles are translated in every complete language
Labels that the UI looks up through a variable or a context (#1259) had
rows only in the Bulgarian, Czech, Greek, Spanish, Finnish, French,
Polish, Brazilian Portuguese and Swedish catalogs. In German, Hungarian,
Italian, Japanese, Korean, Russian, Ukrainian, Vietnamese and both
Chinese, the UI showed most of them in English: the file dialogs' filter
names, the status bar's render and UI timings, New Document's preset
name, and the contributor tooltips with their plurals. The Printer mark
type list showed English "Roman" in each of those languages except
Hungarian, and in Japanese, Korean, Vietnamese and both Chinese it
showed the language's name for Japanese.

Those catalogs get the rows, each next to its related entry. German also
gets rows for the 3D gizmo's light, which the UI showed as "Hell"
(bright), and for Grain's Regular type, which the UI showed in English.
complete_catalogs_translate_render_site_labels requires the rows those
catalogs get and the light's row in every complete catalog, and checks
German's Regular row.

Docs: docs/localization-parity.md, docs/target-app-parity.md.
2026-10-17 13:52:06 +07:00
dotCooCoo b9b681c748 Type: redefining a text style back to a default updates the text that uses it
A style captured from text left out the attributes that the text's
JSON skips at their defaults, such as the Hyphenation options, the
Composer, tab stops, Auto kerning and the character alignment.
Redefining a paragraph or character style from text set
back to the defaults therefore did not reach the text using the
style: a paragraph that had the old Hyphen Limit or Single-line
Composer from the style kept it. setAttrs dropped a default value it
was given in the same way.

New and Redefine now capture every attribute of the selected text,
and charStyle.setAttrs and paraStyle.setAttrs keep a default value.
When a style changes, text whose value equals the old definition's
takes the new value, a default included. Local overrides on text that
uses a style other than the Normal styles stay, and an attribute that
setAttrs leaves out still leaves the text's value alone. The Character
Styles and Paragraph Styles panels compare the attributes a style holds
with the same attributes of the selection. A paragraph style made from
type with Auto alignment no longer shows "+" on that type.

Two existing behaviors change. Applying a style made with New or
Redefine from text, without Clear Overrides, now also sets the
attributes that text had at their defaults, such as Auto kerning, no
OpenType features, the Every-line Composer, no tab stops and the
default Hyphenation options. Redefining [Normal Paragraph Style] or
[Normal Character Style] from text gives every paragraph or run that
uses it those attributes too, so local values of them on that text are
reset.

Docs: docs/target-app-parity.md.
2026-10-17 13:52:05 +07:00
dotCooCoo 78330cfa2f Actions: Batch keeps files named after data sets inside the destination folder
Batch over data sets names each output file after the document and the
data set, as the reference app's File + Data Set Name option does. It
joined that name onto the destination folder as it was, and a data set
name can hold any character. A data set named a/b failed to save, and
one named a/../../x wrote its file outside the destination folder. On
Windows, one named c\d also failed to save, and one named e:f left an
empty file.

The data set's name now goes through file_part, which Export for
Screens uses for its file name prefix and a format row's suffix, so
output file names have - in place of /, \ and : from a data set's name.
When two data sets then give the same name, compared without case, the
later ones get -2, -3 and so on, as artboard file names do, through a
distinct_names helper split out of unique_file_names. This covers the
folder destination and Override Save/Export.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-17 13:52:05 +07:00
dotCooCoo 187a75ca3f Character: Small Caps, Superscript and Subscript buttons and menu items set the text
The Character panel's Small Caps, Superscript and Subscript buttons
were disabled, with "(on the roadmap)" tips, and always drawn off, and
the panel menu's items of the same names were disabled, although
text.setFormat and text.setRangeStyle already set smallCaps and
position. The reference app applies all three from the Character
panel menu.

The three buttons and menu items now turn small caps and the
superscript or subscript position on and off for the selected type, or
for the text the Type tool has selected, in Document Setup's
proportions. Turning Superscript on turns Subscript off, and turning
Subscript on turns Superscript off. They show as on when the first
selected character has the attribute, as the All Caps, Underline and
Strikethrough toggles do.

docs/mcp.md did not mention smallCaps and position; it now describes
them for text.setFormat and text.setRangeStyle.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-17 13:52:05 +07:00
dotCooCoo 13df37f9d1 .ai import: a Drop Shadow with Darkness comes in with its Darkness
An object whose art style had a Drop Shadow with Darkness (a csrc
other than 0 in the shadow filter's dictionary) came in as its drawn
look, with a warning that its appearance isn't editable yet. With a
Color shadow instead, the same object comes in as itself with its
fill, stroke and shadow.

The reader now reads such a shadow as Drop Shadow with shade
"darkness" and darkness set to the filter's dark value, a percentage
kept within 0 to 100, or 100 when the value is missing or not a
finite number. The shadow's other keys are read as for a shadow with Color,
and the object comes in as itself wherever it would with a shadow with
Color. A shadow with Color comes in with the same params as before.
The import fuzz's .ai tokens now include csrc and dark.

Docs: docs/target-app-parity.md.
2026-10-17 13:52:05 +07:00
dotCooCoo 8f71c94473 Type: a change of paint, baseline shift or rotation on one character keeps the font's kerning
Each style run was shaped on its own, so restyling one character, even
only its fill, dropped the font's kerning pairs at the new run
boundaries. A fill change on the V of 40 pt "AVAV" moved it 0.56 pt and
moved the characters after it 1.12 pt. Manual kerning on a character
also dropped the font's kerning with the character before it.

Adjacent runs that differ only in fill, stroke, underline,
strikethrough, overprint, character style name, leading, baseline
shift, rotation, Character Alignment, kerning, or tracking (as long as
both values leave standard ligatures on or both turn them off) are now
shaped together, and each glyph takes those attributes from its own
run. The font's kerning pairs apply after characters with automatic
kerning: a manual value replaces the pair after its character, and the
pair before it stays, except before a character that All Caps or Small
Caps sets as two letters (ß as SS). A change of font, size, scale or
OpenType features still shapes the characters apart, and a ligature or
a mark still does not join characters in different styles.

Docs: docs/target-app-parity.md.
2026-10-17 13:52:05 +07:00
dotCooCoo 8511d0321b SVG export: justified and Mojikumi lines keep their letter spacing in live text
SVG export wrote each style run of a line of live text as one <tspan>
and positioned the text again only after tabs and, on justified lines,
after word spaces. A viewer then spaced the characters inside the
<tspan> with the font's own advances, so it lost the letter gaps of a
Justify All line without word spaces, the gaps between the characters
of a justified Japanese line and the spacing that a Mojikumi Set
changes. On justified lines, the textLength of horizontally scaled type
also took in the justification space after it, so a viewer stretched
the glyphs into that space.

On a line that is justified or set with a Mojikumi Set, text_lines now
lays the text out again as point type without the Mojikumi Set to get
the advance a viewer gives each character. Each character that the
layout draws away from where a viewer would draw it starts a new
positioned <tspan>, and textLength and line anchors use the viewer's
widths. The last character of a horizontally scaled <tspan> takes its
width without the font's kerning against the next character. Other
lines, and lines written with Fewer <tspan> Elements, are written as
before.

Docs: docs/target-app-parity.md.
2026-10-17 13:52:05 +07:00
dotCooCoo 1e87e85ab7 i18n: Simplified Chinese translates status-bar and error messages
The Simplified Chinese catalog had 20 `@msg` rows, against 714 to 718
in each catalog listed in COMPLETE_MESSAGES, so with the interface in
Simplified Chinese most status-bar and error messages showed in
English.

- zh-hans.tsv gains the 694 missing `@msg` rows in one block with a
  header comment on its conventions: no final 。 unless the English
  ends with a period, a space between Chinese and adjacent Latin
  text, numbers or placeholders, and 、 between list items. The rows
  use the catalog's existing terms (画板, 图稿, 编组, 描边, 色板, 实时上色,
  图像描摹, 增效工具 ...).
- zh-hans joins COMPLETE_MESSAGES, so
  complete_languages_translate_every_message and the narrower message
  tests fail when a zh-hans message row is missing, and a new message
  needs a zh-hans row.
- The registry comment, docs/development.md (which also gains Greek,
  already in COMPLETE_MESSAGES), the zh-hans row of
  docs/localization-parity.md, and the interface-languages line of
  docs/target-app-parity.md (which also gains Czech, Greek, Hungarian,
  Swedish and Finnish) list Simplified Chinese among the languages
  that translate messages.
2026-10-17 13:52:05 +07:00
dotCooCoo c5d2297ef1 Docs: type-parity and the roadmap list the Hyphenation options as done
docs/type-parity.md and the Set type row of docs/roadmap.md still
listed the Hyphenation… options as missing, although the Hyphenation
dialog (#2310) sets them for each paragraph.

The type-parity row is now partial and names Words Longer Than, After
First, Before Last, Hyphen Limit, Hyphenation Zone and Hyphenate
Capitalized Words. Justification… stays missing. The Set type row of
roadmap.md lists only the justification options as missing. Its score
is unchanged. docs/target-app-parity.md and docs/gaps.md are left as
they are.
2026-10-17 13:52:05 +07:00
dotCooCoo 5a3e833ec8 Type: a click in a Devanagari syllable goes to its nearer end, and shaping is tested
A syllable drawn with several glyphs that share one cluster, such as a
pre-base i matra and its consonant (कि), had a caret stop at the edge
of each glyph. A click just right of the matra, in the left half of
the syllable, put the caret after the syllable. byte_in_line now takes
one extent for each cluster, and type on a path compares the click
with the middle of the cluster, from the start of its first glyph to
the end of its last.

New tests with an installed OpenType Devanagari font check that क्ष
forms one conjunct glyph, that र्क draws the reph after its base, that
कि draws the matra before its consonant, that Ka with a nukta matches
the precomposed क़, and that each syllable is one cluster with the
arrow keys stopping at its ends. They print SKIPPED when no such font
is installed.

Docs: docs/localization-parity.md (Hindi row), docs/target-app-parity.md.
2026-10-17 13:49:49 +07:00
dotCooCoo 5bdd9d539f Variables: Import Data Sets… reads tab-separated files and files without a name column
dataset.import read only commas between cells and refused a file whose
first header cell wasn't `name`. A value that didn't match its
variable's kind, and a repeated data set name, were reported with the
row's record number, which is not the file line after a blank line or a
quoted line break. A data set name over 255 characters gave no line.

- When the header row, read with tabs between cells, has a tab and no
  comma outside quotes, tabs separate the cells. A quoted cell may then
  hold tabs and commas.
- Without a `name` column every header cell is a variable, and the rows
  take the first free names Data Set 1, Data Set 2 and so on, as Capture
  Data Set and blank name cells do. A first header cell `name`, in any
  case, still holds the data set names.
- Value errors, a repeated data set name and a name over 255 characters
  give the file line the row starts on.
- The Variables panel's Import Data Sets… dialog also lists .tsv files.
- import_fuzz.rs imports random text, random bytes and mutated exports,
  comma- and tab-separated, into a document with bound text, visibility
  and graph variables, selects up to three of the imported data sets in
  turn, and exports every data set.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-17 13:49:49 +07:00
dotCooCoo a285efda46 Print: Print, Print Preview and Print Setup take a print preset by name
file.print, print.preview and print.setup took print settings only as
settings, so an agent had to read a preset with print.presets.list and
copy its settings into the call. They now take preset, the name of
[Default] or of a saved print preset in any case. The preset's settings
are used instead of the document's, settings applies over them, and
print.setup saves them with the document as one undo step. An unknown
name is a parameter error that lists the presets. The app's file.print
with params passes preset on to the engine.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-17 13:49:49 +07:00
dotCooCoo d5e44b321e Swatches: Alt-click on a swatch recolors the selected gradient stop
With a gradient stop selected, an Alt-click on a color swatch painted
the inactive Fill or Stroke, as it does with no stop selected. The
reference app's Default keyboard shortcuts page lists "Apply swatch
color to active (or selected) color stop: Alt-click swatch in the
Swatches panel" for the Gradient panel.

Now, when the active Fill or Stroke is a linear or radial gradient with
a selected stop, an Alt-click on a color swatch or a tint swatch
recolors that stop in one undo step, and the stop stays selected. The
stop changes as a drop of the same swatch on the Gradient panel's ramp
changes it (drop_color and dropped_link in panels/gradient.rs): it
becomes fully opaque, and a global or spot color or a tint stays
linked, so editing the swatch recolors the stop.
With a freeform gradient, the selected point takes the color through
paint.freeform.setPoint. With no stop or point selected, Alt-click
paints the inactive Fill or Stroke as before. The Fill and Stroke chip
popovers show the same swatches and behave the same.

Docs: docs/control-protocol.md, docs/target-app-parity.md.
2026-10-17 13:49:49 +07:00
dotCooCoo 3fc78187ec TIFF: planar grayscale files with extra channels, and planar CMYK files, open and place
TIFFs that store each channel as its own plane (PlanarConfiguration 2)
did not open, place or relink when they were CMYK or grayscale with
extra channels: the image decoder refuses planar CMYK, and the readers
for CMYK inks and for extra samples returned nothing for planar files.

The two readers now read every plane through the tiff crate and put
each pixel's samples together. A planar CMYK TIFF keeps its ink
amounts and is stored as a CMYK TIFF with each pixel's inks together,
at the file's resolution. Grayscale TIFFs with an alpha channel or
other extra channels, and CMYK TIFFs with alpha, read as their chunky
versions do.

A TIFF whose header claims more strips or tiles than the tiff decoder
can count is refused as too large to draw. The decoder multiplies
those counts without a check, which panics in builds with overflow
checks.

Part of #1466.

Docs: docs/file-format-parity.md, docs/target-app-parity.md.
2026-10-17 13:49:49 +07:00
dotCooCoo f0bb2760c9 Accessibility: buttons, toggles, checkboxes and sliders have names for screen readers
The controls that the shared widgets draw had no name in the
accessibility tree: icon, flat, primary, secondary and split buttons,
chips, toggles, checkboxes, radio buttons, tabs, sliders, the angle
dial, the Reference Point's squares, the spinner and preset arrows and
the searchable combo boxes. Screen readers announced each clickable one
as an unnamed element of unknown role, and a disabled icon button was
not in the tree. The Color Guide's library button, the Color Themes
Save button and the status bar's artboard buttons also had no name.

Each helper in widgets.rs except color_field now gives its response a
role, a name and its state. The name is the text, label or translated
tool tip the helper shows. The spinner, the preset arrow, the angle
dial, the harmony wheel and the Reference Point's squares, which show
none, are named "Step up or down", "Presets", "Angle", "Color wheel"
and Top Left to Bottom Right. A toggle, checkbox, radio button, tab or
Reference Point square reports whether it is on, an icon button reports
that it is pressed while it is lit, the angle dial reports its angle,
and a searchable combo box reports the text it shows. A checkbox or
radio button drawn without text is named by the label beside it. The
Tools panel's tool buttons, flyout rows and floating strips are named
by their tools' tool tips, with the active tool pressed. The title
bar's column toggle, the tear-off bars, a floating strip's close box
and the Color, Gradient and None buttons are named by their tool tips.

The status bar's First, Previous, Next and Last Artboard buttons now
have tool tips, and the Fill and Stroke chips' tool tips are
translated. The six new strings (the four artboard tips, "Color wheel"
and "Step up or down") have rows in every catalog.

split_button, chip_button, color_slider, channel_slider and the dialog
form's slider_field and slider_rail keep their arguments. New _named
variants beside them take the arrow's tool tip, the chip's tool tip or
the slider's name, and every caller in the tree uses them.

Docs: docs/target-app-parity.md.
2026-10-17 13:49:49 +07:00
dotCooCoo ee61583606 Artboards: commands given an index that names no artboard fail instead of editing another one
artboard.setProps and artboard.fitToSelection read an `index` that is
negative, fractional, a string or null as 0 and edited the first
artboard, and artboard.setProps edited the first artboard when the
call named no `index`. artboard.duplicate, artboard.copy and
artboard.cut took the Artboard tool's artboard for such an `index`.
artboard.reorder failed for it with "missing index".

These commands now read `index` as artboard.delete, artboard.move and
artboard.fitToArt do, and fail with "no such artboard" when it names
no artboard; the document and the clipboard stay as they were.
artboard.setProps without `index` fails with "missing index". Without
`index`, artboard.fitToSelection still fits the first artboard and the
copy commands still take the Artboard tool's artboard.

Docs: docs/mcp.md.
2026-10-17 13:20:23 +07:00
dotCooCoo 1e3886c053 Document Raster Effects Settings: a file's resolution outside 1 to 2400 ppi opens in range
document.rasterEffectsSettings accepts a resolution from 1 to 2,400
ppi, but a file's raster_effects_ppi opened as it was saved. With 0,
-5 or 5000 in the file, object.rasterize without a ppi param failed
with "ppi must be between 1 and 2400", and document.rasterEffectsSettings
and document.info reported the stored value. With 0 or -5, EPS and
PDF export wrote a freeform gradient as a 1 x 1 px image, and EMF
export wrote every gradient as one. A saved New Document preset in the
preferences kept such a value too.

A file's or a saved preset's resolution outside 1 to 2,400 ppi is now
read as the nearer end of that range. Documents with a resolution in
range open as before.

Docs: docs/mcp.md (resolution, rasterEffectsPpi).
2026-10-17 13:20:23 +07:00
dotCooCoo 5eea7e4f66 Appearance: the inline effect editor's text fields keep what is typed
The Appearance panel's inline effect editor drew each text parameter
(such as Drop Shadow's color and Revolve's edge) as a text field over
a copy of the stored value made again in every frame. A character
typed in one frame was gone in the next, so the field changed a value
only when its new text and the Enter press arrived in the same frame,
as with a paste.

The field is now the panels' shared exact_text_field, which keeps the
text typed while it has the focus, as the number fields beside it do.
Enter or a click elsewhere sends the value with effect.setParams as
one undo step, and Escape drops it.
2026-10-17 13:20:23 +07:00
dotCooCoo 2e9870b1d0 Rotate View: a Space+Shift drag rotates the view from any tool
A drag with Space and Shift held panned the view, as a Space drag does.

With Space and Shift held and Cmd up, a drag on the canvas now turns
the view about the canvas center, as a Rotate View tool drag does,
whatever the tool. Shift does not round the angle to 15° as it does
with the Rotate View tool, and the drag keeps turning the view when
Shift is let go. Space alone still pans, and Cmd+Space still zooms
with or without Shift. The pointer shows the rotate cursor while Space
and Shift are held without Cmd, and during the drag. The Rotate View
tool also shows it now, where it showed the arrow.

Docs: docs/target-app-parity.md.
2026-10-17 13:20:23 +07:00
dotCooCoo 951f7c2842 View: in Presentation Mode no key but ←, → and Escape acts, and no context menu opens
In Presentation Mode the canvas showed the active tool's cursor, a
right click selected the object under the pointer and opened the
context menu, and every key other than the arrows and Escape worked as
in Normal Screen Mode: tool keys, Delete, Enter, Tab, F and the
command shortcuts. A two-finger tap undid. With a tool that claims
Escape, such as the Intertwine tool, Escape did not leave the mode.

In the mode only the Escape, Left Arrow and Right Arrow keys now act,
and Escape leaves the mode whatever the tool. Every other key press,
the text it types and the clipboard keys are dropped before any
shortcut reads them, the macOS menu bar's key equivalents included. A
right click or a long touch opens no context menu, a two- or
three-finger tap undoes or redoes nothing, and the pointer over the
canvas is the default one. F and Shift+F no longer leave the mode;
Escape does.

Docs: docs/target-app-parity.md.
2026-10-17 13:20:23 +07:00
dotCooCoo 34ae88292d Links: Shift-click selects a range of links
In the Links panel, Shift-click and Cmd/Ctrl-click both added or
removed the image of the one row clicked. Shift-click now selects the
images of every row from the last row clicked without Shift to the
clicked one, in the order the panel shows them (its Show filter and
Sort order), and Cmd/Ctrl-Shift-click adds them to the selection.
Cmd/Ctrl-click still adds or removes one image, a plain click selects
one, and a double-click still goes to the link. When the last row
clicked is not shown, or the last click was in another document,
Shift-click acts as a click without Shift.

A click on a row runs select.set, select.add or select.toggle, and the
range is worked out as in the Layers panel's Shift-click.

Docs: docs/target-app-parity.md.
2026-10-17 13:20:23 +07:00
dotCooCoo a8fbae1b1f Graphic Styles: Shift-click selects a range of styles
In the Graphic Styles panel, Shift-click and Cmd/Ctrl-click both added
or removed the one style clicked. Shift-click now adds to the selection
every style from the last one clicked without Shift through the
clicked one, in the order the panel lists them, and Cmd/Ctrl-click
still adds or removes one. A plain click applies the style and selects
it alone, Alt-click still adds it on top, and the Control bar's Style
picker still applies on every click.

The panel uses swatches::click_selection, as the Swatches panel and
the library panel do.

Docs: docs/target-app-parity.md.
2026-10-17 13:20:23 +07:00
dotCooCoo 3c5d0e7ce4 Libraries: a plain click sets where a Shift-click range starts
A plain click on an item in the library panel selected the item but did
not record it as the anchor that a Shift-click ranges from. A later
Shift-click then selected only the clicked item, or ranged from an
older anchor set by an earlier Cmd/Ctrl-click or folder click. This
applied to swatch libraries and graphic style libraries.

Every click on an item or folder now goes through click_selection, as
in the Swatches panel. A plain click still selects the item alone and
adds and applies it.
2026-10-17 13:20:23 +07:00
dotCooCoo fbcb217789 Part of #2384: imported PDF text keeps its place after wide gaps between words
PDF import reads a gap wider than a fifth of an em between two glyphs
as a space, and a space that the line's tracking cannot place gets
tracking of its own. That tracking was clamped to 1000 thousandths of
an em, while type takes tracking from -1000 to 10000. A gap wider than
an em plus the width of a space came in short by the excess, so the
words after it sat to the left of where the file put them, and each
further wide gap added to the shift.

The tracking that import sets for such a space, and for the line around
it, now has the same range as type's.
2026-10-17 13:20:23 +07:00
dotCooCoo 476d228262 Tests: a language one ui-egui test sets no longer translates the tests running beside it
The language the UI is drawn in was one process-wide value in unit
tests too, and the tests run on parallel threads.
current_language_is_a_frame_setting sets Traditional Chinese for a
moment, and a test that drew text in that moment read it in Chinese.
chrome::tests::the_zoom_hint_follows_animated_zoom failed that way in
29 of 500 runs together with the i18n tests.

The drawing language is now one per process in the app and one per
thread in unit tests, as ui_mirror! keeps the shortcut and workspace
mirrors. The app keeps the same static. A new test sets the language
on another thread and checks that its own thread still draws in
English.
2026-10-17 12:50:38 +07:00
dotCooCoo 6961bed99d Tests: CLI tests do not fail on folders an earlier run left behind
The tmp() helpers in apps/vectorcraft-cli/tests/cli.rs and fileio.rs
name their folder after the process id (vectorcraft-cli-test-<pid>,
vectorcraft-cli-fileio-<pid> in the temp folder) and never empty or
remove it. When a later run gets the same process id, it finds the
earlier run's files there. mcp_automation_roots then fails on Unix with
EEXIST (Os { code: 17, kind: AlreadyExists, message: "File exists" }):
its first symlink() call finds the link in/escape.svg that the earlier
run made. A stale output file can also let a fileio test read an
earlier run's export.

The first tmp() call in each test process now removes the folder and
creates it again empty. The roots test's body moves into
automation_roots(), and mcp_automation_roots_in_a_folder_an_earlier_run_left
runs it twice in one folder, emptying the folder in between as tmp()
does. Without the removal the second run fails with the same error.
2026-10-17 12:50:38 +07:00
dotCooCoo efdb04db8a Lint: action_batch.rs compiles without an unused import on wasm32
On main, cargo xtask wasm printed an unused-import warning for json in
crates/engine/src/cmd/fileio/action_batch.rs, and this check reported
it as an error:

    RUSTFLAGS=-Dwarnings cargo check -p vectorcraft-engine --target wasm32-unknown-unknown

Every use of json! in action_batch.rs is in code compiled only for
native targets. The wasm32 version of action_batch uses Value only.

The json import now has the same #[cfg(not(target_arch = "wasm32"))]
attribute as the code that uses it. Native builds import the same
names as before.
2026-10-17 12:50:37 +07:00
dotCooCoo 3d2e8bbe82 PDF export: Add Around is clamped when a file opens, and art too large for f64 gets no effect image
document.rasterEffectsSettings accepts addAround from 0 to 1,000 pt,
but a file's raster_effects.addAround opened as it was saved. With
addAround 1e300, the area of a raster effect image overflowed f64,
the image scale became 0 and the image got the transform
[inf, NaN, NaN, inf, x, y]. PDF export left that image out, so the
drop shadow was missing.

A file's addAround now opens clamped to 0 to 1,000 pt. An effect
image whose area or scale is not finite is not made. Export then
keeps the object with its effects, and Expand Appearance expands it
without an image, as when its PNG can't be encoded.

Docs: docs/mcp.md (addAround).
2026-10-17 12:50:36 +07:00
dotCooCoo db2bbcf405 Images: sampling and recoloring check an image's RGBA size without overflowing
Before the Eyedropper samples an image, or Edit Colors, Recolor Artwork
or a color adjustment effect recolors an embedded one, `rgba_within` reads
the image's size from its header and checks its size as RGBA against the
allowance. The check multiplied width × height × 4 in 64 bits. A TIFF's
sides go up to 2^32 - 1 pixels, so a header of 2^31 × 2^31 pixels
overflowed that product: debug and test builds panicked with "attempt to
multiply with overflow", and release builds wrapped it to 0 before the
decoder's own 512 MB limit refused the image.

The check now compares width × height with the allowance divided by 4.
Every size whose RGBA bytes fit in 64 bits gets the same answer as
before, and a larger one is refused.
2026-10-17 12:48:19 +07:00
dotCooCoo 9bee299dd5 Render: an export no longer warns about images another thread could not decode
A raster export compared the process-wide count of images that could
not be decoded before and after it drew, and warned that "an image
couldn't be decoded and is missing from the picture" when the count
rose. Any other thread that failed to decode an image in that time
raised the count too: a second export, the UI thread drawing the
canvas, or the canvas, thumbnail and placed-document workers drawing a
document with such an image. The export then warned about an image it
does not contain. In the engine's tests,
options_not_applied_yet_and_document_features_warn failed in 5 of 60
runs of the tests_raster and tests_pdf* tests.

The count is now kept per thread. An export reads it on the thread it
draws on. That thread decodes every image the export draws that is not
already in the shared image store, and an image that failed to decode
is never stored.
2026-10-17 12:48:19 +07:00
dotCooCoo 0f987812b0 Render: check an image's RGBA size before decoding it
The renderer decoded each image within its 2 GB allowance (512 MB in
the web build) and then converted it to RGBA. The decoder holds the
decoded image, before conversion, to that allowance, so an image stored
with fewer than four bytes a pixel (gray, 1-bit, gray with alpha, or
RGB without alpha) could take up to four times as much (4/3 times for
RGB) once converted, and as much again as the pixmap drawn from it.

The renderer now reads the image's size from its header first. When
width × height × 4 is over the allowance it does not decode the image
and handles it as one it can't decode: the renderer draws nothing for
it, and a raster export warns that an image is missing. Images within
the allowance are decoded and drawn as before.
2026-10-17 12:48:19 +07:00
dotCooCoo 6b0d1ed1c0 Crop Image: trim checks the image's RGBA size before decoding it
Crop Image's trim (object.cropImage {trim: true}) decoded the image
within MAX_RASTER_ALLOC (2 GB) and then converted it to RGBA. The
decoder holds the decoded image, before conversion, to that allowance,
so a gray or 1-bit image within it could take up to four times as much
once converted.

Trim now reads the image's size from its header first and, when
width × height × 4 is over the allowance, fails before anything is
decoded, with the check and message Load Texture uses: "an image of
W × H pixels is too large to draw". Images within the allowance are
trimmed as before.
2026-10-17 12:48:19 +07:00
dotCooCoo 1d7ad173aa Fix #866: macOS save sheets have a Format menu again
On macOS, the save sheets of Save As, Save a Copy and the first Save of
a new document have a Format menu again, and the Save as Template sheet
now has one too. The panel is an NSSavePanel built in
apps/vectorcraft/src/mac_save_format.rs, and the menu is its accessory
view, set before beginSheetModalForWindow:completionHandler: shows the
sheet. The menu from #1235 was added after rfd had begun the sheet, and
recent macOS versions dismissed the sheet at once (#1800, #1831,
#1518); #1855 removed it.

Choosing a format changes the extension in the name field and puts
that format's extension first in the allowed file types, which AppKit
adds to a name typed without one. A name typed with another listed
extension is saved in that format. Save panels with one file type and
the open and folder panels are still shown by rfd.

Docs: docs/target-app-parity.md, docs/ui-parity.md.
2026-10-17 12:26:10 +07:00
dotCooCoo 35b9d5a645 Hide Edges: the hidden bounding box, type widget and corner widgets do not answer the pointer
With View › Hide Edges on, the canvas does not draw the bounding box,
the type widget beside it or the Live Corners widgets, but the
selection tools still answered the pointer on them. A press on a hidden
corner widget rounded the corners, a drag from a hidden handle scaled
the selection, a double-click on the hidden type widget converted the
type, and the cursor changed over each of them.

While edges are hidden, the app now tells the tools that the bounding
box and the corner widgets are off. A press at their places reaches the
art or the canvas under them.

Docs: docs/target-app-parity.md.
2026-10-16 16:07:22 +07:00
dotCooCoo 18b472092e Guides: Release Guides turns selected ruler guides into paths
View › Guides › Release Guides only cleared the guide flag on paths
made with Make Guides. Ruler guides could not become paths by any
route, and with only ruler guides selected the command released every
guide path in the document instead.

Release Guides now also releases the selected ruler guides. Each one
becomes an open straight path with no fill or stroke at the top of the
guide's layer: an artboard guide runs across its artboard, and a
canvas guide runs across the bounds of all the artboards. The ruler
guide is deleted, the released paths are selected, and the release is
one undo step. With no guide selected, every guide path is released,
as before.

view.guides.release takes index (a ruler guide) and ids (guide paths,
and the guide paths inside them). Both are refused while guides are
locked, and index takes only a guide on a shown, unlocked layer.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-16 16:07:21 +07:00
dotCooCoo 24583effe0 Paragraph: the Hyphenation dialog sets each paragraph's hyphenation limits
The Paragraph panel menu's Hyphenation… was always disabled, and
automatic hyphenation used fixed limits: words of 6 letters or more,
3 letters on each side of the hyphen, any number of hyphenated lines
in a row, and capitalized words hyphenated.

Each paragraph now has hyphenation options: Words Longer Than, After
First, Before Last, Hyphen Limit, Hyphenation Zone and Hyphenate
Capitalized Words. The defaults are the fixed limits, and documents
keep their line breaks. The options are saved only when they differ
from the defaults. text.setFormat takes wordsLongerThan, afterFirst,
beforeLast, hyphenLimit, hyphenationZone and hyphenateCapitalized, and
paragraph styles carry the options. Hyphen Limit applies with both
composers. Hyphenation Zone applies with the Single-line Composer: a
line whose text before the next word already ends within the zone
breaks before that word instead of hyphenating it.

Hyphenation… is enabled with type selected and opens a Hyphenation
dialog (ui.hyphenation) on the values the Paragraph panel shows. OK
runs text.setFormat with the values that changed, as one undo step.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-16 15:27:55 +07:00
dotCooCoo adea2d307d Print: Print and Print Preview warn about type that doesn't fit its frame
Area type and type on a path whose text doesn't fit print without the
text past the end of the frame. text.create, text.createInPath,
text.setText and text.setStyle report it in their replies, but
file.print and print.preview gave no warning, and the Print dialog's
Summary showed none.

Both commands now add a warning that counts the area type and type on
a path that prints and doesn't fit, and gives their ids (the first 20,
then how many more). It counts the objects that the fallback font
warning counts: visible objects on the layers that Print Layers picks,
on the paper of the artboards or tiles that print. The walk that finds
the fonts of the printed type also collects these ids. Type in the art
of symbols, patterns, brushes, opacity masks and placed documents is
not named.

Docs: docs/mcp.md (Print, Fitting area type), docs/target-app-parity.md
(Type).
2026-10-16 15:27:54 +07:00
dotCooCoo dfe215ca30 Effects: Effect › Path › Outline Object turns type into live, editable outlines
Effect › Path had Offset Path and Outline Stroke but not Outline Object.
The new effect (path.outlineObject) has no options: the Effect menu
applies it at once, and effect.apply refuses any parameter. It is a
geometry effect that returns its input. Type already reaches geometry
effects as its glyph outlines, so type that carries it is drawn,
exported (PDF and SVG as paths, with no font) and expanded as those
outlines, stays editable type, and the effects after it reshape the
outlines. Paths, compound paths and images draw as before, and no
bounds change: with Use Preview Bounds on, type is still measured by
its layout box.

Every complete catalog gets a row for the label.

Docs: docs/effects-parity.md, docs/target-app-parity.md (an evidence
line), docs/mcp.md (the effect, and what Expand Appearance does to
type).
2026-10-16 15:27:54 +07:00
dotCooCoo bc8e42d8c7 Export: the art's bounds take in the effects of groups, type and images
The bounds of the visible art (`vectorcraft_render::encode::art_bounds`)
are the crop of Export with Use Artboards off, EPS without artboards,
Export for Screens' Full Document and Save for Web (Legacy) with Clip to
Artboard off, and the size Fit to Artwork Bounds gives an artboard. They
measured a group or layer by its members and left out its own effects,
and measured type and images without their effects. A group's drop
shadow, or the shadow of a line of type, was cut off at the image's edge
and left outside the fitted artboard. They also counted a Shaper group's
transparent source art.

Each visible object is now measured by `vectorcraft_render::effect_bounds`,
the measure of Export Selection's crop. A group or layer without effects
of its own is still measured by its visible members, skipping a Shaper
group's source art. A group or layer with a drop shadow, a glow or another
raster effect is measured without its hidden members, guides and template
layers, unless it is a Shaper group or also has a geometry, Pathfinder,
Crop Marks or 3D effect or a visible effect on its own fill or stroke.
The SVG of a chosen artboard also keeps an object whose effects alone
reach the artboard.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-16 15:27:54 +07:00
dotCooCoo d973805269 Type: geometry effects on a type object's own fills and strokes are drawn and exported
A geometry effect applied to one of a type object's own fills or
strokes (effect.apply with item) was kept but not drawn or exported
unless the type also had a geometry effect or perspective of its own.
With a geometry effect, the renderer measured the type for culling
without the fill's or stroke's effects, so an outline that reached past
the glyphs was not drawn once the glyphs left the view.

Type whose own fills or strokes have a visible geometry effect is now
drawn, culled and exported through its outlines, the same way as type
with an object-level geometry effect. Each effect reshapes the whole
outline that its fill or stroke paints. SVG, PDF, EPS and the other
vector exports write such type as paths. Type that holds inline
graphics stays type, as before.
2026-10-16 15:27:54 +07:00
dotCooCoo 56acb8d3b6 Select: Update Selection replaces the recalled saved selection with the current selection
The Select menu had no Update Selection: to add objects to a saved
selection or take objects out of it, the selection had to be saved
again under the same name. Select › Update Selection (`select.update`)
now gives the saved selection last recalled the objects that are
selected, in one undo step. It is enabled while objects are selected
and the document still holds that saved selection.

Each open document keeps session ids for its saved selections, so the
recalled one is found under a new name after Edit Selection…, and after
Undo and Redo. Deleting it disables Update Selection until another
saved selection is recalled or the delete is undone.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-16 15:27:54 +07:00
dotCooCoo 917dcdeeef Simplify: add Corner Point Angle Threshold, Convert to Straight Lines and Show Original Path
The Simplify dialog had Tolerance, Preview and the anchor point counts.
It now also has three of the options of the reference app's advanced
Simplify dialog:

- Corner Point Angle Threshold is a slider from 0° to 180° that opens
  at 150°, with its label above it so the dialog stays under the
  420-point width that menu_parameter_dialogs_are_compact allows. An
  anchor whose corner is sharper than the threshold stays a corner, so
  moving the slider right keeps more corners. The dialog sends
  object.path.simplify's cornerAngle as 180° less the threshold, so the
  opening value gives the same result as before.
- Convert to Straight Lines sends straightLines.
- While Preview is on, Show Original Path draws the paths as they were
  before the preview, in a faint band of their layer's color under the
  selection outlines.

The three labels have rows in the 19 catalogs that translate every
label.

Docs: docs/target-app-parity.md, docs/ui-parity.md.
2026-10-16 15:27:53 +07:00
dotCooCoo 25d348f391 Rotate View: fitting, the Zoom marquee, canvas guides and the Navigator follow a turned view
In a rotated view, seven parts of the canvas and the Navigator worked as
if the view were not turned. Fit Artboard in Window and Fit All in
Window sized the zoom from the unturned width and height, so at 45° a
612 x 792 pt artboard ran off a 1000 x 800 canvas. The Zoom tool's
marquee (Animated Zoom or GPU Performance off) set the zoom to 3.13% at
90° and 180°. Ruler guides on no artboard were drawn square to the
screen while the tools pick them, snap to them and move them by their
position in the document. The rectangle a tool drags out (the selection
marquee, and the drags of the Type tool, the Artboard tool, the Place
cursor and Rectangle Cut) was drawn as a box square to the screen while
the tools work in the document rectangle. Show Hidden Characters skipped
type whose turned outline reached into the canvas when the box between
two of its corners did not. At 180° the Contextual Task Bar covered the
selection, and the Navigator's red box outlined an area that the window
did not show.

Now fitting uses the size of the artboard or art turned with the view,
and the Zoom tool's marquee zooms by its size on screen and centers on
its middle. A canvas guide is drawn along its document line, cut to the
canvas. Rectangles that tools drag out are drawn through the screen
corners of the document rectangle the tools use. Show Hidden Characters
uses the type's turned outline on screen, and the task bar uses the
selection's. The Navigator draws the canvas's corners mapped into the
document. At 0° each of these does what it did before.

Docs: docs/target-app-parity.md.
2026-10-16 15:27:53 +07:00
dotCooCoo ddfd08f321 Blob Brush: Keep selected and Merge only with selection options
A Blob Brush stroke merged with every touching closed path of its
color that had no stroke, and the blob it drew or merged into was
always left selected. The Blob Brush Tool Options had Size only.

The Blob Brush Tool Options now have Keep selected and Merge only
with selection, both off by default. A blob that a stroke draws or
merges into is no longer left selected unless Keep selected is on,
and the rest of the selection stays as it was. While Merge only with
selection is on, a stroke merges only with selected blobs, and with
nothing selected it draws a new blob. The options persist like the
tool's Size, and path.blob takes keepSelected and mergeOnlySelection,
both false by default. Keep selected stays on by default for the
Pencil and Paintbrush.

Docs: docs/target-app-parity.md, docs/control-protocol.md.
2026-10-16 15:27:53 +07:00
dotCooCoo f0d3f81f3b Canvas: a right-click on locked objects offers Unlock with each object's name
A right-click on the canvas now records the locked objects under the
pointer with vectorcraft_doc::hit::locked_objects_at, topmost first and
at most 20. A locked group is listed instead of the objects it holds.
Hidden objects and objects on hidden, locked or template layers are not
listed, and in isolation mode only the isolated group's contents are
listed. While the menu is open, it has an Unlock submenu after Undo
and Redo that lists each of them by the name the Layers panel shows.
Choosing a name runs layer.setProps {ids: [id], locked: false}, as the
Layers panel's lock column does, in one undo step. ui.contextMenu.list
shows the same items.

Docs: docs/control-protocol.md, docs/target-app-parity.md.
2026-10-16 15:27:53 +07:00