Commit Graph
8546 Commits
Author SHA1 Message Date
el-j 02a8928f8e M2.1: copy and cut direct-selected segments and anchors as independent paths (#2122)
Squashed from #3903 onto main.
2026-11-05 19:40:58 +07:00
el-j 08ff029ac2 fix(pathops): bound offset path complexity and handle arrangement failure safely (#4145)
Squashed from #4345 onto main.
2026-11-05 19:40:58 +07:00
BFlat c98007a43d Merge pull request #5792 from dotCooCoo/fix/ai-legacy-ts-te
.ai import: type with the older TS, Te and TM operators opens
2026-11-05 19:40:48 +07:00
dotCooCoo ab86a62ea0 .ai import: type with the older TS, Te and TM operators opens
The version 7 and version 3 file format specifications list three text
operators the editing-data reader did not dispatch, so a file whose type
used any of them on a visible layer did not open from its editing data:

- `(text) justified TS` (special characters): the reader adds its
  characters to the story, as it does for `Tx` and `Tj`.
- `Te` (end render) is read and ignored. The reader accepts it with any
  number of operands.
- `TM` (pop text matrix) is read and ignored.

Tests cover each operator in a text object, `TS` without a string and
outside a text object, and `Te` inside a tab definition. The import fuzz
tests add the operators to the legacy type tokens.
2026-11-05 19:28:59 +07:00
BFlat c547dacad6 Merge pull request #3071 from KnottyDyes/squad/raster-export-default-ppi
Export: raster output defaults to the document resolution
2026-11-05 19:14:05 +07:00
BFlat 184c591d56 Merge pull request #5791 from sadkotheguest/feat/layers-panel-nonprinting-italic
Layers panel: a layer that does not print shows its name italic
2026-11-05 19:13:48 +07:00
KnottyDyesandClaude Opus 5.5 65b8192b7e Docs: a document at another resolution exports at it, not 72 ppi
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 19:02:23 +07:00
bflatastic 3f940efae1 Layers: the italic layer comments name Layer Options › Print 2026-11-05 19:02:23 +07:00
KnottyDyes c9588457b9 Export: raster output defaults to the document resolution 2026-11-05 19:02:23 +07:00
Alex 82251c8e8b Layers: a layer that does not print shows its name italic (#5739)
Layer Options › Print off means the layer's art does not print. The Layers panel now paints
that layer's name italic, as Illustrator shows non-printing layers, and the ghost of a row
being dragged keeps the italic. A printing layer's name stays upright, and ticking Print
again restores it.

Row names are painted rather than widgets, so the italics come from a layout job's text
format (egui slants the glyphs itself; no italic face is needed).

Tests: panels::layers::tests::a_layer_that_does_not_print_has_its_name_italic covers Print
off, Print on again, and a plain printing layer; the layers suite (64 tests) passes.
Docs: roadmap, target-app-parity and ui-parity carry the change.
2026-11-05 19:02:23 +07:00
BFlat 2030536afe Merge pull request #5790 from dotCooCoo/fix/ai-os22-g13
.ai import: 1-bit image masks open colorized by their fill color
2026-11-05 19:02:13 +07:00
dotCooCoo d5473acb09 .ai import: 1-bit image masks open colorized by their fill color
The version 7 file format specification gives `XI` and `XF` an
ImageMask operand: 0 is an opaque image, 1 is an image that is
transparent and can be colorized (sections 8.1, 8.2 and the 1-bit
examples in 8.4,
https://13thmonkey.org/documentation/fonts/AI7FileFormat.pdf). The
reader ignored the operand, so a 1-bit image mask opened as an opaque
black-and-white image.

A 1-bit gray image with ImageMask 1 now opens with the fill color set
before it as the color that colorizes it. A spot or global color keeps
its swatch and tint. When the fill is a pattern, a gradient or none,
the image is colorized black. VectorCraft's own rule is that the pixels
that open black with ImageMask 0 take the color and the others are
transparent. A linked image (`XF`) keeps the same color for when its
file is relinked. 8-bit gray, RGB and CMYK images, and images with
ImageMask 0, open as before.
2026-11-05 18:50:05 +07:00
BFlat ce31f57645 Merge pull request #5789 from dotCooCoo/fix/ai-clip-group-masks
.ai import: several clipping paths clip to their intersection
2026-11-05 18:49:56 +07:00
dotCooCoo 68f6f8755b .ai import: several clipping paths clip to their intersection
In `.ai` and EPS editing-data import, a clipping group, layer or opacity
mask's art that holds more than one clipping path (`W`) now clips to the
intersection of the paths, as each `W` intersects the current clip (AI7
spec section 5.6.2,
https://13thmonkey.org/documentation/fonts/AI7FileFormat.pdf). Before,
the paths were joined into one compound path, which clipped to their
union, and kept a stroke only when every path had the same one.

Each clipping path after the first clips a clipping group nested inside
the one before it, and keeps its own stroke, opacity and blend mode. The
innermost clipping group holds the art in the order written; as before,
every object in the group is clipped by all of its clipping paths. A
clipping path's painted fill stays at its path's level. A compound path
of clipping paths (`*u` ... `*U`) is still one clipping path. Clipping
paths that would nest the art past the document's 32 levels stop the
editing data from being read.
2026-11-05 18:37:23 +07:00
BFlat be886d3b04 Merge pull request #5773 from dotCooCoo/fix/find-font-warning-preserve-postmerge
Refresh Find Font warnings after document changes
2026-11-05 18:37:14 +07:00
dotCooCoo fdde506ec7 Refresh Find Font warnings after document changes
Squashed from #5773 onto main.
2026-11-05 18:23:06 +07:00
el-j e6ac6cd3ba perf(pathops): build Trim and Crop on each_classified_face (#4503) (#4511)
Thanks @el-j — merging.

**Summary:** Pathfinder Trim/Crop (and Merge) now build on `each_classified_face` (generalizing `each_face`) so classification and face construction share one batched pass, dropping the duplicate `batched_classified_contours` after #4375 as @bflatastic asked. Differential tests lock subpath order and the fully-covered shape case.
2026-11-05 12:22:02 +01:00
BFlat 7436b4be59 Merge pull request #5787 from sadkotheguest/fix/floating-palette-over-modal
Panels: a floating palette keeps out of the way of modal dialogs and menus
2026-11-05 18:19:33 +07:00
Alex 347b7e46bc Panels: a floating palette keeps out of the way of dialogs and menus
Floating panels are OS windows of their own, and craft-ui keeps a palette above the
app's other windows while it is active. A modal dialog or an open menu is drawn in
the main window, so a floating panel covered it: the Colour Picker's OK button and
fields, Recolor Artwork, and the menu bar's drop-downs (#5238, defect 1).

While a modal dialog or a menu of the in-window menu bar is open the docking renderer
hides each palette window, and when neither is open each palette shows again at its
palette level: topmost while the app is active, behind other applications' windows
while it is not, as craft-ui leaves a palette it creates. Hiding rather than lowering
covers both: a lowered window can be clicked back to the front, and craft-ui varies
the level through the viewport builder, which egui applies when the window opens and
not to one already on screen, so the commands go explicitly to the windows there are.

The menu bar tells the docking renderer whether one of its top-level menus is open,
the same state its hover switching uses; egui's own "any popup open" misses the
in-window menu bar.

Measured on Windows with the app focused and panels floating: before, a palette was at
the front of the z-order and topmost over the main window with the Colour Picker open;
after, the main window is in front and the palette is hidden until the dialog or menu
closes.

Tests: a_modal_lowers_the_native_palette_windows and
a_menu_of_the_menu_bar_lowers_the_palette_windows check the commands sent, and
a_palette_window_keeps_out_of_the_way_of_the_main_windows_dialog_and_menus the level
and visibility table. The wheel test
`wheel_over_a_floating_character_font_field_changes_the_font` fails on this machine
without this change too (font-order dependent), so it is not part of it.
2026-11-05 18:11:00 +07:00
BFlat f82e174fe5 Merge pull request #5786 from dotCooCoo/fix/ai-gradient-name-serial
.ai save: new gradient names continue from the last one given
2026-11-05 18:10:50 +07:00
dotCooCoo 6fef93de8f .ai save: new gradient names continue from the last one given
Each gradient definition that no swatch names took its name by trying
`Gradient`, `Gradient 2`, `Gradient 3`, ... from the start, so a document
with many such gradients tried every earlier name again for each new
one. The writer now keeps the number the next search starts from, and
still skips names that a gradient, swatch or pattern already has.

- A test names 1,000 gradients around two taken names and checks that
  each name is tried once (1,002 tries; 502,496 when each search starts
  over).
- An export test writes 300 gradients next to a `Gradient 3` swatch and
  a `Gradient 7` pattern, and checks the names in order and the same
  output when written again. It passes with the old search too.
2026-11-05 17:59:03 +07:00
BFlat 064b9b3ef8 Merge pull request #5784 from dotCooCoo/feat/ai-editing-data-s4
.ai save: the editing copy keeps images and linked files
2026-11-05 17:52:44 +07:00
dotCooCoo 653e6ff7fb .ai save: the editing copy keeps images and linked files
The editing copy of a PDF-compatible .ai wrote every image as its outline
filled with its average color. Embedded images are now written with their
pixels, and linked images as placed files with a preview:

- Embedded images are rasters in the form of the version 7 specification
  (section 8, https://13thmonkey.org/documentation/fonts/AI7FileFormat.pdf):
  %AI5_BeginRaster, the color space, the image matrix and the XI operands
  of current files, then the samples in binary in a %%BeginData section.
  Gray, RGB and CMYK images keep their samples (CMYK images their ink
  amounts), and see-through images their alpha channel after the color
  samples. The matrix is written to ten decimals.
- Linked images are placed files (section 16): the matrix, the box of the
  file's pixels and its path, %%IncludeFile, the folder relative to the
  document's (when the relative path ends in the path's file name), and a
  preview of at most 256 pixels a side.
- Images that can't be decoded or have more than 33,554,432 pixels (the
  most the reader reads) are written as their outlines filled mid gray,
  images of 16 bits a channel (CMYK TIFFs included) at 8 bits, and linked
  images in opacity masks and pattern tiles as embedded images, each with
  a warning.

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

The reader now gives an image its name and opacity mask when the
operators that end its raster (XH, Xr and the N after %AI5_EndRaster) come
before its dictionary, reads an empty relative folder as the document's
own folder, and looks for the %%EndData that ends an image's samples
after the alpha channel its XI operands give it, preferring one whose next
line is XH, so samples that hold the marker's bytes are read whole.

Tests: the round trip of every kind of image and its state, a second write
of what is read back giving the same bytes, the raster and placed-file
forms, previews of large linked images, and the engine's editing copy with
every save option; a fuzz case for damaged images and placed files.

Docs: docs/file-format-parity.md.
2026-11-05 17:38:55 +07:00
BFlat 88496dcefd Merge pull request #5783 from dotCooCoo/fix/ai-os22-g8
.ai import: area type with linked areas opens as threaded frames
2026-11-05 17:31:42 +07:00
BFlat 250098e19f Merge pull request #5782 from dotCooCoo/fix/stale-pointer-requests-frame-limit
MCP: given-up stale pointer requests do not use up a frame's request limit
2026-11-05 17:31:27 +07:00
BFlat 6f0e15452c Merge pull request #5781 from storytold/perf/1280-bounds-nan-close
#1280: PathData::bounds closes a NaN anchor as kurbo does; the perf row names what it times
2026-11-05 17:31:11 +07:00
dotCooCoo 48f0347351 .ai import: area type with linked areas opens as threaded frames
In the legacy format, area type whose story flows through several linked
areas has one text path (`Tp` ... `TP`) for each area, each with its own
matrix and frame (version 7 specification, sections 15 and 15.5.3). The
reader kept only the first frame, placed it with the text object's last
valid matrix, and put the whole story in that one frame.

Each linked area with a frame now opens as its own area type object,
placed with its own matrix, and the frames are threaded in file order.
The first frame holds the story: the characters after every `TP`, and
the overflow text. When the file opens, the story is laid out again
through the thread (section 15.1 lists the flow from area to area as
final-form information). A linked area without a frame is left out.
Area type with one area opens as before.

The dictionary that follows a text object (its opacity mask, text wrap,
name and other keys) went to its one object. Its opacity mask and text
wrap now go on every frame, and its other keys on the first frame.

Point type is read with its first text path's matrix, and type on a
path with the matrix and start of the first text path that has a path.
Area type with no frame at all opens as point type with its first text
path's matrix. Before, these used the last valid matrix and, for type
on a path, the last start.

One text object reads at most 4,096 text paths. The number of frames
times the story's character count is kept to 4,194,304, so a longer
story keeps fewer frames. Frames past either limit are left out, with a
warning.
2026-11-05 17:17:36 +07:00
dotCooCoo 7e97255334 MCP: given-up stale pointer requests do not use up a frame's request limit
A ui.pointer request made before synthetic input was dropped counted
toward the per-frame request limit even when its caller had already
given it up and no reply was sent. A backlog of such requests could hold
live requests back for a frame. It now counts only when the app claims
it and replies with the error, as the other given-up requests are
skipped.
2026-11-05 17:17:36 +07:00
FredAppsandClaude Opus 5.5 c1f7e4ed6d #1280: PathData::bounds closes a NaN anchor as kurbo does; the perf row names what it times
Follow-up to CodeRabbit's review of #5778.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:17:36 +07:00
BFlat b13335d188 Merge pull request #5780 from dotCooCoo/fix/font-wheel-raised-layer
Keep font wheel input with the top native panel layer
2026-11-05 17:17:22 +07:00
BFlat 8bd84206fa Merge pull request #5779 from dotCooCoo/fix/ai-os22-g7
.ai import: version 7 linked images open as linked images with a placeholder
2026-11-05 17:17:07 +07:00
dotCooCoo 89875f1a6b Keep delayed font wheel turns with the current field 2026-11-05 17:02:57 +07:00
dotCooCoo c5269bd096 .ai import: version 7 linked images open as linked images with a placeholder
A legacy .ai in the version 7 form names a linked image's file with
`(path) modified XG` and draws the image with `XF`, which takes the
operands `XI` takes and has no samples after it (Adobe Illustrator File
Format Specification 7.0, sections 8.2 and 8.3). The reader ignored both
operators, so the image was dropped without a warning.

- `XF` after `XG` becomes a linked image of that file, its pixels placed
  as an `XI` image of the same operands is placed. The file keeps no
  pixels of it, so a placeholder stands in until the file is relinked in
  the Links panel, with a note in the open result. No pixels are read,
  so the image is not held to the pixel limit of `XI`'s samples.
- An `XF` without a file name before it, or with operands `XI` couldn't
  read (no matrix, too few numbers, no pixels, or samples of other than
  1 or 8 bits), is left out with the existing "an image VectorCraft
  can't read was left out" warning. An `XG` applies only to the image
  after it in the same raster object; `() 1 XG` before an embedded `XI`
  keeps that image embedded.
- A PostScript page draws nothing for `XF`, so there the placeholder's
  area is left out of the comparison of the layers with the page, and
  the file keeps its layers. A PDF part draws the image's pixels, so a
  file with one is compared with it there as before, and its layers are
  not used without the PDF part.
- Linked images of different files get image keys of their own even
  when they show the same pixels (the placeholder, or two identical
  placed-file previews), so updating or exporting one file's image
  reads that file only.
- `vectorcraft_doc::links::missing_placeholder` draws the placeholder: a
  light tile, crossed out, as SVG import shows for a missing file.

Tests: reader tests for the placement, the image keys and the cases left
out, an end-to-end test of a legacy file with a linked image, a test of
a file with a PDF part, and a fuzz case for damaged files with `XG` and
`XF`. The new note has a row in each catalog that translates the
placed-file note.
2026-11-05 17:02:57 +07:00
dotCooCoo d0069d7134 Keep font wheel events with their overlay target 2026-11-05 17:02:57 +07:00
dotCooCoo 1d00259168 Preserve font wheel turn order across pointer leaves 2026-11-05 17:02:57 +07:00
dotCooCoo 98f0c69a1c Restore floating viewport pointer after wheel replay 2026-11-05 17:02:57 +07:00
dotCooCoo 38354d7864 Keep floating font wheel input with the visible layer 2026-11-05 17:02:57 +07:00
dotCooCoo a5ac92d056 Defer floating font wheel input until layer order settles 2026-11-05 17:02:57 +07:00
BFlat 5ca8ef49e2 Merge pull request #5778 from storytold/perf/1280-bounds-without-bezpath
#1280: perf --deep measures pointing; PathData::bounds builds no path
2026-11-05 17:02:47 +07:00
FredAppsandClaude Opus 5.5 0a8566e1b0 perf: the import comes first
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:49:06 +07:00
a33d83d84f #1280: perf --deep measures pointing; PathData::bounds builds no path
First slice of https://github.com/storytold/vectorcraft/pull/1281, split
as the review asked. `--deep` gains rows for hovering, a hit test that
misses, marquee, smart guide targets and shift-click on a large document,
so the later slices have a baseline.

PathData::bounds built a kurbo path on every call. It now measures the
segments in place, with the same guards, and a proptest compares the
result bit for bit with what the path gave.

Co-Authored-By: arhammusheer <arhammusheer@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:49:06 +07:00
BFlat be2228c277 Merge pull request #5777 from dotCooCoo/fix/large-canvas-eps-type
.ai/EPS import: type that names no style takes the normal styles
2026-11-05 16:48:56 +07:00
dotCooCoo 6dd86b337a .ai/EPS import: type that names no style takes the normal styles
Type in the text document of a .ai or EPS file's editing data that names
no character or paragraph style, a story without style runs included, is
set in [Normal Character Style] and [Normal Paragraph Style]. The reader
took what such type leaves out from the text document's base attributes
instead, or built-in defaults for a story without paragraph runs, and it
did not read paragraph styles.

A Large Canvas file (1:10) holds the normal styles' lengths at the art's
scale (1.2 for 12 pt) and the base attributes' at the page's (12). The
reader scaled the base attributes' 12 by 10, so the type came in at
120 pt. In an EPS the page comparison then rejected the layers, and the
file opened as its printed page without its linked images.

In any file whose normal styles differ from the base attributes, such
type now takes what the normal styles set, such as font, size, colors,
stroke, alignment, indents, space before and after, and hyphenation.
2026-11-05 16:37:13 +07:00
BFlat 413446fdc1 Merge pull request #5775 from dotCooCoo/fix/ai-object-tags
.ai import: object tags open as each object's URL and data
2026-11-05 16:17:01 +07:00
BFlat 3ac2bd6afc Merge pull request #5774 from ruchirguitar/fix/5663-batch-views
Document tabs: views stay with their documents when one batch closes and moves documents (#5663)
2026-11-05 16:16:41 +07:00
dotCooCoo a90ffe2a5f .ai import: object tags open as each object's URL and data
`.ai` and EPS editing-data import now reads object tags
(`/identifier (string) XT`) and applies each one to the object before it,
whatever kind of object it is. The `AdobeURL` tag sets the object's URL.
Every other tag is added to the object's data in order, up to 64 per
object. A tag with no object right before it, with an empty identifier,
or with an identifier or string longer than 64 KiB is ignored, and so
are tags in a pattern's or brush's art. A clipping path keeps its own
tags; the copy that paints its fill gets none.

Before, a tag on art that shows was an operator the reader did not know,
so it stopped the editing data from being read, and brush art holding a
tag was not read, so the brushes using it were left out.

An image's tags follow the `XH` that ends its data, and the state written
after a group or a gradient mesh is still theirs when their tags come
first. When a drawn look stands for an object whose style is not found,
the look keeps the object's note, URL and data, and a live blend keeps
those of its drawn steps.
2026-11-05 16:04:32 +07:00
ruchirguitarandClaude Opus 5.5 a89b44ee2e Document tabs: views stay with their documents when one batch closes and moves documents (#5663)
`sync_views` matched views to documents by uid only while the number of
documents was unchanged. A `command.batch` (or a recording) that closed a
document fell back to matching by position, so the documents after the closed
one took their neighbours' views, and a move later in the same batch was not
applied to the views. The views are now matched by uid whenever the documents
change: a closed document's view is dropped and a new document gets its saved
view. Closing from the UI no longer removes the view by hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:04:29 +07:00
BFlat 7bfcc005f4 Merge pull request #5772 from dotCooCoo/fix/discarded-batch-cancels-pointer
MCP: pointer_gesture fails when the input sent before it is dropped
2026-11-05 15:50:40 +07:00
BFlat 6ed686bae2 Merge pull request #5771 from dotCooCoo/feat/ai-editing-data-s3
.ai save: the editing copy keeps gradients and patterns
2026-11-05 15:50:24 +07:00
dotCooCoo cefb44c033 MCP: pointer_gesture fails when the input sent before it is dropped
Squashed from #5772 onto main.
2026-11-05 15:38:38 +07:00