Commit Graph
622 Commits
Author SHA1 Message Date
dotCooCoo e4a3adcea0 Shape Builder: a Shift-drag draws a marquee and merges every region it reaches
A Shape Builder drag merged only the regions the pointer passed over,
and the tool did not read Shift. The reference app's Shape Builder page
describes a Shift-drag that draws a rectangular marquee to merge many
paths at once.

With Shift held at the press, the drag now draws a rectangular marquee
from where it started to the pointer. The regions it reaches are shaded
while it is drawn and merge into one shape on release; with Alt also
held at the press, they are deleted. The marquee reaches each region
whose outline crosses it or has an anchor inside it, and the region that
holds the whole marquee. A Shift-click that does not move is a click,
and a drag without Shift works as before. With In Merge Mode, Clicking
Stroke Splits the Path on, a Shift-click on a stroke splits it as a
click does; a marquee splits no stroke, and while it is drawn the
pointer does not show scissors and no stroke is highlighted.

shapeBuilder.merge takes the marquee as rect: [x, y, width, height],
where x and y are where the drag started, and points may then be left
out. For Pick Color From Artwork, the corner where the drag started
counts as the drag's first point, and the opposite corner counts as its
last. With rect, splitStroke does not split a stroke.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-23 06:51:20 +07:00
dotCooCoo 6da6b4440f Shape Builder: a split stroke keeps its path's direction and gradients
With In Merge Mode, Clicking Stroke Splits the Path on, the run a click
split off could run against its path. A click on a rectangle's top side
gave a side from (100, 0) to (0, 0) on a rectangle that runs from
(0, 0) to (100, 0) there, so arrowheads and other settings that follow
a stroke's direction moved to the other end. The run now goes the way
the path goes.

The two paths of a split, and what an Alt-click or a merge leaves of a
path, had a gradient fitted to the path's bounds fitted again to their
own bounds. They now keep it where it was on the path.

A run that left 0.05 pt or less of a path still split it, into the
run and a speck. Such a path now does not split, and the tool shows
no scissors over it.

Docs: docs/mcp.md (Shape Builder Tool Options).
2026-10-23 04:14:38 +07:00
bflatastic efe2429618 Shape Builder: in merge mode, a click on a stroke splits the path there
Squashed from #3665 onto main.
2026-10-23 01:53:34 +07:00
Taha-Mahmoodi 6beb68f934 Gradients: the Gradient panel's Method menu and paint.editGradient's method choose Classic or Perceptual
Illustrator 30.0's Method choice in the Gradient panel
(https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-gradients/control-perceptual-interpolation-in-gradients.html).

- paint.editGradient takes `method: classic|perceptual` (one undo step);
  paint.setFill/setStroke take it in `gradient`, and gradient params (swatches,
  copied paints) carry it when Perceptual. Anything else is refused without
  editing. One edit can set `method` and `dither` together.
- The Gradient panel shows a Method menu (Classic, Perceptual) for linear and
  radial gradients, dimmed without a gradient; the Dither box stays under the
  stops.
- i18n: "Perceptual" where a complete catalog lacked it, and the `method`
  error in all 24 catalogs.
- Docs: G13 (estimate 14–24 h → 13–23 h, after Dither's 15–25 h → 14–24 h),
  target-app-parity (row and evidence), ui-parity, mcp.md, plugins.md.
2026-10-23 01:53:30 +07:00
dotCooCoo 44c306f142 Artboards: an action or Batch… that reverts or switches documents keeps the active artboard
With the Artboard tool, File › Revert kept the active artboard, but a
command.batch with a file.revert step made artboard 1 the active one.
A batch that switched to another document and back, and the Actions
panel menu's Batch… over a folder whose files it closes or over data
sets with a Revert step, did the same. Each of them starts the tool
afresh and ends in the document it started in, and the app moved the
view to the fresh tool's artboard 1. Only a plain file.revert was
handled, by its command id.

The engine now counts the times switching or closing documents, or
replacing the active one, starts the tool afresh (Session::tool_resets),
and records whether the tool's options were set after that
(Session::tool_set_since_reset). When the count changes during a
command and nothing set the tool's options after it, the app starts
the tool on the active artboard, as it does when a command chooses
another document. When a later step chose the tool's artboards (a
Paste or Cut after the revert), the artboard the tool then has active
becomes the active one.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-23 00:20:54 +07:00
dotCooCoo fffc8418d9 Shape Builder: Pick Color From Artwork takes the look where the drag starts, else where it ends
With Pick Color From Artwork, a Shape Builder merge took the look of
the front-most object over the first covered region the drag touched,
and the current fill when the drag touched only areas no object
covers. The reference app's combine page gives the look of the object
where the drag starts, else of the object where it ends, else of the
topmost selected object.

shapeBuilder.merge now follows that rule: the merged shape takes the
appearance, opacity and blending mode of the front-most of the objects
it is given (the selection by default) under the first point, else of
the one under the last point, else of the front-most of those objects.
A drag from a hole across a gray bar into a red square now gives a red
shape, where it gave the bar's look. A click in an area no selected
object covers, such as a hole that gray bars enclose or a triangle
that lines enclose, takes the front-most selected object's look
instead of the current fill. Pick Color From Color Swatches works as
before. The tool already sends the press point first and the release
point last.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-22 23:56:01 +07:00
Taha-Mahmoodi cc1a660a6d Gradients: Dither in the Gradient panel draws a gradient fill with fine noise that hides banding
Illustrator 30.0's gradient Dither, from the public documentation
(https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-gradients/control-dither-in-gradients.html):
a Dither checkbox in the Gradient panel; dither converts the gradient into
pixels with subtle noise, works with gradient fills only, and follows the
Document Raster Effects resolution.

- Gradient gains `dither` (serde default false, written only when on, so
  older files open unchanged and new files keep it).
- The renderer draws a dithered linear or radial fill as an image paint: the
  gradient sampled a cell per device pixel or finer (quarter-octave steps,
  cached per thread), each channel given seeded triangular noise of up to a
  level before rounding. Same pixels every run; strokes and undithered fills
  draw exactly as before. PNG and other raster exports go through it.
- SVG and PDF write a dithered fill as an image at the document's raster
  effects resolution, clipped to the shape (the freeform-gradient path);
  characters' fills stay vector gradients in SVG, with a warning.
- paint.editGradient and paint.setFill take `dither`; strokes refuse it.
  The Gradient panel's Dither box is dimmed behind the Stroke proxy.
- i18n rows in all 24 catalogs; docs: G13, target-app-parity, ui-parity,
  mcp.md, plugins.md.
2026-10-22 22:38:42 +07:00
dotCooCoo 781e62b9cc Place: Link keeps a placed PDF or .ai page linked, as it does images
The File Handling preference Link Placed PDF and .ai Files
(linkPlacedPdf) is removed. File › Place with Link now places a page of
a PDF, .ai or .ait file read as PDF pages as a linked placed document,
as it links a raster image, and link: false embeds the page. Link is on
by default in the Place dialog and in file.place, so a PDF or .ai page
placed from a file, which used to be embedded, is now linked unless
Link is off. A password or textAs: "outlines" still embeds, and so do
EPS and PostScript .ai files.

The Place dialog's Link tooltip describes a PDF or .ai file as a
document.

A .ai file read from its editing data (its PDF part only a placeholder)
finds the files its art places by name, beside it. Package now copies
those files into the same folder as the .ai file, and warns when one has
to be copied under another name.
2026-10-22 21:52:23 +07:00
dotCooCoo 3a1d407f1a Place: a placed PDF or .ai can stay linked to its file (preference, off by default)
File › Place with Link kept a link only to raster images and VectorCraft
documents. A PDF, .ai or .ait page was always embedded as a group, so
Update Links never saw a newer version of the file.

A new preference, File Handling › Link Placed PDF and .ai Files
(linkPlacedPdf), is off by default. With it on, Link places the page as
a placed document, as it places a VectorCraft document: the object
keeps the file's bytes, a preview, the page number and the page's box
(crop, art, trim, bleed or media, stored as page_box). Links check,
update and relink it, Break Link and Object › Expand give the group
placing it without Link gives, every output draws the page's vectors,
and saving and reopening keep the link. With the preference off, or
with a password or textAs: "outlines", the page is embedded as before.
EPS and PostScript .ai files are still embedded.

Package copies a placed PDF or .ai file as it is. The Place dialog's
Link tooltip describes a PDF or .ai file as a document while the
preference is on. Relinking a placed document to a file that is not a
VectorCraft document, a PDF or a PDF-compatible .ai file says so.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-22 21:52:23 +07:00
bflatastic 1b7cfc8bee Automation: --automation-read-root and --automation-write-root may each be given more than once (#3546)
Squashed from #3631 onto main.
2026-10-22 21:36:15 +07:00
Taha-Mahmoodi 87fdd4a370 CLI: run --commands reads the steps from a JSON file or standard input (#3147) 2026-10-22 21:34:37 +07:00
Taha-Mahmoodi 648a8f5104 Fix #3545: draw_path and draw_shape with a paint the engine refuses leave the document unchanged
The MCP drawing tools ran the creating command (path.create, shape.*)
and only then paint.setFill, paint.setStroke, stroke.set,
object.setProps and effect.apply, each as a command of its own.
check_draw_options checked dash, cap, join, opacity, name and effects up
front, but not the paints: a fill or stroke the engine can't read
(stroke: "currentColor"), or a swatch the document lacks, failed after
the object was made, so the call returned an error and left the path,
its undo step and its SVG node behind. Each styling command was also an
undo step of its own.

The tools now send the creating command and the styling commands as one
command.batch (crates/mcp/src/tools.rs, draw): the engine rolls the
whole batch back when a step fails, so a refused paint leaves no object,
no undo step and nothing in the SVG, and a good call is one undo step
named as the creating command names its own (Pen, Rectangle, …). The
styling steps act on the new object, which the creating command selects,
with item: null (its whole appearance), as the ids did before. The
reply gives the failed step's own error, without the batch's prefix.
docs/mcp.md says so.

Test: tests_drawopts::a_bad_paint_draws_nothing (invalid fill, stroke,
a missing swatch and a non-numeric strokeWidth on draw_path and
draw_shape: error, node count, undo history and SVG export unchanged;
a good call adds one undo step named Pen / Rectangle). It fails without
the change (draw_path with stroke "currentColor" drew a path) and passes
with it; the other draw option and command_batch tests still pass.
2026-10-22 18:56:32 +07:00
dotCooCoo 5d4cac31cf Artboards: the active artboard follows the Artboard tool after Paste, Cut and tool.setOption
With the Artboard tool, Paste and Paste in Place select the pasted
artboards, Cut gives the tool the artboard before the first cut one
(the new first artboard when artboard 1 is cut), and tool.setOption
sets its selected or active artboard. The active artboard, the view's,
stayed where it was, so the navigator showed another artboard than the
tool and the Artboards panel. document.activate and File › Revert
started the tool on artboard 1, while choosing a document's tab starts
it on that document's active artboard.

After a command that changes the artboards the Artboard tool has
selected, the app now makes the tool's active artboard the active one,
as a click with the tool does. After a command that chooses another
document, and after Revert, the tool starts on the document's active
artboard.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-22 16:59:38 +07:00
dotCooCoo de1182fbe3 Library panels: Locate Library… reads a missing library whose panel isn't persistent
ui.libraryPanel.locate decided whether a panel's library was missing
through missing_file, which answers only for a persistent panel. A
command that turns Persistent off leaves the panel open on its missing
library until the next drawn frame closes it, and while the window is
hidden the app draws no frame between control requests. Locate
Library… on that panel answered "isn't missing its library" and read
no file.

The check now asks whether the panel's library can be found, the
lookup the tab label and the per-frame tidy use. A panel whose library
is there is still refused, persistent or not.

window.swatchLibrary and window.graphicStyleLibrary had the same
check: given the library id of such a panel, they answered "no
library". They now bring the open panel forward and answer its file's
name with count 0; the panel still closes at the next tidy.

Docs: docs/mcp.md (ui.libraryPanel.locate, window.swatchLibrary).
2026-10-22 11:34:47 +07:00
dotCooCoo bfc3f5251a Commands: path.create with path data refuses closed given as text, and the grids refuse a count
path.create read closed only when it drew from anchors. Given path
data, {"d": "M0 0 L10 0 L10 10", "closed": "true"} drew the path and
recorded an undo step, while the same closed with anchors fails with
"`closed` must be true or false". shape.rectangularGrid and
shape.polarGrid read rows, columns, concentric and radial given as text
as the count left out, so {"rows": "3"} drew the default 5 horizontal
dividers instead of 3.

path.create now checks closed before it reads d or anchors, and the
grid commands refuse such a count with "`rows` must be a number, not
"3"". Neither command changes the document or its undo history then.
With d, a closed of true, false or null still leaves the path as its
path data draws it.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-22 08:24:09 +07:00
dotCooCoo 17fbf6ad6a Library panels: Locate Library… refuses a panel whose library is there
ui.libraryPanel.locate checked only that the panel was open. Run on an
open panel whose library was there, such as the built-in Pastels, it
loaded the file given or picked and showed that library in the panel
in place of Pastels.

It now answers "library panel `{given}` isn't missing its library" for
such a panel, before it asks for a file or loads one, and the panel
keeps its library. A persistent panel whose library's file is gone
locates as before.

Docs: docs/mcp.md (ui.libraryPanel.locate).
2026-10-22 07:03:20 +07:00
dotCooCoo caa6962508 Commands: Transform Each, artboards and more refuse a boolean or number given as text
object.transformEach refused reflectX, reflectY, random and copy given
as text, but read strokes, corners, objects and patterns given as text
as the key left out. With Scale Strokes & Effects on,
{"scaleH": 200, "scaleV": 200, "strokes": "false"} scaled the stroke,
recorded an undo step and kept the params for Transform Again, which
repeated the call. artboard.rearrange, artboard.move and
artboard.duplicate read their patterns option the same way, and
artboard.setProps reads its patterns, strokes and corners options the
same way.

These commands now refuse such a value with "`<key>` must be true or
false" before they change anything. So do the other options and
numbers of artboard.setProps and artboard.move, the art of Duplicate,
Copy and Cut Artboard, the shape commands and path.create, path.close,
path.setHandle, path.moveAnchors, the Pencil, Curvature, Reshape
Segment and Blob Brush options, text.create, text.createInPath,
text.setRangeStyle, text.editRange, text.getRange, text.touchType,
text.areaOptions, type.step, the font size steps,
type.opticalMarginAlignment, type.systemLayout, type.fractionalWidths,
type.ruby, the symbol and swatch commands, the Appearance panel's
commands, the gradient and freeform gradient commands, Shape Builder,
Live Paint, Image Trace, the opacity of object.setProps, the radius,
sides and relativeRadii of object.setLiveShape, the reference point of
object.setBounds and the artboard of Align, Distribute and Distribute
Spacing. They refuse a number given as text with "`<key>` must be a
number, not <value>", and a gradient stop's tint with "stop <n>:
`tint` must be a number". A null still reads as the key left out.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-22 06:40:27 +07:00
dotCooCoo e66bca39b2 Export: legacy .ai gives the size note for an image with empty data
An image whose data is empty failed to decode before the size check, so a
version 7 .ai said it couldn't be decoded. The writer now checks for empty
data first and gives "an image that is empty or too large for legacy .ai
was left out". docs/mcp.md now says the DXF decode note applies to R13 and
later; R12 leaves out every image with its own note.
2026-10-22 06:13:32 +07:00
dotCooCoo 4e4723699d Export: EPS, legacy .ai, EMF/WMF and DXF give one note for images that can't be decoded
EPS, EMF, WMF and DXF exports gave each image they left out because it
couldn't be decoded its own warning, "image '<key>' could not be
decoded and was left out", which names the internal key the document
stores the image under. A version 7 .ai said "an image that can't be
decoded or is too large was left out".

The four writers now give the note raster exports, PDF and the legacy
.ai flattening give, "an image couldn't be decoded and is missing from
the picture", once per export. In a version 7 .ai it is one line with
the note for flattened art. The other image notes in the same
functions no longer name the key: EPS's note for an image too large to
write, and DXF's notes for an image with no pixels (its rows in 23
catalogs are reworded) and one that could not be written. Legacy .ai
gives a separate note for an image that is empty or too large.

Docs: docs/mcp.md (EPS, DXF export, EMF and WMF, aiVersion "7").
2026-10-22 06:13:32 +07:00
dotCooCoo 5f6c7b8da9 PDF export: a 16-bit PNG is written at 8 bits for PDF 1.3 and 1.4 instead of failing the export
When the Compression settings keep a PNG as it is, the PNG goes to
the PDF writer unchanged, and the writer refuses images of 16 bits a
sample below PDF 1.5. Saving a document with a placed 16-bit PNG as
PDF 1.4, or as a .ai file at 1.4, failed with "PDF writer error:
SixteenBitImage(...)" and wrote nothing. PDF 1.3 and PDF/X-3 failed
the same way for an opaque 16-bit PNG, and PDF/X-1a failed for a
16-bit gray PNG.

In PDF 1.3 and 1.4 files, such a PNG is now decoded and encoded again
at 8 bits a sample, with the same color type and ICC profile, before
the writer reads it. From PDF 1.5 on, it keeps its 16 bits. A 16-bit
PNG whose pixel data can't be decoded is left out with the note "an
image couldn't be decoded and is missing from the picture".

Docs: docs/mcp.md (PDF options).
2026-10-22 05:57:11 +07:00
bflatastic 34cde7c3db Character: Character Direction sets the selected characters left to right or right to left
Squashed from #3436 onto main.
2026-10-22 04:18:35 +07:00
dotCooCoo 4bd55029e8 Live Paint: the Live Paint tools and livePaint.fill skip hidden faces
A face hidden on its own (for example with Object > Hide after Live
Paint Selection picks it) while its Live Paint group stays visible was
still highlighted, selected by Live Paint Selection and filled by the
Live Paint Bucket, and it kept the face of a group behind it from being
picked.

live_paint_at and lp_part_at now take the front-most face that is not
hidden (visible_face_at). A point on a hidden face falls through to the
visible face of a group behind it; where there is none, an edge pick
takes the nearest visible edge as it does outside the faces.
livePaint.fill also skips hidden faces. With group, a point on a hidden
face is an error. With ids, it fills the face there of another Live
Paint group among the objects, or else fails and does not merge the
group.

Docs: docs/mcp.md.
2026-10-22 02:01:58 +07:00
bflatastic 62776d5f46 Recolor: recolor.apply takes a model option for the new colour's model (#2170)
Squashed from #2520 onto main.
2026-10-22 02:01:57 +07:00
bflatastic bbe026607f Variables: Load Variable Library… reads a variable library's text variables and data sets
Squashed from #2853 onto main.
2026-10-22 00:12:39 +07:00
bflatastic e7cdf154ca Commands: a boolean or number given as text is refused
Squashed from #3428 onto main.
2026-10-22 00:10:57 +07:00
bflatastic 2cd7f156fb Add lighting presets to 3D effects
Squashed from #3442 onto main.
2026-10-22 00:01:37 +07:00
dotCooCoo 4d1581f3da Document Info: a Repeat Objects category lists the document's repeats
Document Info had no Repeat Objects category, and document.info
refused category "repeats". The reference app's Document Info panel
menu lists Repeat Objects after Font Details.

Document Info now has a Repeat Objects category after Embedded Images.
It lists each radial, grid and mirror Repeat object by name with its
kind and the number of instances it makes. For a grid it gives the
rows and columns instead, followed by the number of instances it
draws when that is fewer. The count comes from the new
RepeatSpec::instances, which gives the number of instances a repeat
draws. That number is smaller than its options give when repeats
nested in its source would pass the instance budget. With Selection
Only the category lists the Repeat objects in the selection.
document.info gives them as repeatObjects, the panel's flyout offers
the category, and the text report that Document Info › Save… and
File › Package write has a REPEAT OBJECTS section. The title has a row
in every catalog.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-22 00:01:36 +07:00
bflatastic e5471be4d9 Commands: an index of 2^32 or more no longer wraps around on 32-bit builds
Squashed from #3292 onto main.
2026-10-21 23:38:03 +07:00
bflatastic 58066428c6 Artboards: the Artboards panel locks an artboard and the art on it
Squashed from #2592 onto main.
2026-10-21 23:38:02 +07:00
bflatastic 9c20027778 Keyboard Shortcuts: each Align and Distribute button of the Align panel can be given a key
Squashed from #3317 onto main.
2026-10-21 23:38:02 +07:00
bflatastic 5e24e59bd3 Brushes: New Brush and Bristle Brush Options take a bristle brush name of at most 31 characters
Squashed from #3312 onto main.
2026-10-21 23:38:01 +07:00
dotCooCoo 7d58ce4f9d Place: Word's low, medium and high kashida justification sets Insert Kashidas
A Word paragraph justified with w:jc lowKashida, mediumKashida or
highKashida placed as plain justified type, and its kashida length was
dropped. ECMA-376 Part 1, 17.18.44 (ST_Jc) gives these values a
slightly longer, a medium and the widest kashida length.

Such a paragraph now places justified with Insert Kashidas set to
Short, Medium or Long. A paragraph justified with both, and every other
alignment, still places with Insert Kashidas set to None. VectorCraft
writes no Word or RTF files, so this change does not affect export.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-21 23:32:26 +07:00
dotCooCoo 6807e757af PDF export: PDF and .ai files carry File Info's copyright status, notice and URL
The PDF writer's metadata has no copyright fields, so the XMP packet of
PDF exports and PDF-compatible .ai and .ait saves left out File Info's
copyright status, notice and URL. A PDF exported without its editing
data opened with the status Unknown and an empty notice and URL,
although the reader takes them from dc:rights, xmpRights:WebStatement
and xmpRights:Marked.

The export now adds those three properties to the XMP packet after
the writer has written it, as it does for the modification date.
Marked is True for Copyrighted and False for Public Domain, and is left
out for Unknown. The notice and URL are escaped for XML, and characters
XML 1.0 does not allow are left out. A document without a copyright
gets none of the properties.

Docs: docs/mcp.md (File Info), docs/target-app-parity.md.
2026-10-21 23:32:26 +07:00
Midasdf 32d012f6cc Print: Japanese paper sizes (JIS B4/B5/B6, はがき・封筒・四六判)
The Print dialog's Media list only had Letter/Legal/Tabloid and the ISO A
and B papers, so Japanese papers had to be entered as a custom size, and
"B4"/"B5" were ISO 216 (250 × 353 and 176 × 250 mm) although Japanese
printing uses the JIS sizes (257 × 364 and 182 × 257 mm, JIS X 6310).

Media gains JIS B4, B5 and B6 next to the ISO ones, and はがき
(japanese postcard, 100 × 148 mm), 往復はがき (japanese double postcard,
148 × 200 mm), 封筒 長形3号/長形4号 (japanese envelope #3/#4, 120 × 235
and 90 × 225 mm), 角形2号/角形3号 (kaku #2/#3, 240 × 332 and 216 × 277 mm),
四六判 (yonroku-ban, 127 × 188 mm) and A6, all in millimetres at 72/25.4.
b4 and b5 keep their ISO 216 dimensions so print settings and presets
saved before the Japanese papers existed keep their paper; the dialog
labels say "B4 (ISO)"/"B5 (ISO)" beside "B4 (JIS)"/"B5 (JIS)".

A paper is named by its JSON id or by its dialog label, any case and
spacing ("b5 (jis)", "B5 (JIS)" and "b5jis" all mean the JIS B5), so
agents and the control channel need not know the id spelling; an unknown
name is still refused. Media is written out instead of through the
choice! macro to carry that lookup.

The print.setup/file.print parameter docs and docs/mcp.md list the new
papers; the Print dialog picks them up from Choice::ALL with no UI code
change, and the Japanese labels are translated in ja.tsv (はがき,
往復はがき, 長形3号, 長形4号, 角形2号, 角形3号, 四六判).

Tests: japanese_paper_sizes checks every added size in points, that the
ISO B4/B5 values did not change, name resolution and id/label round
trips, and that each paper prints a PDF page with its own MediaBox size.

Validation: cargo fmt --all -- --check clean; cargo clippy
-p vectorcraft-pdf --all-targets -- -D warnings clean; cargo test
-p vectorcraft-pdf -> 330 passed, 0 failed (328 lib + 2 integration).
2026-10-21 23:32:26 +07:00
dotCooCoo 7d6eb3246e Type: Optical Margin Alignment hangs punctuation in right-to-left paragraphs
Optical Margin Alignment and Roman Hanging Punctuation left every
right-to-left paragraph of point and area type, and type on a path
whose first paragraph runs right to left, as it was: the quotation
marks, full stops and commas at the ends of its lines stayed inside
the margins.

A right-to-left line now hangs at the margins its alignment places it
against, as a left-to-right line does. Auto aligns a right-to-left
line at its right, so it hangs there. The line is read from right to
left: where it hangs at its left margin, a full stop and the quotation
mark after it that end the line hang together. In vertical type,
where the characters of a right-to-left paragraph run from the top
down, the pair is read from the top.

Type on a path whose first paragraph runs right to left hangs at its
brackets by the same rule. In horizontal type, Auto places it at the
end bracket, so it hangs there. On a closed path, its hang at the end
bracket gets the space between the end and start brackets before its
hang at the start bracket does, as in right-aligned type.

A justified right-to-left line of one word is spread between its
letters as they are drawn. The gap after each letter went to the
letter that follows it in the text, so the two leftmost letters
touched and the word ended one gap short of the right margin.

Docs: docs/mcp.md, docs/type-parity.md, docs/target-app-parity.md.
2026-10-21 23:15:07 +07:00
bflatastic b008e83658 Brushes: New Brush asks for the type and opens its Brush Options
Squashed from #3390 onto main.
2026-10-21 23:15:05 +07:00
dotCooCoo 7aa25698d2 Brushes: dragging a brush reorders it within its type
The reference app reorders a brush that is dragged to a new place in
its Brushes panel, among the brushes of its type only. VectorCraft's
Brushes panel could not reorder brushes, and no command moved one.

brush.move {name | names, to?, type?} moves brushes to index to among
the brushes of their type, in brush.list order, as one undo step. A
move that keeps the order records no step. Brushes of different types,
or a type other than theirs, are refused with "brushes move only within
their type". In the Brushes panel, a brush dragged onto another brush
of its type goes before or after it by the half of the tile or row
under the pointer, and a bar marks the slot while it is held there. A
brush dropped on a brush of another type stays where it is, and the
status bar shows the refusal.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-21 23:14:33 +07:00
dotCooCoo b4fef5dc98 Library panels: a persistent panel whose file is gone stays listed as missing
At startup, a persistent swatch or graphic style library panel whose
file could not be read was closed and left out of the preferences,
with only a status-bar note. The next preferences save dropped it, so
moving or renaming the file once lost the panel and its settings.

Such a panel now reopens where the saved layout had it and stays in
the preferences. Its tab shows the file's name without the extension,
and its body shows "Missing:" with the file's path and a Locate
Library… button. The new command ui.libraryPanel.locate {panel,
path?} loads the library from the file given, or from one picked in
an open dialog, and shows it in the same panel, which keeps its view,
Show Find Field and Persistent. When the file's library is open in
another panel, the missing panel closes and that panel is brought
forward and made persistent.

A persistent panel whose User Defined library file is deleted while
the app runs shows the library as missing the same way; other panels
still close. window.swatchLibrary and window.graphicStyleLibrary take
a missing library's id, so a closed missing panel opens again.
Previous Library and Next Library are dimmed in a missing panel's
menu.

Docs: docs/mcp.md, docs/ui-parity.md, docs/target-app-parity.md.
2026-10-21 23:14:32 +07:00
dotCooCoo bac132d8d4 Docs: the document.open pages sentence names the file saved without PDF compatibility
docs/mcp.md said that `pages` that name every page "of such a file"
open it from its editing data. The sentence before it is about a file
without editing data, so "such a file" could be read as that file.

The sentence now says "a file saved without PDF compatibility".

Docs: docs/mcp.md.
2026-10-21 23:14:23 +07:00
bflatastic 30203e60a1 PDF/.ai import: a .ai saved without PDF compatibility is placed and pasted from its editing data
Squashed from #2182 onto main.
2026-10-21 17:11:08 +07:00
dotCooCoo 1267efbcf9 Use Preview Bounds: objects are measured with their geometry effects
With Use Preview Bounds on, the bounding box, the Transform panel,
Align, Distribute, Distribute Spacing and object.setBounds measured
each object without its geometry effects. A square with Offset Path
10 measured as the square, and type with Outline Object measured by
its layout box instead of its glyphs.

The new vectorcraft_effects::preview module measures objects as they
are drawn. A path is measured by its effected shape, with the
geometry effects of each fill and stroke that paints. Type, images,
live objects and symbol instances are measured by their reshaped art.
A group or layer with geometry or Pathfinder effects, or with a fill
or stroke of its own that has geometry effects or paints members with
geometry effects, is measured by its evaluated art. A blend, envelope
or repeat whose objects have geometry effects is measured by its
evaluated art, with those objects measured after their effects. Raster
effects are not measured. In one object, the art of at most 4,096 symbol
instances is measured with its geometry effects, and the other
instances are measured without them. With Use Preview Bounds off, the
measures are unchanged.

object.setBounds sizes objects with geometry effects by measuring
them after each scale it tries, so type with Outline Object set to
100 pt wide gets a glyph box 100 pt wide.

Selection::anchors_box is split out of Selection::anchor_bounds. The
doc comment of VectorcraftApp::selection_bounds said that it includes
effects; it now says that it leaves them out.
2026-10-21 17:11:08 +07:00
bflatastic dda52d0b20 Artboards: each artboard can have a background color on the canvas
Squashed from #2403 onto main.
2026-10-21 17:11:08 +07:00
dotCooCoo e5b1f0c191 Shape Builder: double-clicking the tool opens Shape Builder Tool Options
Double-clicking the Shape Builder tool reported that the tool has no
options. The reference app opens Shape Builder Tool Options there.

The dialog sets Consider Open Filled Paths as Closed, Pick Color From
(Artwork or Color Swatches), Cursor Swatch Preview, Selection (Freeform
or Straight Line) and Highlight Fill. The defaults keep today's
behavior. With Consider Open Filled Paths as Closed off, a filled open
path cuts regions as an unfilled one does. With Color Swatches, a merge
fills the new shape with the current fill, and the Left and Right arrow
keys step the current fill through the document's color swatches.
Cursor Swatch Preview shows the current fill and the swatches before
and after it above the pointer. With Straight Line, a drag merges what
the line from its first point to its last crosses. With Highlight Fill
off, merge mode shades no regions.

The options persist like other tools' options. shapeBuilder.merge
takes openFilledAsClosed and pickColorFrom, and shapeBuilder.regions
takes openFilledAsClosed. Gap Detection, "In Merge Mode, Clicking
Stroke Splits the Path" and Highlight Stroke When Editable are not part
of this change.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-21 17:11:07 +07:00
dotCooCoo bdf54007a0 Isolation mode: object.isolate refuses hidden or locked objects and locked guide paths
A click with the Selection tool does not select a hidden or locked
object, an object in a hidden or locked group or layer, or a guide path
while Lock Guides is on, but object.isolate isolated each of them. Once
a locked or hidden group, or a group on a locked or hidden layer, was
isolated, a Selection tool drag moved the member under the pointer. The
Layers panel's Enter Isolation Mode sent the same command for a locked
or hidden layer or group.

object.isolate now refuses a hidden or locked object, or one in a
hidden or locked group or layer, with "the object is hidden or locked"
(the text of layer.target), and a guide path while Lock Guides is on
with "guides are locked". Isolation mode stays off, and the selection
stays as it is.

Docs: docs/mcp.md.
2026-10-21 16:46:57 +07:00
dotCooCoo 0701340720 Layers: Cmd+Alt+L opens Layer Options for a new layer
The reference app adds a layer while opening the New Layer dialog with
Ctrl+Alt+L on Windows and Option+Command+L on macOS. VectorCraft opened
that dialog from an Alt-click on the Layers panel's New Layer button,
from Help › Search Commands… and from the panel menu's New Layer…
item. It had no key for it, and a Cmd+Alt+L press ran New Layer as
Cmd+L does.

ui.newLayer (New Layer Options…) now has the default Cmd+Alt+L, which
is Ctrl+Alt+L on Windows and Linux. The key opens Layer Options for a
new layer, as the Alt-click does: OK adds the layer above the current
layer, and Cancel adds none. Cmd+L still adds a layer without the
dialog. Keyboard Shortcuts now lists the command with its key.

Docs: docs/mcp.md, docs/ui-parity.md, docs/target-app-parity.md.
2026-10-21 16:46:41 +07:00
dotCooCoo b0056a127d Type: Optical Margin Alignment hangs punctuation at the ends of type on a path
Optical Margin Alignment and Roman Hanging Punctuation hung punctuation
in point and area type only. Type on a path kept its first and last
glyphs inside its brackets with either option on.

When Optical Margin Alignment is on, or the first paragraph has Roman
Hanging Punctuation on, the glyphs at the ends of left-to-right type on
a path now hang outside its brackets by the same amounts, measured
along the path. The type hangs at the start bracket when its line is
placed there, hangs at the end bracket when the line is placed there,
and hangs at both when it is centered. Glyphs that hang past the end
bracket do not make the type overflow. On an open path a hang stops at
the path's end, and on a closed path the two hangs together take at
most the stretch between the end and start brackets. SVG export writes
type on a path that hangs as outlines, with a warning.

Docs: docs/mcp.md, docs/type-parity.md, docs/target-app-parity.md.
2026-10-21 16:46:23 +07:00
dotCooCoo 1cea163b11 Paragraph: Insert Kashidas justifies Arabic lines with kashidas
The reference app justifies Arabic text with kashidas, set per paragraph
with the Paragraph panel's Insert Kashidas list (None, Short, Medium,
Long, Stylistic). VectorCraft had no kashidas, so a justified Arabic
line was spaced out at its word spaces like a Latin one.

The Paragraph panel, with the Indic options shown, now has Insert
Kashidas. On a justified horizontal area type line that is shorter
than its measure, each Arabic word with joined letters gets one kashida
between its last pair of them, drawn as tatweels (U+0640) in the font
of the letter after it. The kashidas share the line's room equally,
each at most a quarter of an em (Short), half an em (Medium) or an em
(Long); a Stylistic kashida is a row of the font's own tatweels, up to
an em. The word spaces take the room the kashidas leave, so the line
keeps its width, and line breaks do not change. The letter after a
kashida advances over it, so the caret before that letter, a selection
of it and a click on it take the kashida in. None, the default, leaves
lines as they were.

In horizontal type that is not on a path, the caret in a letter drawn
with several glyphs (a letter with a vowel mark) is now placed by the
whole letter, as hit testing already measured it, instead of by its
first glyph.

text.setFormat takes kashida. The setting is saved with the paragraph
only when it is not None, paragraph styles carry it, and Reset Panel
sets it back to None.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-21 16:46:22 +07:00
dotCooCoo 9be3ddab31 Type: the Every-line Composer composes paragraphs that mix type sizes in rectangular area type
A paragraph with one character in a different type size was broken
with the Single-line Composer, even when Every-line was chosen: the
composer gave up whenever the paragraph's glyphs did not all have the
same line metrics (ascent, descent, leading, cap height, x height and
em box top).

In rectangular area type without text wrap, rows and columns included,
the width of a line does not depend on how far down the frame it is.
There the Every-line Composer now composes a paragraph whose
characters differ in line metrics (type size, font, leading or
vertical scale, or an inline graphic taller than the text), and each
line takes its height from its own text, as with the Single-line
Composer. Area type of another shape, or with text wrap, still breaks
such a paragraph with the Single-line Composer.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-21 16:46:19 +07:00
dotCooCoo 819efd465f EPS import: a PostScript error shows what its name means in the interface language
An EPS or PostScript file that stops on a PostScript error named the
error, such as typecheck or rangecheck, with the operator and the
procedure, but did not say what the name means in any language.

The warning or error now gives the name as PostScript spells it,
followed by VectorCraft's own wording of what it means in the reader:
"a PostScript error (typecheck in `add`; a value has the wrong type for
the operator)". vectorcraft_eps::POSTSCRIPT_ERRORS lists the 14 names
the reader raises other than undefined, with their meanings, and a
test checks that each of those names has one. Each meaning and the
new message shapes have a row in every catalog. i18n::message now
translates placeholder values five levels deep instead of three, which
the status bar's line for an EPS file that can't be opened needs.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-21 16:46:17 +07:00
dotCooCoo d55a5245fb Graphs: column designs show each column's value where their type has % and two digits
The reference app's graphs help describes adding totals to a column
design: type in the design holds a percent sign followed by two digits.
VectorCraft drew that type as typed, stretched with the design.

Type in a column design that holds a % followed by two digits now shows
the value of each column or stacked segment in place of the % and the
two digits. The first digit is the least number of places before the
decimal point, with zeros added in front (0 for as many as the value
needs), and the second is the number of places after it. Values are
rounded half away from zero. The type keeps its own size and moves to
where the design's copy puts the point it is set from. A repeating
design shows it once per column, where a whole copy ending at the value
end puts it, and Chop Design does not cut it. A sliding design moves it
with the part of the design on its side of the guide. Type inside a
group that clips or has its own opacity, blend mode, appearance or
opacity mask, or inside a blend, compound shape, envelope, repeat or
intertwine, stays in the design and shows the value in each copy. Type
inside a symbol stays as typed. The legend swatch leaves the totals
out.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-21 16:46:15 +07:00