Commit Graph
2142 Commits
Author SHA1 Message Date
dotCooCoo ed3efcbd7d Render: a long dashed stroke is outlined only near the view after an edit
A dashed stroke of a path with more than 1,024 path elements was outlined
whole in every frame after an edit. It is now outlined in and around the
view like a solid stroke. dash_kept runs the pattern along every segment
and keeps only the dashes and dots on the segments near the view, so they
sit where dashing the whole path puts them and the pixels are the same.
A pattern too fine to draw is still drawn solid, and that solid stroke is
cropped too.

At 1600 x 1200 the frame after one anchor moves takes 14 ms instead of
102 ms on a filled closed path of 50,000 anchors with 6 pt dashes and
4 pt gaps. anchor_bench now includes that document.
2026-11-05 03:55:48 +07:00
dotCooCoo cd71257ac4 UI-5636: Preserve styled Type-range copies on canvas paste 2026-11-05 02:23:51 +07:00
dotCooCoo a5157654f4 Tests: the link-watch and place tests wait for the link scan instead of sleeping
The link-watch worker now signals a condition variable when it ends,
whether it stored its stamps or unwound, and Session::wait_link_scan
blocks until then. The place tests' idle helper waits on it once per look
in place of a 5 ms sleep under a 20 s wall-clock bound, and the engine's
link-watch and link-path tests wait on it in place of spinning under a
10 s bound. The link-watch tests date a rewritten file 2 s after the one
it replaces instead of sleeping 30 ms before writing it.
2026-11-05 02:23:48 +07:00
dotCooCoo 440b3b7444 Glyphs: overflowing vertical type keeps a chosen default glyph
Squashed from #5676 onto main.
2026-11-05 02:23:42 +07:00
dotCooCoo b0d1ca4419 Glyphs: a character with a combining mark reports the glyph it is drawn with
text.setGlyphAlternate looked for the character's glyph only in a
cluster as long as the character. A character followed by a combining
mark is shaped in one cluster with the mark, so the lookup found
nothing: under a substituting feature such as salt, `gid: null`
reported the default glyph instead of the one drawn, and a request for
the default glyph changed nothing.

The lookup now also takes a cluster that holds the character's
grapheme, and in a cluster of several glyphs takes the character's
default glyph or one of its alternates. Text that overflows its area
is shaped with the whole grapheme.
2026-11-05 01:56:16 +07:00
dotCooCoo 584b9706cc Gradient: Perceptual PDF help counts stops as written, CMYK documents included
The method help of paint.editGradient and docs/mcp.md said a Perceptual
gradient with spot stops becomes an image in PDF unless every other stop
is paper white, CMYK or gray. PDF export checks each stop's color as it is
written: a CMYK document, or an export that converts colors to a CMYK
destination profile, writes RGB and Lab stops in CMYK, so the gradient
becomes a DeviceN shading. Both texts now say so, and a test covers the
CMYK document and CMYK destination cases.
2026-11-05 01:31:24 +07:00
dotCooCoo be8438c13d Tabs: setting the stops a paragraph already has ends the typing session
text.tabs.set and text.tabs.clear return without an edit when every
targeted paragraph already holds the stops. That path skipped the
settlement Session::edit does first, so a typing session in progress
stayed open. The command now ends it first, as the Variables commands
do: the typing is its own undo step whether or not the stops change.
2026-11-05 00:55:35 +07:00
dotCooCoo f026ebe686 Tests: an RGB spot color converts with Document Color Mode and stays spot
A spot swatch defined in RGB keeps its spot flag and its own plate when
the document changes to CMYK, and its definition converts like any other
swatch: a fill that uses it and a 40% tint stroke take the converted
color. The Lab spot test's comment now says it covers a Lab definition.
2026-11-04 23:58:58 +07:00
dotCooCoo fae49d31fd Glyphs: the default glyph can be chosen back under a substituting feature
Follows #5442.

- text.setGlyphAlternate keeps the default glyph as a choice when a
  feature of the character's style (salt, a stylistic set) draws another
  glyph for it, so the default glyph is drawn. A character that is not
  laid out (overflowing text) is shaped alone in its style to decide.
  gid: null reports the glyph the style draws.
- The Glyphs panel's alternates grid shows the default glyph first when
  the font does not list it, so a chosen alternate can be put back.
- A test for a chosen character that starts a longer grapheme (f with a
  combining acute), which is not split from its mark.
- The alternates tests in the engine hold the shared font lock while
  they add fonts to the global font database.
2026-11-04 23:58:54 +07:00
dotCooCoo a9eae3a333 Appearance: Add New Fill and Add New Stroke comment names the paint they take
The comment in add_item said a new fill or stroke is a copy of the top
one. The code takes only the top item's paint, and for a stroke its
weight. The comment now says so, as the params text does.
2026-11-04 23:44:58 +07:00
ruchirguitarandClaude Opus 5.5 6e1c1c9377 Select by Artboards: test that a symbol instance is measured by its art (#5525)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 22:57:59 +07:00
ruchirguitarandClaude Opus 5.5 7e016a0bcc Select by Artboards: select objects overlapping, inside or outside several artboards (#5525)
addon.select.byArtboards {artboards, relation} in a new Selection Tools
add-on. Overlapping is All on Active Artboard's test, now one shared
helper (select::on_artboard); Fully inside is within one artboard, edges
included; Fully outside is on none. Objects are those Select All takes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 22:57:57 +07:00
dotCooCoo aee5671734 File formats: Pixar tile pointers into the header or tile table are refused
A nonzero tile pointer below the end of the tile pointer table made the reader decode header or table bytes as pixels. The table end is now computed with checked arithmetic, and such a pointer is refused as damaged image data.
2026-11-04 22:57:53 +07:00
dotCooCoo 88b5170622 Glyphs panel: alternates for the current text selection
Squashed from #5442 onto main.
2026-11-04 22:29:53 +07:00
dotCooCoo 0990bfb244 .ai save: the editing copy keeps transparency, clipping and opacity masks
The editing copy of the art that a PDF-compatible .ai carries now holds:

- Transparency: `mode opacity isolate knockout shape Xy` (the blending
  mode's number, Knockout Group 0 off, 1 on, 2 neutral), written when it
  changes: in a path's paint after its colors, before a guide, and after
  a group's end followed by `0 0 Xd` and `6 () XW`. A group without
  transparency of its own ends `9 () XW`, and a group's name comes
  before its state block. A layer's transparency follows its art: `Xy`,
  `0 0 Xd`, `7 () XW`, `LB`.
- Clipping groups as `q`, the art, the clipping path or compound path last
  with `h` (or `H`) and `W` before its painting operator, `Q`, as section
  5.6.2 of the version 7 specification shows them
  (http://mercator.elte.hu/~saman/hu/okt/AI7FileFormat.pdf). A clipping
  layer whose clipping object is a path or compound path sets the last
  `Lb` flag and writes its clipping path last.
- Opacity masks as an `AI9_Mask` entry in the object's dictionary: the
  mask's art between `X=` and `X+`, and Clipping, Inverted, Disabled and
  Linked.

Before, clipping groups were written as their art cut to the clipping
path, and transparency and opacity masks were left out. Still left out,
with a warning: the opacity and blending mode of single fills and
strokes, opacity masks on layers (disabled ones too), and opacity masks
nested more than 8 deep. A clipping group or layer whose clipping object
isn't a path or compound path is written as a group of the art cut to
that object, in a layer that doesn't clip, and so are the clipping
groups inside that art. A clipping path's paint that a plain fill and
line can't draw is written as paths inside the clipping, fills under the
art and strokes over it, each side in a group of its own when it has
several, with the clipping path's transparency, lock and hidden state;
the clipping path's opacity mask stays on the clipping path, with a
warning. A clipping group without members is an empty group, and an
empty clipping layer is an empty layer.

Each of these is written in the form VectorCraft's .ai reader reads, and
the tests read the written data back through that reader.

Reading .ai editing data: an `Xy` after a layer's last group or compound
path, followed by the layer's `7 () XW`, is the layer's (a following
`6 () XW` keeps it the group's); groups, compound paths and layers keep
Opacity & Mask Define Knockout Shape.

The `.ai` option `editingData` and docs/file-format-parity.md describe
what the copy now holds.
2026-11-04 22:29:43 +07:00
d8abyte-dkandClaude Opus 5.5 b9594b233c Plug-ins: color parameters, and filters that make guides and lock
A `color` parameter shows a color button that opens the color picker in plug-in
dialogs and the Appearance panel's effect editor; its default and values may be
a color object or a hex string, and the plug-in gets a color object. A filter's
returned objects may set `guide` and `locked`; objects it locked or made guides
are left out of the selection after the run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 22:29:38 +07:00
dotCooCoo 3c80b314e1 Select: All on Same Layers takes the layers of objects that can be selected
Select > Object > All on Same Layers took the layer of every selected
object, including one that select.set had kept while it is hidden or
locked, on a template layer, on a locked artboard, or outside the
isolated group. That object's layer then decided which art was
selected. The command now takes the layers only of the selected
objects that the Select commands could select. When the selection
holds no such object, it clears the selection.
2026-11-04 22:29:22 +07:00
dotCooCoo eb03e8943f Gradient: PDF shading notes name linear and radial gradients and DeviceN
The PDF writer writes a spot gradient as a Separation or DeviceN shading
only when it is linear or radial; freeform gradients and dithered fills
are written as images in process colors.

- `paint.editGradient`'s method help and the Method paragraph in
  docs/mcp.md limit the Perceptual exception to linear and radial
  gradients.
- The Linked gradient stops paragraph in docs/mcp.md no longer says the
  writer has no DeviceN: a gradient mixing spot inks with other spot,
  CMYK or gray inks exports as a DeviceN shading, and only a mix with RGB
  or Lab colors is written in process colors, with a warning.
2026-11-04 21:58:59 +07:00
dotCooCoo be52e4e8af Tabs: setting the stops a paragraph already has records no undo step
text.tabs.set and text.tabs.clear return without an edit when every
targeted paragraph already holds the stops. A Ctrl-drag that the 0 pt
bound keeps from moving, or a drag released where it started, no longer
adds an undo step that changes nothing. When one targeted paragraph
holds other stops, the command still sets them all.
2026-11-04 20:51:10 +07:00
dotCooCoo 535d423d6c Appearance: Add New Fill and Add New Stroke params text match the code
appearance.addFill copies the active fill row only when the call has no
`ids`, no `id` and no `target: "contents"`; the params text now names
`id` too. appearance.addStroke gives the new stroke at least 1 pt, so a
0.5 pt top stroke gives a 1 pt stroke; the params text now says so.

Tests pin both: `id` alone leaves the active row alone and puts the new
fill on top, and Add New Stroke on a 0.5 pt stroke adds a 1 pt one.
2026-11-04 20:51:07 +07:00
dotCooCoo dd41552c30 PDF export: warn about spot stops in freeform gradients without points
A freeform gradient whose field has no points of its own takes its
colors from the stops, so spot-linked stops in it are written in
process colors too. The export now warns in that case, as it does for
the other gradients drawn as images.
2026-11-04 20:36:11 +07:00
Don Omondi 33b57fb581 Batch Trace Folder: move to Object › Plug-ins as an add-on
Batch Trace Folder replaces a script, so it is now a Document Tools
add-on command, addon.document.batchTraceFolder, under Object ›
Plug-ins › Document Tools (as Export All Open Documents is), and no
longer in Object › Image Trace. Behaviour, caps and the refusal of a
folder too big to list in full are unchanged; the tests move with it.

The status messages have @msgplural rows in every catalog, as does the
menu label. When some images trace and some don't, the status is one
counted message ("Traced {n} images; not traced: {failed}") so it
translates as a whole.
2026-11-04 20:12:07 +07:00
Don Omondi 7790c37eb3 Batch Trace Folder: refuse a folder too big to list in full
list_dir cuts its listing before sorting, so a cut listing could skip any image. The batch now
stops with an error instead of tracing part of the folder.
2026-11-04 20:12:06 +07:00
Don Omondi 7c04fd8973 Fix #4921: Image Trace > Batch Trace Folder
imageTrace.batchFolder opens each image in a folder, traces it with a
preset (optionally expanded), saves it into a destination folder under
its own name (vectorcraft, svg, pdf, ai or eps; taken names numbered so
nothing is replaced) and closes it again whether it traced or not; the
active document stays active. Object > Image Trace > Batch Trace Folder
asks for both folders. Not on the web.
2026-11-04 20:12:05 +07:00
Don Omondi ed634a78f4 Guide clipboard: Paste after a guides-only Copy takes the guides, not older system clipboard content
A Copy of ruler guides alone left the system clipboard as it was, so the
next Paste took whatever another app had copied before instead of the
guides. The guides-only Copy now empties the system clipboard (on the
web, empty text through egui with Copy As SVG on), as any Copy replaces
what the clipboard held, and drops an earlier Copy's formats not written
yet. What another app copies afterwards still pastes.
2026-11-04 20:12:01 +07:00
Don Omondi ad64852255 Guide clipboard: guides alone stay off the system clipboard; test two pastes and layers
Copying only ruler guides published an empty SVG and text to the system clipboard, replacing
what other apps put there. The clipboard now offers other apps nothing without art. Tests cover
two pastes adding guides of their own on the current layer (following its visibility) and the
SVG carrying no guides.
2026-11-04 20:12:00 +07:00
Don Omondi 2402f8e1aa Copy and paste guides: Cut with direct-selected anchors takes the guides too
With anchors and ruler guides selected, Cut deletes the anchors and the
selected guides in one undo step (edit.clear's guides: true reaches the
anchor-deleting path).
2026-11-04 20:11:59 +07:00
Don Omondi 484a78f812 Copy and paste guides: Cut takes the selected guides with the objects
Cut with objects and ruler guides selected now removes both, in one
undo step: edit.clear takes guides: true beside ids to delete the
selected guides with the objects it names.
2026-11-04 20:11:59 +07:00
Don Omondi f3faa115c6 Fix #3226: copy and paste ruler guides
Edit > Copy and Cut put the selected ruler guides on the clipboard,
with the selected objects or on their own. Pasting puts them on the
layer new art goes to, selected, in the same undo step: with objects
they move as the objects do; alone they keep their places, while Paste
in Place / in Front / in Back put them on the active artboard as they
lay on theirs, and Paste on All Artboards on every artboard.
2026-11-04 20:11:59 +07:00
dotCooCoo ac0ca081bb Commands: a color param with an alpha part is refused instead of losing its alpha
Squashed from #5603 onto main.
2026-11-04 20:08:42 +07:00
Don Omondi 1348f827d0 Area Type Options: applying to several frames keeps each frame's own options
text.areaOptions given for several area type frames (ids, or a selection of
several) copied the first frame's options onto all of them, so setting one
option (say the vertical alignment) also replaced the others' columns, gutter,
inset and fit. Each frame now takes only the options given over its own, and
fit resolves against each frame's own setting, so fitMinPercent alone changes
only frames that already shrink text to fit. Options that change nothing add
no undo step and keep redo.
2026-11-04 20:07:58 +07:00
dotCooCoo 9275b902a9 PDF/.ai import: a file whose type all comes from its text document opens without its PDF part
A PDF-compatible .ai (or a PDF saved with the editing data) now opens
from its layers without its PDF part being imported, and without being
compared with its page, when:

- its text document makes the type of every text object, hidden ones
  included, at least one text object shows, and none is drawn by the
  appearance of the layer or group it is in;
- its layers hold no unread object, no placed file with a second
  placement and no live envelope with drawn art to fall back on, and
  no image too large to read;
- it declares no large canvas, and its artboards are its pages' trim
  boxes one for one, in points or in the first page's UserUnit.

A file whose layers draw differently from its page then opens from its
layers instead of its PDF part. The bleed comes from the pages' bleed
boxes. The check runs only when no pages are picked, layers and the
editing data are on, the crop box is asked for and type comes in as
text. Files saved without PDF compatibility, files whose PDF part
draws only type or nothing, and files whose PDF part's content can't
be read take the same path when they qualify: their bleed comes from
the pages' bleed boxes, pages in units larger than a point draw them
at their size in points, and the note that the PDF part couldn't be
read is not given, since its content is not read.

Files that don't qualify open as before, and the editing data decoded
and read for the check is reused after the PDF part is imported
(layered_ai_from).

When a file whose PDF part can't be read is scaled up to its size in
points, its text wraps are now laid out again at that size, and the
default graphic styles of a file without graphic styles of its own are
no longer scaled. The same applies on the new path.
2026-11-04 19:41:56 +07:00
dotCooCoo 3f9545c962 Native files: documents from before 0.21.0 keep their Twist turning as saved
#5139 (in 0.21.0) made a positive Twist angle turn clockwise, but
documents saved by earlier releases stored their angles with the other
sign and reopened twisted the other way.

The document version (FORMAT_VERSION) goes from 1 to 2. Opening a
version 1 document negates its Twist angles (in layers, opacity-mask
art, Shaper edits, symbols, graph designs, pattern swatches, graphic
styles and brush definitions), unless the file is of the current file
version and its generator is VectorCraft 0.21.0 or later. The document
opens and saves as version 2. A save for an older file version, which
only apps before 0.21.0 read, writes a version 1 document with the
angles negated back. Graphic style libraries (.vcstyles) go to version
2: a version 1 library's Twist angles are negated when it is read.
Readers do not check either version, so older builds open the new files
as before.
2026-11-04 19:41:51 +07:00
dotCooCoo 27fc855246 Appearance: Add New Fill takes the active row's paint, as before
Add New Fill again copies only the active fill row's paint; Duplicate
Item copies a whole row. The params text of appearance.addFill and
appearance.addStroke now says the new fill or stroke takes the paint
(and for a stroke, the weight) of the row it starts from, and a test
pins the paint-only copy.
2026-11-04 19:41:42 +07:00
dotCooCoo db468928d4 Appearance: Add New Fill copies the whole active fill row
With a fill row active, Add New Fill copied only the row's paint and
reset its opacity, blend mode, visibility, overprint and effects. The
new row is now a copy of the whole active row, as the command's
description says.
2026-11-04 19:41:42 +07:00
dotCooCoo 5626a1614c Gradient: method help names the PDF Separation and DeviceN shadings
The `method` text of `paint.editGradient` and the Method paragraph in
docs/mcp.md said SVG and PDF exports draw a Perceptual gradient as an
image. PDF writes an eligible one natively: tints of one spot ink as a
Separation shading in that ink, and spot inks mixed with other spot,
CMYK or gray inks as a DeviceN shading. Gradients without spot stops,
with RGB or Lab stops, dithered fills and curves with too many stops
still become images there. Both texts now say so.
2026-11-04 19:41:38 +07:00
dotCooCoo 95b3fbfec9 Type: the font warning leaves out characters Missing Glyph Protection kept
text.setStyle and text.setRangeStyle collected the fonts for their
warning after Missing Glyph Protection had run. A character the new font
has no glyph for keeps its earlier font, and when that font was not
available the warning named it, although the command did not set it.

protect_missing_glyphs now returns the byte ranges that kept their font,
and both commands leave those ranges out of the warning's fonts.
2026-11-04 19:19:04 +07:00
dotCooCoo bc2af8df35 Appearance: the active row is matched in each selected object on its own
Reduce to Basic Appearance and Add New Fill matched the Appearance
panel's active row only when every selected object had an equal row.
With three objects whose third had a different lower fill, the second
object lost the match too: Reduce kept its top fill instead of the
active one, and Add New Fill put the copy on top instead of above the
active row.

items_per_target now matches each object against the first one
separately, so an object keeps the match whenever it has an equal row.
2026-11-04 19:19:00 +07:00
dotCooCoo af14fe3d59 Align Text Tweaker ranges to character boundaries 2026-11-04 18:56:40 +07:00
dotCooCoo a403e75802 Validate character style numeric dimensions
Squashed from #5540 onto main.
2026-11-04 18:29:25 +07:00
dotCooCoo d79f5ccb75 Layers: a layer or gap color with an alpha part is refused instead of losing its alpha
layer.setProps, layer.new, layer.newSublayer and livePaint.gapOptions read
an 8-digit hex color such as #ff000080 as #ff0000 and set it without its
alpha. They now refuse it with the error a bad color already gets, and
change nothing. #rgba was already refused. #rrggbb, #rgb, a preset index
and a preset name are read as before.
2026-11-04 18:29:17 +07:00
dotCooCoo 8762242dfe Select: describe which object Same Symbol Instance compares with
The params text of select.same.symbolInstance, docs/mcp.md and
docs/target-app-parity.md now say that the object compared with can be
of any kind (a group member selected on its own, or an object that is
not a symbol instance, which leaves the selection empty), and that new
art on a locked artboard is not passed over while it stays selected.
A test covers the three cases.
2026-11-04 18:29:09 +07:00
dotCooCoo c220c500e9 Type: Done during a Cmd press does not save empty point type
Done (pattern editing), Stop Editing Opacity Mask and Exit Isolation
Mode end the Type tool's edit first, so point type a click placed and
left empty is discarded. While a Cmd/Ctrl press had lent a selection
tool, the Type tool was not the active tool, its edit was not ended,
and the empty point type was saved into the pattern tile or the mask.

The lent tool is now given back first. A drag the lent tool started is
kept as its own step, with its last position applied when Real-time
Drawing and Editing is off.
2026-11-04 18:28:56 +07:00
dotCooCoo e67277ebe7 Hide: Unselected Artwork keeps groups that hold a selected layer or a locked guide
A layer nested in a group whose art was all locked counted for nothing,
so the group was hidden or locked with the layer inside it. The selected
layer now counts as selected itself.

While Lock Guides was on, a guide path that was locked itself, or sat in
a locked object, was not kept, so the group around it was hidden. Guide
paths inside locked objects are now kept too.
2026-11-04 18:28:44 +07:00
dotCooCoo abc2201664 Select: the font Same commands skip type on a template sublayer
Squashed from #5566 onto main.
2026-11-04 18:28:32 +07:00
dotCooCoo f0f19077df Document Color Mode: convert reusable paints and restore them with Undo
Squashed from #4517 onto main.
2026-11-04 16:55:43 +07:00
dotCooCoo 8d0f251c37 Examples: anchor_bench's doc comment says what the example prints 2026-11-04 16:55:30 +07:00
dotCooCoo 676cc0ee8e Render: a long stroke is outlined only near the view after an edit
The renderer outlined the whole stroke of an edited path on every frame,
so dragging one anchor of a path with many anchors re-outlined every
segment. A stroke line of more than 1,024 elements without dashes or a
width profile is now outlined only for the segments within the stroke's
reach of the view, inflated by the view's own size on each side. The
cached outline keeps that region, and a frame outside it outlines the
stroke again. Closed subpaths that a wide stroke crosses over stay whole,
and a cut closed subpath is walked from a removed segment so no cut lands
where it starts and ends. The pixels are the same.

At 1600 x 1200, the frame after one anchor moves takes 28 to 30 ms
instead of 103 ms on an open path of 200,000 anchors, and 10 ms instead
of 30 ms on a closed path of 50,000 anchors (the new anchor_bench
example: cargo run --release -p vectorcraft-engine --example anchor_bench).
2026-11-04 16:55:30 +07:00
dotCooCoo e3a55b97fd Type: Text Tweaker leaves byte offsets to range_of
Remove Text Tweaker's own start and end check: range_of already refuses
a bad offset given with an id, and like the other type commands Text
Tweaker now ignores start and end without one. The test's "2" case
gets an id so it still reaches the check, and docs/mcp.md lists
addon.type.textTweaker with the commands that refuse such offsets.
2026-11-04 16:55:22 +07:00
dotCooCoo 595db10900 Type: refuse negative or fractional byte offsets in text commands
text.startList read a negative or fractional `at` as the end of the
text, so it could remove a typed list marker there and start a list.
The `start` and `end` of text.editRange, text.getRange,
text.setRangeStyle and the type shortcut commands given an `id` read
such values as the start or end of the text.

These now return "`at` must be a whole number" (naming the key) and
change nothing. Omitted or null keys keep their defaults, and 2.0 is
read as 2.
2026-11-04 16:55:22 +07:00