Commit Graph

1427 Commits

Author SHA1 Message Date
bflatastic 988f52ecc8 Fix #1423: an Affinity text frame too short for its first line is made taller instead of showing nothing 2026-10-12 23:42:56 +07:00
dotCooCoo ffa948dcc3 Part of #1429: Edit and Object commands on many objects take time linear in the document
Delete, Cut, Group, Make Compound Path, Make Clipping Mask, Send to
Current Layer, Ungroup and Release Compound Path removed, inserted or
moved one object at a time. duplicate_in inserted one copy at a time;
Duplicate, Copy in the Move, Rotate, Scale, Reflect and Shear dialogs,
Transform Again after a copy, Transform Each with Copy, the perspective
copies and each move of an Alt-drag call it. Each insertion or removal
moved the later objects of the same layer or group to other indices.
The next lookup by id then walked the tree, and after every fourth such
walk Document::index_path rebuilt the whole lookup cache. Each command
did work that grew with the number of objects times the size of the
document: with 25,000 rectangles selected, a release build of
vectorcraft-cli took 7.2 s to group them and 11.5 s to copy them with
object.transform.

Document::remove_many, Document::wrap and Document::insert_beside
remove nodes, wrap them in a new node, or put nodes directly above or
in place of their anchors, in one pass over the children of each
parent. Each finds the nodes with one index_paths call, checks every id
before it changes anything, and clears the lookup cache once. The
commands above use them. The objects keep their order, the copies take
their ids in the same order, and the selection, the undo step and the
journal are the same. duplicate_in still copies one root at a time when
a root is listed twice or lies inside another root. A release build
with this change groups the 25,000 rectangles in 27 ms and copies them
in 121 ms.

Docs: docs/target-app-parity.md.
2026-10-12 23:42:43 +07:00
bflatastic 9e4ed2d430 Catalog rows for the trusted editing data note; the fuzz reads hostile editing data trusted too 2026-10-12 23:08:58 +07:00
EricKuck 42c923d62a .ai import: editingData "always" and convert --editing-data open a file from its editing data even where its page draws differently 2026-10-12 23:07:25 +07:00
EricKuck 939bc02f6b .ai import: art styles, a clipping path's own fill, compound paths of looks and a group's own transparency from the editing data 2026-10-12 23:07:05 +07:00
EricKuck 77120ada62 .ai import: radial and skewed gradient matrices, focal points and stop order, gradient meshes and artboard names from the editing data 2026-10-12 23:06:51 +07:00
EricKuck 5b413448d9 SVG export: knockout groups of opaque art draw in order, without masks 2026-10-12 23:06:34 +07:00
EricKuck 34b3c59a03 PDF import: an editor's .ai keeps its RGB numbers, and a fill faded or masked by its group keeps its stroke apart 2026-10-12 23:06:19 +07:00
bflatastic 4d17702536 Fix #1412: the Transform dialogs preview on the canvas, and arrow keys step their angles and percentages 2026-10-12 21:01:15 +07:00
bflatastic a41d59604b Fix #1408: the Transform tools and commands act on the anchors selected with Direct Selection 2026-10-12 21:01:15 +07:00
bflatastic 003234a056 Fix #1410: an .ai compound path takes its last path's paint, and paths grouped in it stay its paths 2026-10-12 21:01:15 +07:00
BFlat 54afb67b4f
Merge pull request #1409 from vbasky/fix/1396-fat-stroke
Fix #1396: a stroke wider than its closed shape paints solid
2026-10-12 20:36:00 +07:00
BFlat 22f14bec24
Merge pull request #1404 from vbasky/fix/1385-paste-picture
Paste the picture when the clipboard text beside it is blank.
2026-10-12 20:22:21 +07:00
bflatastic 1168c319b0 Close the hole whenever the inner offset of a closed stroke has crossed over
The check added for #1396 dropped the inner contour only when the path itself fell in the hole, which takes a stroke more than twice as wide as the shape. Between one and two times the size (a 30 pt ellipse with a 50 pt stroke), and on rectangles with round or bevel joins, the hole stayed.

An offset contour is now left out when no point of it lies half the weight from its subpath: it is then nowhere the edge of the stroke. Subpaths are judged one by one, and only when the stroke is at least as wide as the subpath's bounds, so ordinary strokes take the same single call as before.
2026-10-12 20:22:07 +07:00
BFlat c26ff9a22a
Merge pull request #1402 from vbasky/fix/1229-corner-bounds
Hide the selection bounds while a corner-radius drag is underway.
2026-10-12 20:22:04 +07:00
Vikram Bhaskaran aed5d380e9 Cover a small rectangle in the fat-stroke test. 2026-10-12 20:09:53 +07:00
Vikram Bhaskaran b4c1cdbcea Fill the hole a stroke leaves when it is wider than the closed shape.
A closed path stroked wider than it is came back as a ring, because the inner offset crossed the middle. When that outline no longer covers the path, the outer contour is kept so the stroke paints solid.
2026-10-12 20:09:53 +07:00
bflatastic ec38b00a22 object.isolate: an id that doesn't exist is reported as such 2026-10-12 20:06:04 +07:00
bflatastic a366e8ee0d Part of #1389: a compound path's members say so instead of claiming no paint; docs 2026-10-12 20:05:34 +07:00
bflatastic e72a010e1c Merge remote-tracking branch 'origin/main' into K-1397ff 2026-10-12 20:05:33 +07:00
bflatastic 7f5d8a3db1 Fix #1387: a .ai whose PDF part is only text opens from it 2026-10-12 19:50:53 +07:00
bflatastic bfb173e967 Fix #1388: PDF import keeps overprint 2026-10-12 19:50:53 +07:00
Vikram Bhaskaran f908545204 Paste the picture when the clipboard text beside it is blank.
Hosts put whitespace in the text flavour ahead of the bitmap, and that empty text was taken as the paste.
2026-10-12 19:50:12 +07:00
Vikram Bhaskaran bf2b5643dc Hide the selection bounds while a corner-radius drag is underway.
The sharp outer box stayed on screen because a corner drag was not treated as a transform.
2026-10-12 19:50:12 +07:00
Vikram Bhaskaran 1b79120568 Report compound-path paint and a non-normal blend in document summaries.
document.inspect skipped fill and stroke on compound paths because they are containers, and it never wrote the blend mode.
2026-10-12 23:41:01 +11:00
bflatastic 689ad46999 A just-opened dialog gives its focus to the first enabled field, not a greyed-out one 2026-10-12 19:30:20 +07:00
bflatastic ddebe5e479 Part of #1377: Rectangular Grid and Polar Grid tool options (skew, frame, compound path, fill) 2026-10-12 19:22:36 +07:00
Ayberk A. 3073f0ab03 Isolation mode: double-clicking a single object isolates it
With Double Click To Isolate on, the Selection tool isolated groups only. A
double-click on a single path, compound path, image, mesh or placed file now
isolates it too, as the public documentation describes. Type still switches to
the Type tool; blends, envelopes, repeats and symbol instances keep their own
editing modes.

While a single object is isolated only it is hit, marqueed and taken by Select
All, new art goes beside it in its group or layer, and a double-click on the
empty canvas leaves with it selected. object.isolate accepts any object except
type.
2026-10-12 18:56:41 +07:00
dotCooCoo 412e83c825 Render: the placed art of symbol instances and inline graphics is kept between frames
The renderer drew each symbol instance by copying its symbol's art,
staining the copy for a stained instance and moving every object in it
by the instance's transform, in every frame. The copies were new
nodes, so the caches the renderer keys by a node's address (cull
bounds, path geometry, glyph layouts, live-effect geometry) missed for
every object of every instance, and the copies stayed in those caches
until they were pruned. Stroke outlines missed as well, because their
cache uses an entry only when the entry holds the node being drawn.
The renderer copied and moved the art of inline graphics in type the
same way.

The renderer now keeps the art it places in a frame cache with a
64 MiB budget, under the address of the symbol's art, the transform,
whether strokes scale with it, and the instance's stain. When the art
is a group or a layer without an appearance of its own, the renderer
copies none of it in a frame whose instances and symbols did not
change, and the objects in it hit the caches keyed by their own
address (cull bounds and paths, glyph layouts, live-effect geometry).
The stroke-outline cache keys a stroke by its address in the
appearance. In the art placed for an unstained instance, each object
without a gradient fill or stroke shares its appearance with the same
object in the symbol's art. When a symbol has two or more unstained
instances, the renderer therefore still outlines the strokes of those
objects again for each instance in every frame. Moving or restaining
an instance, or editing its symbol's art, places that art again, and
the other instances keep theirs. Other art is placed again in every
frame, as before, and so is art that holds a symbol instance, type
with an inline graphic, an image, a placed document, a blend, an
envelope, a repeat or a raster effect. Art that holds type is placed
again when the fonts change.

vectorcraft-cli bench of a symbol of eight paths (three filled and
stroked, two only filled, three only stroked) shown by 2,000
unstained instances took 51.6 ms a frame on main and 32.3 ms with
this change, at 2880x1800 on 8 threads; the same paths in 2,000
groups took 26.0 ms. The peak working set of a bench run at that size
went from 150 MiB to 83 MiB. In the tests, a renderer that keeps
placed art draws the same pixels as a new renderer, and instances
draw the same pixels as groups of their placed art.
2026-10-12 18:56:39 +07:00
dotCooCoo a9a7a4cddf Fix #1257: a dragged object reuses its filter and shadow rasters
Each pointer move of a drag applies the move to the interaction's
snapshot, so the dragged object is a new node in every frame. The
renderer kept the rasters of an object's drop shadows, outer glows and
filtered content (Gaussian Blur, Feather and the Photoshop-style
filters) only for the same node, so every frame of a drag ran those
effects again. On main, a moved frame of texture_drag_perf (a CMYK
rounded rectangle at 190.07% and DPI 1.5 in an 1800 x 1500 view) took
112 to 197 ms in a release build.

The canvas now renders with RenderOptions::interacting while a drag or
a dialog's preview is in progress. When an object misses its cached
raster in such a frame, the renderer takes the raster an earlier frame
made for an object with the same id, slot and ink at the same zoom and
rotation, made by the same effects from content of the same size (to
within 0.000001 pt). It draws the object's content into a raster
placed as that one was and compares the pixels byte for byte (only the
alpha under a shadow or glow). For Feather it also compares the points
of the clip outline, to within 0.000001 pt. When they match, the old
raster is drawn at the new place, rounded to a whole pixel, as a pan
draws a cached raster. Content under De-Interlace never takes a moved
raster, and a raster made over moved rasters counts as moved itself.

A frame rendered without interacting never draws such a raster, or a
raster made over one: the frame after the drag runs the effects again
and draws what a fresh renderer draws. The canvas's cache key includes
whether an interaction is in progress. On the desktop, the canvas
sends the frame after an interaction to the render worker when the
frame shown drew moved rasters. Each cached raster keeps the content it
was made from, within the existing 512 MiB budget. The pixels kept
with a filtered raster take as many bytes as the raster, and the alpha
kept with a shadow or glow takes a quarter as many as the shadow.

texture_drag_perf now renders a drag's frames as the canvas does, then
the frame after the drag, and that frame once more with every raster
cached. A moved frame now takes 20 to 26 ms, close to the 18 to 25 ms
of the cached frame; the frame after the drag takes 110 to 189 ms.

Docs: docs/target-app-parity.md.
2026-10-12 18:56:38 +07:00
bflatastic 0cf59a6cbd .ai import: area type shows its first line in a substituted font (follow-up to #1264)
Area type whose frame is too short for a line of the font it shows in here
drew nothing: a font that isn't available shows in the fallback font, whose
line may be taller than a frame made for one line of the file's font (overset
type isn't drawn). Where the file itself lays lines out in the frame, the
frame of the imported text object is made taller, from its top, by what its
first line needs, and the open result says how many frames were.
2026-10-12 18:52:09 +07:00
BFlat 46131bd091
Merge pull request #1391 from storytold/fix/1264-ai-labels
.ai import: type goes to its own text object across looks, hidden layers and artboards (follow-up to #1264)
2026-10-12 18:51:55 +07:00
bflatastic 8e16c8529f .ai import: type of text objects with an appearance, on hidden layers and on later artboards goes where it belongs (follow-up to #1264)
- The type in a text object's drawn look (1 /FreeUndo) is a story of the
  undo-free text document, which counts its stories on its own. It was read
  as the story of that number in the text document, so a label could show
  another object's text in another object's frame. The object itself, which
  follows the look, is the type now.
- The page's type of a text object on a hidden layer stays with that object
  (hidden) instead of going to the nearest text object that shows.
- The type on the page of a later artboard is matched where the layers have
  that artboard, not by the first page's place.
2026-10-12 18:42:18 +07:00
bflatastic 8ce63aa1d1 .ai import: a gradient with stops in the middle runs its whole ramp (follow-up to #1264)
The editing data places a gradient with matrices after Bg: Xm for a linear
gradient's whole ramp and one Bm for each pair of stops next to each other.
The reader kept the last Bm, so a gradient of three stops drew only its last
part. It now uses Xm (or, without one, the span of the Bm's from the first
stop to the last), and a radial gradient's one Bm as before. The angle and
length are in the matrices, so Bg's own numbers are used only without them.

An object's own note of its gradient after it (Bb, Bg, BB with no path) no
longer becomes the fill of the next path.
2026-10-12 18:42:16 +07:00
BFlat 885a6e38cd
Merge pull request #1381 from storytold/fix/1264-ai-placed
Fix #1264: placed (linked) files in a .ai file's editing data come in as linked images
2026-10-12 18:05:00 +07:00
bflatastic 0765bc9bcd Fix #1367: a dialog opened from a menu takes the keyboard focus every time
A dialog opened by a click is drawn in the frame of that click, and in that
frame egui takes the focus from any field the click missed. The first time a
dialog's window shows, that frame only measures it, so its first field took
the focus a frame later and kept it; every later time the field took it in
the click's frame and lost it at once. The field now takes it the frame after
the click.
2026-10-12 17:48:09 +07:00
bflatastic 39c14abd35 Fix #1264: placed (linked) files in a .ai file's editing data come in as linked images
A .ai file with placed files opened from its PDF part (no guides, no sublayers,
no hidden objects), because the editing-data reader stopped at them.

- Lexer: a placed file's preview section is one token (it was skipped).
- Reader: %AI5_BeginPlace ... ~ becomes a linked image with the file's path,
  showing the preview the data keeps of it (premultiplied ARGB, capped at
  MAX_PIXELS, its rows checked against the bytes there), in its place: the
  matrix takes the file's pixels from the canvas, where they start at the point
  of art space's origin. A placed file without a preview, a path or a matrix
  still makes the file open from its PDF part.
- The open result lists one warning saying linked pictures show the
  low-resolution preview until their files are found (rows in the catalogs).
- Engine: a link nothing is known of whose image shows a preview is read where
  it is found as the document opens, into the image's bounds, and isn't
  reported as modified.
- Tests: lexer, reader (hand-written data), engine open with the link missing
  and found, fuzz tokens.
- Docs: file-format-parity, target-app-parity, gaps, ROADMAP.
2026-10-12 17:46:01 +07:00
BFlat 0a357c68a9
Merge pull request #1379 from storytold/fix/pdf-editing-data-bleed
PDF import: a PDF saved with its editing data opens on its artboard with its bleed
2026-10-12 17:29:01 +07:00
bflatastic 9d63926291 PDF import: a PDF saved with its editing data opens on its artboard with its bleed
A PDF-compatible .ai opens on its trim boxes, its editor's artboards,
with the bleed boxes round them as the document's bleed (#1349). A PDF
saved with its editing data (#1345) keeps that data under
AIPDFPrivateData keys, which has_illustrator_data didn't look for, so
such a PDF with a bleed still opened on its crop box: the artboard took
the bleed in and the editing data was refused as being for a different
page. has_illustrator_data now finds either key. has_private_data, which
also decides how a page is clipped, is unchanged.

Test: a_file_with_a_bleed_opens_on_its_artboard_with_the_bleed_round_it
(the .ai case, and the same hand-written file with AIPDFPrivateData).
2026-10-12 16:44:16 +07:00
bflatastic 4251e337c1 Fonts: a character no font covered is looked for again after a font rescan (#992)
A character missing from every installed font was remembered as missing for the whole
session, so installing a font that has it didn't help until a restart. A rescan of the
font folders now forgets those misses, and a search a rescan overtook doesn't record one.

Taken from #992 by sanyamsingh001, without its font file probe.

Co-authored-by: sanyamsingh001 <314051937+sanyamsingh001@users.noreply.github.com>
2026-10-12 16:43:41 +07:00
bflatastic 8b96db8f0b Artboards: art flush with an artboard moves with it despite a rounding error (#1295)
A background drawn at the artboard's own position could sit 3e-14 pt outside it and
stay behind when the artboard moved, was copied or scaled its art. Art within a
millionth of a point of the artboard's edges now counts as inside it.

The fix and both tests are from #1295 by Luca Lorenz, without its change to art that
only partly overlaps the artboard.

Co-authored-by: Luca Lorenz <lucalorenzetti1993@gmail.com>
2026-10-12 16:43:41 +07:00
bflatastic 268a4452f9 Part of #1358: the Image Trace section no longer widens the Properties panel
Its View row asked for the row's whole width for its menu before its label was laid out, so
the row ran past the panel, the panel's content grew and the start of every label was cut off.
2026-10-12 16:32:43 +07:00
Diego Ospina a747daa346 i18n(es): add 371 rows other catalogs already had
Document presets, shortcut editor, print/PDF options, arrowheads,
harmony rules, Save for Web, graphic styles, file filters.

Models: Opus 5.5, Sol 6.1, DeepSeek V4 Flash
2026-10-12 16:07:18 +07:00
Ayberk A. 5a8682e45d Toolbar: centre the tool buttons inside the panel's border
The buttons were placed (WIDTH - 36) / 2 from the panel's content edge, but
the docked panel's content is narrower than WIDTH: the 1.5 pt frame stroke on
both sides and the 1 pt separator line egui reserves on the right leave 44 pt.
So the single column sat 6.5 pt from the left border and 1.5 pt from the right
(2.5 pt right of centre), and the double column's second button ran over the
right border. Centre each row on the panel's inside instead, and widen the
double column to 80 pt so its two buttons fit with 2 pt either side.
2026-10-12 15:41:47 +07:00
callmeBalloch 52e9b25371 Part of #666: Affinity rectangles keep per-corner radii and straight corners
Unlocked rectangle corners now each use their own radius and type (top left,
top right, bottom right, bottom left) instead of all following the first one,
and straight corners are cut across instead of rounded. patchy-shapes.af now
matches its embedded thumbnail (mean difference 1.91 -> 0.24) with no warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:41:46 +07:00
dotCooCoo 5d5e7a1dd2 PDF export: art beyond 1,000,000,000 pt is left out, so dev and test builds no longer abort
In builds with overflow checks (`cargo run`, `cargo test` and
`cargo xtask ci`), PDF export aborted the process when a number it
wrote rounded to -2,147,483,648 as an f32. pdf-writer 0.15 writes
-2,147,483,648.0 as an integer, and `Limits::register_int` overflows
when it takes the absolute value. krilla 0.8.2's `Surface` then fails
an assertion in its `Drop` while that panic unwinds.
`svg_hostile_markup_never_panics` in `import_fuzz` hit it with type
shifted 2,147,483,649 pt down. Paths, gradients, dash offsets, images
and artboards that far out hit it too, and so does a group around a
stroke wide enough to reach that far. Release builds have no overflow
checks.

PDF export now leaves out art whose coordinates or transform entries
lie beyond 1,000,000,000 in its own space or in the content stream it
is drawn into, and glyph lines whose spacing lies beyond 1,000,000,000
thousandths of an em. That covers paths and clipping paths, glyph
lines, images, gradients, strokes whose width, caps or miter spikes
reach that far, and the art and marks of a page whose artboard lies
that far, which is written blank. A dash offset beyond the bound is
written as its remainder after whole periods of the pattern.

The type's transform is now pushed inside the form that marks a fill
or stroke of type to overprint. The exporter checks what that form
holds through the type's transform, as it checks the same fill or
stroke without overprint.
2026-10-12 15:41:45 +07:00
bflatastic aca313f10a Part of #1264: an image on %_ lines of a .ai file's editing data is read as an image
Its samples follow a %%BeginData comment that comes after the %_ prefix, where the lexer
took it for a comment later in a line and read the samples as tokens: the rest of the file
was misread ("U closes something it didn't open") and the file opened from its PDF part.
2026-10-12 15:30:11 +07:00
bflatastic 62bf8c862c Polar Grid dialog: Hungarian and Korean rows for its two labels 2026-10-12 15:30:11 +07:00
FredApps 1dc61af4d1 Add a Polar Grid canvas-click dialog
A click with the Polar Grid tool opens the shared shape dialog (width, height, concentric and
radial dividers), as the Rectangular Grid tool does, and keeps the divider counts as the tool's
options.
2026-10-12 15:30:11 +07:00
zakariayacine 84336342f3 Live Corners: with Direct Selection, a click on a widget selects its corner
Rounding a single corner is done by selecting that corner, then
dragging its widget: a drag on a path selected whole rounds every
corner with both tools, as before. What was missing is selecting the
corner on the widget itself. With the Direct Selection tool a plain
click on a corner widget (press and release, no drag) now selects that
corner, as a click on the corner's anchor does; Shift-click toggles it.
The widget is then the only one showing, and the next drag on it rounds
that corner alone.

Unchanged: Alt-click cycles the corner kind, the Selection tool's click
on a widget does nothing, corners picked by their anchors edit together.

Test: corners::tests::a_click_on_a_widget_with_direct_selection_selects_its_corner.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-12 15:17:28 +07:00