Commit Graph
4105 Commits
Author SHA1 Message Date
dotCooCoo fe89fc39a2 Package, Place, Export: commands reject a param of the wrong type
file.package, file.place, file.place.queue, file.place.info,
document.serialize, document.export, document.exportSelection and
document.exportForOffice read their switches, strings and numbers with
helpers that fell back to the default for a value of the wrong type.
file.package {"copyFonts": "false"} copied the fonts, and
document.exportForOffice {"path": 5} returned the bytes instead of
writing a file. Each such value is now an error naming the key, and
nothing is placed or written. Null still means the key was left out.
2026-11-11 16:17:10 -08:00
dotCooCoo 74ea25cd6c File formats: HP-GL (.plt) plot files open and place
File › Open and File › Place refused plotter files: HP-GL/2 files
(.plt, .hpgl, .hgl, .hpg) were not a format VectorCraft reads.

The format table gets a read-only HP-GL format, read by a new
vectorcraft_cad::hpgl module. Each line the pen draws becomes a stroked
path in its pen's color and width, with the line type as a dash pattern
and the line attributes as caps and joins. Circles, arcs, wedges and
Bézier curves are drawn as curves. Filled and edged rectangles, wedges
and polygons (polygon mode) become filled or stroked paths, and encoded
polylines, scaling points, user units and rotation are read. Plotter
units (1016 to the inch, y up) become points, and the artboard holds
what the art paints. Place brings the art in as one group. Older HP-GL
files are read by the HP-GL/2 rules for the instructions the two share.

A file is recognized by its first instructions, after any printer
escape sequences and PJL lines, so a plot file also opens under an
extension no other format reads. VectorCraft's own rules where the
device decides: before the first SP the pen is pen 1, and the hard-clip
limits are 11 x 8.5 in, which P2 takes when the file sets no scaling
points. Labels, symbol-mode symbols and raster images are not drawn,
the input window does not clip, hatched fills are filled solid, and
later pages are drawn over the first; the warnings list each. Objects,
path points and pen widths are capped, and the import fuzzer covers
random, mutated and garbage plot files.

An importer plug-in that lists .plt, .hpgl, .hgl or .hpg now opens
those files only when document.open names it with `importer`.

Docs: docs/mcp.md, docs/file-format-parity.md,
docs/target-app-parity.md, docs/releasing.md.
2026-11-11 16:03:41 -08:00
dotCooCoo 4591653f33 MCP: library lists leave out paths outside a confined client's read roots
swatch.library.list, brush.libraries, symbol.libraries and
graphicStyle.libraries returned every library's file path and the user
library folder to a client confined by --automation-read-root, including
paths outside its read roots. They now return null for those paths, and a
library loaded from a file outside the roots is listed as
loaded/<file name>, numbered (2), (3) when names repeat. That id finds the
library in the other library commands, and their replies show it too.

- The naming moves from control.rs into
  swatchlib::concealed_library_ids, which numbers the session's loaded
  libraries first, so ui.inspect, ui.docking and the library commands show
  a library under the same id.
- swatch.library.copyToUser refuses a library whose file is outside the
  read roots without naming the file; it and the library saves with
  `user: true` leave out `path` when the user library folder is outside
  the read roots.

Docs: docs/mcp.md (read roots), docs/target-app-parity.md.
2026-11-11 15:58:01 -08:00
dotCooCoo 335acab01b Scripts: a missing file below a link that leads nowhere is refused
A missing file below a folder link whose target does not exist got
"does not exist", while one below a link to an existing outside folder
was refused, so a script could tell whether the folder a link leads to
exists. A missing path is now refused when the nearest entry above it
that exists is a link that leads to nothing.
2026-11-11 15:53:26 -08:00
dotCooCooandGert Pellin 1b744eae52 Gradients: conic gradients sweep the stops once around the center
A fourth gradient kind, conic (also accepted as `angle` or `conical`),
sweeps the stops once around the center (the gradient's start),
clockwise on the page from the vector, with a seam where the last stop
meets the first. A negative aspect mirrors the sweep, and reflecting an
object turns its sweep the other way, also for a conic without a
vector or with a flat extent.

- Gradient panel: a Conic button in the Type row; the Aspect Ratio
  field works for conic gradients and keeps a mirrored sweep mirrored.
- Gradient tool: the extent ellipse, aspect handle and center ring of a
  radial gradient, without a focal point.
- Rendering, swatch chips, transforms, warps, plug-in round trips, and
  Object > Expand into wedges or a mesh of columns around the center.
- Commands: a negative `aspect` is an error for linear and radial
  gradients, and so is a `focal` point for a conic gradient.
- PDF export writes a function-based shading whose calculator function
  turns the angle into the ramp, and PDF import reads a function-based
  shading that turns around the origin of its space back as a conic
  gradient, also when that origin lies outside the painted area. A
  PDF/A file, or a conic with more than 97 stops, gets an image
  instead.
- SVG export writes 360 solid wedges clipped to the shape for opaque
  stops and an image otherwise, and a linear gradient on characters.
- CSS export writes `conic-gradient(from ...)`, with the stops mirrored
  for a mirrored sweep.
- EPS and legacy .ai export write 360 solid wedges; the .ai editing data
  holds a linear gradient along the vector with the same stops.
- .af files: conical gradients open as conic gradients.
- An export that writes the gradient in another form gives a warning.

Supersedes #2326 and includes switch87's code from it.

Co-authored-by: Gert Pellin <gert@pellin.be>
2026-11-11 15:51:11 -08:00
dotCooCoo ef7fbaac31 Plug-ins: a failed rename back of a removed plug-in's settings is reported and undone later
When plugin.remove could not remove the grant and then could not rename
the plug-in's settings folder back from <folder>.removed, the failure was
only logged: the plug-in got empty settings, and the next removal deleted
the .removed folder with the settings in it.

- The error now names the .removed folder, the plug-in folder and the
  reason, and the plug-in gets no storage until the settings are back.
- The next load, install or plugin.remove of the plug-in renames a kept
  folder back while the plug-in has no folder of its own. When it has one,
  that folder is used and the kept one stays until plugin.remove deletes
  both. A removal while a kept .removed folder exists renames the plug-in
  folder to <folder>.removing.
- Before deleting a renamed folder, the store writes an empty
  <folder>.removed.delete file and removes it once the folder is gone, so
  what a failed deletion leaves is not moved back. When that file can't
  be written, nothing is deleted.
- A .removed folder without a .delete file, including one an earlier
  version left, counts as kept settings: a new install of the id needs
  replace: true, and plugin.remove of the id deletes it.
2026-11-11 15:51:11 -08:00
dotCooCoo 14c8385df3 Clipboard: a paste after an unanswered read waits for its answer
When the check whether the clipboard still holds what VectorCraft copied,
or one of a paste's own reads, gets no answer within its 2 s, the read is
kept. The paste then waits for that answer on its background thread, up
to 10 s after the read was sent, instead of failing its other reads at
once and pasting the internal clipboard. When the answer is what
VectorCraft copied, or none comes, the paste takes the internal
clipboard; otherwise the clipboard is read as a paste reads it, with that
answer standing in for its read. An answer that came before the paste is
dropped and the clipboard is read again.
2026-11-11 15:49:27 -08:00
dotCooCoo b89a6e8ba1 Type: Block's word gap leaves the paragraphs' word spacing as set
The word gap is now the Desired word spacing the layout uses for area type
with Block on (TextObject::layout_justification), with Minimum and Maximum
moved out to it when it lies outside them. The paragraphs keep their own
Justification values, so a wide gap no longer leaves a 1,000 % Maximum
behind, and turning Block off brings back the paragraph's own Desired
value. The SVG writer reads the same values.

- The Contextual Task Bar offers Dynamic Text for area type outside a
  thread, beside Convert To Point Type.
- type.dynamicText with wordGap or lineGap and no area type in the
  selection is an error instead of being ignored.
- Docs: docs/mcp.md, params text, docs/target-app-parity.md.
2026-11-11 15:49:16 -08:00
dotCooCoo b33c004fb0 Tabs: indent drags on threaded frames follow the story's order and set a split paragraph once
With the Selection tool on several frames of one threaded story, the
Tabs panel listed the paragraphs to change in the order the frames were
selected, and listed each frame's paragraphs separately. A marker drag
over mixed indents then ran its per-paragraph steps out of story order,
so a step that made text reflow could move a later step's paragraph,
and a paragraph that runs on from one frame into the next got two
steps. After Type tool Select All the split paragraph also got two.

The panel now lists each paragraph of a story once, in story order,
by its last piece with text in a selected frame. With the Selection
tool only the paragraphs with text in a selected frame change, so a
frame selected alone that starts or ends inside a paragraph sets it.

Docs: docs/target-app-parity.md.
2026-11-11 15:47:06 -08:00
dotCooCoo e6add18663 Fix #6634: Pen closing drag no longer gives the first anchor symmetric handles
Closing a path by dragging on its first anchor made that anchor smooth
with symmetric handles, so the first segment bent along with the closing
one. The drag now sets the first anchor's incoming handle to the mirror
of the pointer. Its outgoing handle stays where it is on a corner, so
the first segment keeps its shape, and on a smooth anchor it turns to
stay in line at its own length. Alt makes the anchor a corner and leaves
the outgoing handle where it was when Alt went down. A click on the
first anchor closes the path with the incoming handle retracted and the
anchor a corner, so the closing segment comes in straight.

The Smart Pen's closing drag follows the same rule.

path.close's `in` now places the handle as path.setHandle does, and a
new `out` param places the outgoing handle and makes the anchor a
corner.
2026-11-11 15:33:09 -08:00
dotCooCoo e05f40d7f8 Type: Center on Top and Center on Bottom for type on a closed path
Type › Type on a Path › Center on Top and Center on Bottom
(type.pathCenter.top and type.pathCenter.bottom, also on the Contextual
Task Bar for type on a closed path) slide the type along its path so that
the middle of the span between its brackets lands on the path's topmost or
bottommost point as the object is drawn, after its transform. On a level
stretch, such as a rectangle's top edge, the middle of the stretch is used.
The span keeps its length, and the first paragraph is set to Center unless
it is Center or Justify Center. The type is not flipped. Type on an open or
compound path, type in a thread, and type on a path with no length or no
height are refused with an error that names them. When the type is already
centered there, no undo step is recorded. This is VectorCraft's own
behavior.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-11 15:33:01 -08:00
dotCooCoo 5e78aa6575 3D: a stroke's walls cover the fill's walls at concave corners
Extrude & Bevel (both kinds) and Inflate drew the faces of the fill's
solid and the stroke's solid in one order by each face's center depth.
At a concave corner a long fill wall could sort after a stroke wall in
front of it, so stripes of the fill showed through the stroke.

- Facets now record whether they are side faces (walls, bevels,
  Inflate's sides) and whether they are turned away from the viewer.
- With caps on, each fill side face turned toward the viewer is drawn
  before every face of the same shape's stroke whose bounds overlap
  it, for strokes whose solid hides the fill's whole edge: a closed
  shape, a width, no dashes, width profile or brush, and a solid color
  or a gradient without transparent stops. Caps, other shapes in a
  group, hollow effects and translucent or blended surfaces keep their
  order.

Docs: docs/three-d.md (Fills and strokes), docs/target-app-parity.md.
2026-11-11 15:32:33 -08:00
Ahmed Gomaa 9a77c2ee11 Save As fallback asks for a changed format's options
When the read-only fallback's Save As panel returns a different format, drop the options captured for the original format and open the picked format's normal Save options; an unchanged format keeps its options. Regenerate the file.save entry in the MCP command catalogue.
2026-11-12 01:27:25 +02:00
dotCooCoo aa73329a40 Document Info: Gradient Objects lists the objects painted with gradients
The gradients category was titled Gradient Swatches and listed every
gradient swatch, used or not. It is now Gradient Objects: one row per
object filled or stroked with a gradient, named as the object is, with
the gradients it uses (a linked swatch by name, else Linear gradient,
Radial gradient or Freeform gradient). document.info adds
gradientObjects and gradientNames, and Selection Only, Save and the
Package report use the same rows.

Gradient objects follow the rules Pattern Objects uses for paint that
draws: hidden fills and strokes, strokes with no width, guides, compound
path members' own paint, image paint without a shape and type that draws
nothing are left out, and type whose characters carry a gradient is
included. objects.gradients now counts the listed objects, so it no
longer counts hidden or zero-width gradient paint and no longer misses
type whose characters use a gradient.

The duplicate Document Info Save line in docs/target-app-parity.md is
removed.
2026-11-11 15:24:12 -08:00
dotCooCoo 7114de8ae5 Scripts: a missing file outside the script's folder is refused like an existing one
A `File` or `Folder` for a missing path got "does not exist" even when
the path led outside the script's folder, through a folder link or, on a
file system where case matters, through a folder whose name differs from
the script's folder only in case, while an existing file there was
refused. A missing path is now refused unless the nearest folder above
it that exists lies inside a granted folder.
2026-11-11 15:23:08 -08:00
dotCooCoo 253c60a23e Type: a line's first word is measured as set when looking for room
In a frame that is not a rectangle, or with text wrap, layout moves a
line down to the first span wide enough for its first word. The word
was measured at full width before Mojikumi set an opening bracket at
the line's start flush, and with its last character at full width, so
a word that fit as set moved down to a wider line. It also ended at any
break opportunity, so an opening bracket counted without the character
kinsoku keeps with it and could be placed alone on a line too narrow
for that character.

- word_width ends the word only where break_line may end a line: at a
  cluster end that kinsoku allows, not inside a No Break run, whose
  spaces count. It leaves out the spaces at the word's end and narrows
  its last glyph by the Mojikumi line-end credit.
- place_flow_line measures the word after the opening-bracket trim, and
  the Every-line Composer's first-line placement takes off the same
  start credit, so the composed widths match the placed lines.
2026-11-11 15:20:50 -08:00
dotCooCoo a5bfa524dc Type: Mojikumi spacing at line ends counts when lines break
With Line-end Punctuation Half Width, a closing bracket, comma or full
stop ending a line is set half width, but both composers measured it at
full width. A closing bracket that fit only at half width went to the
next line with the character before it, and so did a comma or full stop
with Burasagari None.

The single-line and every-line composers now measure each candidate
line with its last character set as narrow as Mojikumi sets it, using
the same rule the line is drawn with. A mark that already lost its half
to the punctuation after it and a line ending in a hyphen take nothing
more off, and the quarter em between Japanese and Latin text counts only
where the line ends. Kinsoku Shori Type measures the pushed-in and
pushed-out lines the same way. Right-to-left lines and Mojikumi None
break as before.

A test font sets ideographs and Japanese punctuation one em wide, so
the layout tests run without system fonts.
2026-11-11 15:20:34 -08:00
dotCooCoo 7f641b9c10 Help: a stalled interface writes a hang report, and Copy Troubleshooting Info copies it
When the interface thread stays in one part of a frame for 3 s, a
watching thread writes a hang report to the log and to
logs/hang-report-<process id>.txt. The report gives the part of the
frame (input hook, logic, interface, or the renderer after a frame in
which any window had input), a system clipboard call in progress, the
last 8 different command ids, the last 16 frame times, the platform,
window system, desktop and session type, Flatpak, graphics adapter, the
present mode and frame latency the app asks for, the running time and
the number of open documents. It holds no document contents, file paths
or command parameters. The interface thread marks its stages with a few
atomic stores a frame, and the watching thread reads those marks without
waiting on it.

The process keeps the report file locked until the interface answers
again, then removes it. A file nobody holds at a later launch was left
by a session that was closed while it did not answer: the launch writes
the report to the log, moves the file to logs/hang-report.1.txt and,
unless another start-up message is showing, says so in the status bar.
Help › Copy Troubleshooting Info puts the platform facts, the recent
commands and frame times and both sessions' reports on the clipboard for
a bug report. An in-line file dialog, an open menu of the macOS menu bar,
and the time after a frame that minimizes or hides the window or starts
moving or resizing it are not timed.

The menu label and the two status messages have rows in every catalog.
2026-11-11 15:19:35 -08:00
dotCooCoo b576a4e39a Shape tools: a drag on a bounding-box handle scales the selection
Since #5839 the Rectangle, Rounded Rectangle, Ellipse, Polygon and Star
tools draw the selection's bounding box with its eight handles, but a
press on a handle drew a new shape from it. A shape just drawn could not
be resized without switching to the Selection tool.

A press on a handle now scales the selection as the Selection tool's
handles do: the same cursors, snapping, Shift and Alt, Use Preview
Bounds, area type frames and Backtick for patterns. The box stays on
show at the changing bounds during the drag. With the bounding box
hidden, a press there still draws.

Docs: docs/target-app-parity.md.
2026-11-11 15:07:21 -08:00
dotCooCoo 78f412f0a3 Clipboard: a paste's bitmap read waits for the owner off the UI thread
A paste on Linux and the BSDs read the bitmap on the clipboard worker but
waited up to 10 s for it on the UI thread, so a bitmap from an app that
never sent its data froze the window long enough for the compositor to
offer to close it.

The paste now sends the bitmap read to the worker and returns at once;
its background thread waits up to 10 s for the answer and converts it to
PNG. While that read is open, other reads fail at once instead of
queuing behind it. Each read gets its own answer channel, so a late
answer is dropped. The check of whether the clipboard still holds the
app's own bitmap waits 2 s at most, as text does.
2026-11-11 14:57:01 -08:00
dotCooCoo 634b9c7ff8 Objects on Path: the Move All widget slides every attached object along the path
With one Objects on Path group selected whose objects have room to move,
the Selection and Objects on Path tools show a diamond below the first
object. Dragging it moves every object along the path together, keeping
the distances between them (object.objectsOnPath.options with start),
in one undo step; Escape cancels it, and the first object's distance
along the path shows next to the pointer.

On an open path the widget shows only when the distance from the first
object to the last is shorter than the path, and the objects stop at its
ends; on a closed path they go on round across the first anchor. Where
Auto or a Space value the open path is too short for sets the distance,
the drag also sets spacing to the distance as drawn.
2026-11-11 14:55:46 -08:00
dotCooCoo 3fda7ad83e Actions: Batch shows the data set File Name row only where it applies
The Batch dialog showed File Name for every destination when Play was
Data Sets, though only Destination Folder writes a file named from it.
The row now shows after Destination Folder for Data Sets with a folder
destination, and the dialog sends datasetFileNaming only then. The
chosen value stays in the dialog while another destination is picked.
action.batch and its default for an omitted datasetFileNaming are
unchanged.
2026-11-11 14:51:21 -08:00
dotCooCoo d9c38df736 MCP: link replies leave out paths outside a confined client's read roots
links.list, links.check, links.info and document.open's missingLinks,
modifiedLinks and updatedLinks rows no longer give an automation client
with read roots the path of a linked file outside those roots. path and
found are left out of the rows, and links.info also leaves out link,
location and the file size and date the document stores. A relative path
is taken from the document's folder. Each row keeps its id, file name and
status, and the document's links are not changed. Without read roots the
replies are as before.

Find in Folder now matches a search's rows by their ids, comparing paths
only when both rows have one, so it still shows the files found for a
row whose path was left out.
2026-11-11 14:47:53 -08:00
dotCooCoo 7bd559cb1d Docs: PDF/A files at PDF 1.4 write long pages in points
A PDF/A file at PDF 1.4 has no UserUnit, so a page longer than 14,400 pt is written in points with or without longPagesInPoints. The param text, docs/mcp.md and the exporter comment now say that only PDF/A files at PDF 1.5 and later keep the UserUnit.
2026-11-11 14:47:18 -08:00
dotCooCoo 5bd1bbf536 Scripts: File and Folder exists checks the kind, encode keeps slashes, every call is counted
- `File.encode` and `Folder.encode` leave `/` unescaped, as the File
  object reference lists it among the characters kept
  (https://extendscript.docsforadobe.dev/file-system-access/file-object/).
- `exists` is true for a `File` only when the path names something
  other than a folder, and for a `Folder` only when it names a folder.
- Fields of the records the app returns are read as own properties, so
  a property a script puts on `Object.prototype` does not turn a
  successful `open` into a failure.
- `File.encode`, `File.decode`, `File.isEncodingAvailable` and the
  `Folder` locations count as file calls and charge the host-call limit.
- On the web the locations are `null` and `File.tempName` names a file
  at the root, without asking the platform for a temporary folder.
2026-11-11 14:46:43 -08:00
dotCooCoo 2da0dcbba7 PDF: a PDF/A-2b file at PDF 1.4 writes long pages as PDF 1.6 with a UserUnit
PDF/A-2 limits page boundaries to 14,400 units (ISO 19005-2, 6.1.13), and
PDF 1.4 has no UserUnit. A PDF/A-2b export at PDF 1.4 with a page longer
than 14,400 pt was written in points, with a MediaBox over 14,400 units,
whether or not longPagesInPoints was on.

Such a file is now written as PDF 1.6 with a UserUnit and a warning that
names the version, as PDF 1.5 files already are. PDF/A-2 files may declare
PDF 1.0 to 1.7. Pages past 32,767 pt that aren't whole numbers of points
are written too, since the refusal applies only to PDF 1.3 and 1.4 files.
Files whose pages are all 14,400 pt or shorter stay PDF 1.4.
2026-11-11 14:43:24 -08:00
dotCooCoo aa27045ef4 Type: Character Style Options sets Tate-chu-yoko Up/Down and Left/Right
Advanced Character Formats in Character Style Options and New Character
Style gains Tate-chu-yoko Up/Down and Left/Right fields, in points from
-1296 to 1296, which set the style's tcyUpDown and tcyLeftRight as the
Character panel menu's Tate-chu-yoko Settings sets them on text. An empty
field leaves the attribute unset, an explicit 0 stays distinct from
unset, a value outside the range is an error, and the section's Reset
Panel clears them with its other attributes. Text that uses the style
takes the values and keeps its local overrides.
2026-11-11 14:40:53 -08:00
dotCooCoo 6a7214df4c MCP: replies hide a library path outside the read roots wherever it appears
Under the read roots, every control reply now shows a hidden library's
id as loaded/<file name> and its path as the file name in every string
and key: status text, dialogs, the command palette query, a script's
report, recorded steps and errors. The id and path are replaced at any
boundary, so a path between spaces, brackets or quotes, followed by a
period or by more path text (<path>.bak), or quoted with escaped
backslashes is hidden too. An occurrence that continues into a longer
path stays only when the request may read that longer path, so a
readable library's id that begins with a hidden one keeps its text.
The libraries hidden after the request ran are hidden, and for a
request that names one, those hidden before it too. A reply with no
path separator in it is not searched. Encoded file data (dataBase64,
pngBase64) is sent unchanged.

Recorded steps in ui.inspect leave out a path outside the read roots
under path, paths, file, files, folder and any param whose name ends in
Path, Paths, File, Files or Folder (diffPath, fontFiles,
destinationFolder), and an absolute path or file: URL outside the roots
under any other param or in any list. The file: scheme is matched in
any case, a URL's %XX escapes are decoded before the check, a file: URL
inside the roots stays, and a file: URL that
names another host is always left out. file_access::is_path_key holds the list of
path params, and testkit's is_path_key uses it.
2026-11-11 14:23:06 -08:00
dotCooCoo f3cff033ca Edit: a command that ends a drag keeps its unapplied preview
With Real-time Drawing off, a tool stores its latest preview and applies
it only when the drag is released. A command that ends the drag first
(Undo, Redo, a filter that calls commands, a plug-in command that calls
commands, and the other callers of settle_interaction) committed the
interaction without that preview, so the drag was dropped: Undo then
took back the step before it, and a calling filter was given the
objects where they were before the drag.

settle_interaction now applies such a preview first, as the tool's own
commit does, and cancels the drag when the preview fails. The re-prepare
of a calling filter's input after settling is now reached, and tests
cover it at both plugin.run and plugin.<plug-in>.<command>.
2026-11-11 14:15:22 -08:00
dotCooCoo e63e341a95 Docs: the Check Script escape example shows the escape
The example of an escape that does not start a name read `0abc`, the character, where the four-digit escape of 0 followed by abc was meant.
2026-11-11 14:03:16 -08:00
dotCooCoo 0923d862a1 PSD import: File › Open converts a PSD file's layers to objects
Opening a .psd or .psb used to give one merged image. document.open now
reads the layer records and builds the document from them. The PSD
import options page
(https://helpx.adobe.com/illustrator/desktop/add-and-import-files/import-from-other-apps/photoshop-import-options.html)
says the option that converts layers to objects keeps as much of the
layer structure as it can, merges the layers of a clipping mask into one
layer and discards hidden layers that require merging, and that Import
Hidden Layers brings hidden layers in. document.open does the same, with
hiddenLayers: true for the hidden layers; layers: false keeps the merged
image.

VectorCraft's own rules:

- Each layer becomes a layer holding its pixels as an image, with its
  name, visibility, opacity and blend mode. A group becomes a layer of
  sublayers; a Pass Through group is not isolated.
- Clipped layers show where the layer below has pixels. That layer's
  fill opacity fades only its own pixels. Layers clipped to an
  adjustment layer, or to a fill that stores no pixels and isn't drawn,
  show over the whole canvas.
- A layer mask becomes the layer's opacity mask, its default color and
  invert flag applied; a disabled mask is kept, switched off.
- Fill opacity multiplies the layer's opacity.
- Layers and groups in Dissolve, Linear Burn, Linear Dodge, Vivid Light,
  Linear Light, Pin Light, Hard Mix, Subtract, Divide, Darker Color and
  Lighter Color are merged with the layers below, whether or not they
  hold transparent pixels; Color Dodge, Color Burn and Difference keep
  their mode. A merged Dissolve layer's pixels are opaque or
  transparent, in a pattern of each layer's own.
- A solid color fill layer that stores no pixels and has no vector mask
  comes in as its color (RGB, CMYK, Grayscale, Lab or HSB).
- A layer name stored only as a Pascal string, without its Unicode
  name, is read as Mac Roman.
- Adjustment layers, fill layers that aren't drawn, layer effects that
  are switched on, knockouts and vector masks are not applied; the
  warnings count them.
- A merge whose canvases need more memory than allowed leaves the layer
  alone, in the Normal mode or unclipped, with a warning.

This changes what opening a layered PSD does. Batch Trace Folder passes
layers: false, so a PSD is still traced as its merged image. A file
whose layers can't be read, or whose layers and merges need more memory
than allowed, opens as the merged image with a warning.

Lengths, counts, nesting, decoded pixels and the canvases merges make
are bounded. Zipped 32-bit data with prediction is now read, in layers
and in the merged image. import_fuzz gains a case that mutates the PSD
and PSB samples and opens them with their layers. The PSD reader's new
error messages have rows in every catalog.

Docs: docs/mcp.md, docs/target-app-parity.md, docs/file-format-parity.md,
README.
2026-11-11 13:59:32 -08:00
dotCooCoo 283d127e2a JavaScript: characters assigned in Unicode 17 are identifier characters
vectorcraft-js and Check Script took identifier categories from
unicode-general-category 1.1.0, which carries Unicode 16.0, so letters
and marks assigned in Unicode 17.0 (U+088F at the start of a name,
U+1ACF after its first character) were refused. Both lexers now read
general categories from unicode-properties 0.1.4 (Unicode 17.0), which
Cargo.lock already holds. The version is exact because 0.1.5 adds the
Unicode-3.0 license, which is not on the license allowlist. The
Other_ID_Start, Other_ID_Continue and Pattern_Syntax lists are the same
in PropList 17.0.

The exhaustive identifier test now expects the DerivedCoreProperties
17.0 totals (145,916 ID_Start, 149,240 ID_Continue), and new tests read
U+088F and U+1ACF written out and as escapes in both lexers.
2026-11-11 13:57:54 -08:00
dotCooCoo fc767dad09 Type: Dynamic Text sets each line of area type to the frame width
Type > Dynamic Text (type.dynamicText) now also works on area type as
Block. Each line as typed (each paragraph, and each forced line break in
one) is set to the frame's width, less the inset, the column gutters and
the paragraph's indents, by changing its font size only. Lines are not
broken again. The first character's size moves in 0.1 pt steps within
0.1 to 1,296 pt, and the line's other characters keep their proportions.
The sizes are written into the characters and fitted again after every
edit to the text or the frame, in the same undo step.

- Word Gap (0 to 1,000 % of a word space, default 100) sets every
  paragraph's Desired word spacing; Line Gap (-1,296 to 1,296 pt,
  default 0) is added to each character's size for its leading. Both are
  VectorCraft's own ranges and defaults, saved in the area options only
  when they differ from the defaults.
- Area Type Options gains a Block fit (text.areaOptions fit "block",
  with wordGap and lineGap), and the Properties panel's Paragraph section
  shows a Dynamic Text checkbox with Word Gap and Line Gap rows.
- Block replaces Auto Size and Shrink Text to Fit. Off keeps the sizes.
  Threaded area type is refused, as for type on a path.
- Empty lines keep their size, a frame with no width changes nothing,
  vertical type fits each line to the frame's height, and right-to-left
  lines and list markers are fitted like other lines. A refit runs at
  most four extra layouts.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-11 13:53:20 -08:00
dotCooCoo 0bf35ac543 Docs: each progress line appears once in the shipped-features evidence list
The "Evidence: what ships today, by area" list in
docs/target-app-parity.md carried three lines twice, each copy
byte-identical: Window › Arrange › Tile, the renderer's path cache for
moved anchors, and the plug-in instruction budget at a bulk memory
instruction. One copy of each is removed. The budget line stays next to
the bulk memory fuzz entry; the other two stay at their first place.
2026-11-11 13:50:57 -08:00
dotCooCoo 8e90c833f6 Scripts: a kept engine's next run starts after the last uncaught exception is dropped
Resetting Folder.current for the next run of a named engine now drops the
last run's uncaught exception and $.error first, so memory only they held
no longer makes the reset fail with the heap limit on every later run.

A File an earlier run left open now counts as closed in a later run:
setting its encoding stores it for the next open, and reading the encoding
gives the stored one.

docs/mcp.md: engines stay on the thread that made them, and a
#targetengine line in a file $.evalFile runs makes that call throw a
SyntaxError when it runs.
2026-11-11 13:50:12 -08:00
dotCooCoo e3093034ea Scripts: opening a macOS menu reads the Scripts folder again
With the macOS menu bar, File › Scripts was read only at startup and
when the window became active, so a script added while the window
stayed active did not appear. The native menu now reports a menu
opening (Event::Opened, when AppKit begins tracking), and
native_menu::run reads the folder again, as opening the in-window File
menu does. Menu updates wait until tracking ends, so the new listing
shows the next time the menu opens.

Docs: docs/mcp.md (File › Scripts), docs/target-app-parity.md.
2026-11-11 13:47:41 -08:00
dotCooCoo dc98b59444 PDF: the in-points option leaves PDF/A pages within the standard's size limit
longPagesInPoints now applies only to files without a standard. PDF/A-2 limits page
boundaries to 14,400 units (ISO 19005-2, 6.1.13), so a PDF/A file with a page longer than
200 in keeps its UserUnit whether the option is on or off. The Save PDF dialog disables the
checkbox for PDF/A as it does for PDF/X.

Follows #6643.
2026-11-11 13:45:46 -08:00
dotCooCoo 133e2c1356 MCP: livePaint.info reads id when group is left out
The docs said every livePaint key left out is reported as missing. livePaint.info reads `id` when `group` is left out or null, and reports a missing group only when both are left out.
2026-11-11 13:17:08 -08:00
Ahmed Gomaa f0e50437c2 Save on a read-only location offers Save As
A plain Save that the location refuses (read-only filesystem or permission denied) now opens Save As with the name filled in, instead of ending in an error dialog. The refusal travels as a typed engine error (READ_ONLY_LOCATION), so the control protocol and MCP still return an error and never open a panel. Recovery failures, explicit destinations, a busy picker and Close/Quit keep their existing flow.

Closes #6146
2026-11-11 23:16:03 +02:00
adambelis a9fd0eb7c0 Merge remote-tracking branch 'origin/main' into feat/width-smooth-profile
# Conflicts:
#	docs/target-app-parity.md
2026-11-11 22:13:44 +01:00
Sven Lafebre 7844657096 Fix #5111: keep ligatures whole and place line starts in SVG by value
- drawn_alike ignores kerning_before, so a space before the "f" of "fi"
  no longer splits the ligature.
- SVG export takes a line's starting space from kerningBefore and the em
  shaping sets the character in (vectorcraft_text::line_start_space),
  not from where the glyph is drawn, which can include a shaping offset;
  the reader layout for justified lines leaves the space out, so those
  lines start there too.
- Type on a path writes the space as the first character's dx; a space
  after a line break, which <textPath> can't write, is outlined.
2026-11-11 22:09:20 +01:00
dotCooCoo b426822a9c Type: Release Type on a Path keeps the size Dynamic Text drew the type at
Release Type on a Path copied the stored runs into point type with the
Dynamic Text flag still set, so the text dropped back to its stored size
and the file saved the flag on point type. The point type now gets the
drawn sizes and the flag is turned off.

The legacy .ai writer writes Dynamic Text type at its drawn sizes; it
now also warns that the setting itself is not written, as the editing
data writer does, whether the type is written as text or as outlines.

docs/target-app-parity.md lists reshaping or scaling the path among
VectorCraft's own rules, and a task bar comment marks leaving the item
out for threaded type as VectorCraft's own rule.
2026-11-11 13:06:51 -08:00
dotCooCoo fd7ff70941 Plug-ins: an update left in the store by a failed put-back turns a disabled plug-in on
When plugin.install writes a module that lists permissions, its grant
can't be written, and the old module can't be put back (or, on a first
install, the new module can't be removed), the store holds the new
module. install now takes put_back_plugin's result, so the plug-in's
disabled entries (by id and by stored file) are cleared as for any
install the store holds. The old grant, or none, still doesn't cover the
new module, so the next start skips it until it is approved.

Docs: docs/plugins.md, docs/mcp.md, docs/target-app-parity.md.
2026-11-11 13:03:38 -08:00
dotCooCoo 5c2e182388 MCP: a malformed object id is refused as such, not reported as missing
Commands that need an object id read it with `id_param`, which returns
None for a value that is not a non-negative integer, so `{"id": "5"}` or
`{"id": -1}` was refused with "missing id". They now read it with
`object_param`, which refuses such a value with "`id` must be a whole
number", naming the key. This covers `document.node`, the `path.*` anchor
and handle commands, the `text.*` range and path commands, the mesh,
envelope and slice point commands, `layer.target`, `layer.move`,
`node.move`, `select.magicWand`, and the `source`, `target`, `group` and
`path` keys of `appearance.transfer`, `appearance.copyFrom`, the
`livePaint.*` commands and `text.createInPath`.

The optional `group` of `livePaint.fill` and `parent` of `node.move` are
refused the same way when given. Before, a malformed `parent` moved the
object to the top level and a malformed `group` fell back to the
selection.
2026-11-11 12:47:49 -08:00
jmvergesandClaude Opus 5.5 543034987e Envelopes: type's strokes paint at their own weight along the bent outlines (#6644)
With Envelope Options > Distort Appearance on, VectorCraft turned the strokes
of type in an envelope into filled outlines and bent them with the art, so a
stroke got thinner where the grid squeezes the type and thicker where it
stretches it. The drawn art of users' .ai files keeps those strokes as stroked
paths at the type's own weights, and the ring they paint is as wide wherever
the grid squeezes or stretches the type. An envelope of stroked type in such a
grid therefore didn't draw as its drawn art and kept it instead of opening live.

Type's strokes (its characters' and those on its own appearance) now paint
along the bent glyph outlines at their own weight, with Distort Appearance on
or off, also in an envelope inside an envelope:

- doc: `map_node` maps the groups that hold type's glyph outlines with
  `Hooks::type_appearance` in place of `Hooks::appearance`, for type in the
  envelope's contents and for type that a live object in them (an inner
  envelope) outlined (`Warper::type_outlines`). Such a group's opacity mask
  is mapped with the ordinary hooks, so it bends as other art does. Groups
  marked as type's outlines in the contents themselves (art an earlier Expand
  made) stay plain art.
- effects: `bake_type_appearance`, `bake_appearance` with strokes kept as
  strokes: geometry effects on type's fills and strokes still bake in and bend.
- doc: `max_outset` counts type's character strokes as type's own visual
  bounds do (`char_stroke_outset`). It left them out, though they already
  painted past an envelope's grid where the glyphs reach its edge; this also
  grows the visual bounds of blends and repeats of stroked type and the reach
  of symbol art holding it.

Paths' strokes, geometry effects, opacity masks, linear gradients and pattern
fills bend as before. The canvas, hit testing, effect bounds, Expand, every
export and the .ai save take the evaluation from the same hooks. Expand of an
envelope of stroked type gives the bent outlines with the strokes on them.
This changes how VectorCraft's own envelopes of stroked type draw.

Tests: envelope_distort::type_strokes_paint_at_their_own_weight_in_a_squeezed_envelope,
a_stroke_on_types_appearance_paints_at_its_own_weight_in_a_squeezed_envelope,
type_strokes_paint_at_their_own_weight_in_an_envelope_inside_an_envelope,
a_paths_stroke_still_bends_in_a_squeezed_envelope,
an_opacity_mask_on_type_bends_as_other_art,
the_visual_bounds_of_an_envelope_hold_its_types_strokes,
the_visual_bounds_of_a_repeat_of_stroked_type_grow_by_half_the_stroke,
expanding_an_envelope_of_stroked_type_keeps_the_stroke_at_its_weight,
bake::tests::type_pieces_bake_their_geometry_effects_and_keep_their_strokes and
tests_native::an_envelope_of_stroked_type_whose_drawn_art_strokes_the_bent_outlines_at_the_weight_opens_live.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 20:45:33 +00:00
Sven Lafebre 6ae7051e4e Fix #5111: kerning at the caret before a line's first character moves it
At the Type tool's caret, kerning goes on the character before the caret.
At the start of the text there is none, so Alt+arrows, the Kerning field
and Reset Tracking and Kerning did nothing; at a later paragraph's start
the kerning landed on the invisible line break.

CharStyle::kerning_before (kerningBefore) is the space before a character
that starts a line by itself: the first character of the text, of a
paragraph, or after a forced line break. Shaping gives its cluster the
space and draws it that much later along the line (right to left the
space is at its right; in vertical type it moves down the column). The
caret at such a line's start sets it (edit::caret_kerning_target), in
type.step, type.resetTracking and the Character panel; text.setRangeStyle
takes it. SVG export starts the line where the character is drawn. The
Eyedropper and character styles leave it alone.
2026-11-11 21:43:31 +01:00
dotCooCoo 96e20d548d Plug-ins: a light filter that reports progress runs in the background with Cancel
A light filter of plug-in API level 2 with the `progress` permission ran
on the UI thread, so its progress reports and `vc_cancelled` never
reached a progress window. It now runs in the background under its light
budget, as a heavy filter does, when it runs at once from Object ›
Plug-ins or its shortcut, or when OK runs it with Preview off. The
progress window shows its share and Cancel stops it. With Preview on, OK
keeps the previewed run. A filter that lists a `commandAllowlist` still
runs on the UI thread.

Docs: docs/plugins.md, docs/mcp.md, docs/target-app-parity.md.
2026-11-11 12:18:18 -08:00
dotCooCoo 3a7c46975c Merge main into feat/script-target-engine 2026-11-11 12:16:55 -08:00
dotCooCoo 7fbac74885 Merge main into feat/script-target-engine 2026-11-11 12:09:30 -08:00
dotCooCoo 825620eb6c Clipboard: Linux and BSD reads wait 2 s at most for the app that copied
arboard's Wayland data-control read waits for the app that owns the
clipboard to send its data with no time limit, and its X11 read waits up
to 4 s for each format it tries. These reads ran on the UI thread: a
Copy or Cut checking what the clipboard held, a paste, and the paste
check once its background thread is gone. A clipboard owner that never
sent its data froze the window.

On Linux and the BSDs, arboard's reads now run on a worker thread with a
clipboard handle of its own. Text and file lists wait 2 s at most and a
bitmap 10 s; the core-protocol fallback's text reads already wait 2 s.
While a read is unanswered, later reads fail at once. Writes stay on the
UI thread's handle.

The core-protocol fallback's worker is now generic and built on every
platform. On X11, closing the clipboard within 25 s of a read that got
no answer waits for that read to end, so the app's own handle is the
last one and hands the copy to the clipboard manager. On data-control
and the core protocol, closing does not wait for an unanswered read.
2026-11-11 12:06:33 -08:00