Commit Graph
7696 Commits
Author SHA1 Message Date
Ahmed Gomaa 1d011ab8ad Render: a compound path's members are assembled once and kept with the node
A compound path was assembled from its members in every frame it was
drawn, and Node::stroke_path grew its buffer by doubling, copying the
path into ever larger buffers for a compound path of many anchors. The
renderer's geometry cache was swept only once it held more than 1,024
entries, so while an object was dragged (a new node, and layer, in
every frame) the old copies stayed alive with all their members.

Now:
- a compound path is drawn from the path the renderer keeps for it in
  its geometry cache, as a plain path already was, and is assembled
  only when the node changes;
- Node::stroke_path sizes a compound path's buffer up front from its
  members' segment counts (the same elements, in the same order);
- unused geometry entries are dropped after a few frames however few
  entries there are.

Tests:
- crates/render/src/tests/memory.rs: the second frame draws the kept
  path and the same pixels as the first and as a new renderer; after a
  five-frame move the renderer no longer holds the compound path drawn
  before it. The test fails without the renderer change.
- crates/doc/src/node/tests.rs: a compound path's path is its members'
  paths in order (open, closed, empty and single-anchor members).

Not timed; vectorcraft-cli perf was not rerun.
2026-11-03 16:03:40 +07:00
Ahmed Gomaa f0e72fb11a Selection: a click on a large compound path is culled by its control points
Every click and hover measured each path and compound path along its
curves to cull it (Node::reach_bounds_with), and a compound path was
then assembled whole to test its outline and fill. For a compound path
of thousands of members that took milliseconds per click.

Hit testing now culls paths and compound paths by the box of their
control points (which holds the curves), grown by their strokes' reach,
so the box is never smaller than the reach box it replaces. A compound
path's outline and fill are tested against only the members whose
control box, grown by the tolerance, holds the point: a member outside
that box is farther than the tolerance and winds zero times round the
point. Painted strokes still follow the whole compound path (alignment,
dashes, width profiles) and are measured along all of it, as before.

Tests (crates/doc/src/hit/tests.rs):
- a 2,000-member compound path hits exactly as the whole-path test did,
  at random points, on and next to anchors and on the edges of the
  members' boxes, for both fill rules, in Outline view and with Object
  Selection by Path Only;
- wide strokes painting past the control points are still hit, for a
  path and a compound path;
- the cull box holds the reach box for paths and compound paths.

Measured in a debug test build (one run, machine not idle): a click on
a 20,000-member compound path went from about 18 ms to about 1 ms.
vectorcraft-cli perf was not rerun.
2026-11-03 16:03:40 +07:00
dotCooCoo 84db6a54a0 Type: discard queued font wheel turns after text selection changes 2026-11-03 16:03:40 +07:00
dotCooCoo a9048ddfab Type: leave wheel input to replacement Artboard controls 2026-11-03 16:03:40 +07:00
dotCooCoo 80f51a62da Type: stop hidden font fields from capturing wheel input 2026-11-03 16:03:40 +07:00
dotCooCoo 89f3d79eec Type: clarify stable font wheel ordering 2026-11-03 16:03:40 +07:00
dotCooCoo 13ede29a5e Type: route font wheel input before scroll smoothing 2026-11-03 16:03:40 +07:00
dotCooCoo 2a95625b99 Type: step font families with the mouse wheel
Apply wheel steps over the closed family fields in Character, Properties and Control, preserving event order and one undo step per effective notch. Document the supported controls and the application choices. Fixes #5253.
2026-11-03 16:03:40 +07:00
dotCooCoo 2b29a680f3 Objects on Path: the Space widget spaces the attached objects along the path
With one Objects on Path group of two or more objects selected, the
Selection and Objects on Path tools show a square Space widget beyond the
last object, ahead along the path. Dragging it moves the last object
along the path, around its bends, as far as the pointer moves along it,
and the objects between follow evenly while the first one stays. With
Alt (Option on macOS) the objects spread from both ends toward or away
from the center of an open path, or of the first anchor of a closed one.
The distance between the objects shows next to the pointer in the
document's units; the drag is one undo step, and Escape cancels it.

- object.objectsOnPath.options takes spacing (points, or "auto", the
  default even spread) and start (points from the first anchor). A
  spacing the path has no room for is drawn smaller and kept, so a
  longer path brings it back. Older files read as Auto.
- Moving the group, Pivot, Rotate, path edits, copies and saving keep
  the Space. Scaling, rotating or shearing the group keeps the first
  and last objects where the transform takes them on the new path, live
  shapes included; a value that cannot be kept that way scales with the
  path's length.
- The Rotate All widget follows the first object when Space moves it.

Behavior: https://helpx.adobe.com/illustrator/desktop/manage-objects/arrange-objects/rearrange-and-edit-objects-on-a-path.html
and https://helpx.adobe.com/illustrator/using/objects-on-path.html.
2026-11-03 16:03:40 +07:00
ham9zah 2bc284aa25 Path type brackets: 8 px reach, hover highlight, and the nearest of a bracket and a port takes the press
Squashed from #5332 onto main.
2026-11-03 16:03:40 +07:00
dotCooCoo 2b6cbfda5d Update SVG, type, recoloring, and symbol feature records 2026-11-03 16:03:40 +07:00
dotCooCoo c633e09d3a Exclude empty runs from matching text selections 2026-11-03 16:03:40 +07:00
dotCooCoo 449dd5f5ed Preserve SVG styles with encoded attribute selectors 2026-11-03 16:03:40 +07:00
dotCooCoo 012ac29d15 Window: Tile with one document open leaves the documents under their tabs
Squashed from #5256 onto main.
2026-11-03 16:03:40 +07:00
dotCooCoo 4b683c3a98 Window: with Open Documents As Tabs cleared, new documents open in windows
Preferences > User Interface > Open Documents As Tabs was stored but did
nothing. With the box cleared, each document opened or created afterwards
opens in a floating window of its own, at the next cascade place, as
Window > Arrange > Float in Window floats it. Documents already open, and
documents open when the app starts, keep their tabs; checking the box
again leaves floating documents in their windows.

The engine records the preference on each document as it opens
(DocState::opened_as_tab), and the UI finds new documents by uid, so a
command batch that closes and opens documents, or changes the preference
between opening them, places each one as the preference was when it
opened.
2026-11-03 16:03:39 +07:00
dotCooCoo ca8aba6d6a Window: drag a document tab out to float it, and a window back to dock it
A document tab dragged more than 24 pt above or below the tab strip
floats its document in a window under the pointer and makes it the
current document (window.arrange.floatInWindow takes index and at for
this). A shorter drag, or one along the strip, leaves the tabs as they
are. A document window dragged by its title bar onto the tab strip goes
back under the tabs; the strip shows an outline while a window is over
it. A desktop document window is an OS window, so it has the document's
tab at its top: dragging that tab moves the window, and dropping it on
the main window's tab strip docks it (window.arrange.consolidateAllWindows
takes index for one document).

A document floated again opens where it is asked to, not where its last
window was.
2026-11-03 16:03:39 +07:00
dotCooCoo 2c0ef0f24e Window: Float in Window, Float All in Windows and Cascade float documents
Window > Arrange > Float in Window (window.arrange.floatInWindow) moves
the current document out of the tab strip into a floating window of its
own, and Float All in Windows (window.arrange.floatAllInWindows), a new
item, does so for every tabbed document. Cascade (window.arrange.cascade)
floats every document and stacks the windows from the upper left of the
document area to its lower right, 30 pt apart, in tab order. On the
desktop each window is an OS window; on the web and on Wayland it is a
window inside the app's window, as floating panels are. While the
control channel is attached, the windows also open inside the app's
window, so ui.click, ui.drag and ui.screenshot reach them.

The current document's window holds the canvas. The other windows, and
the main window while the current document floats, draw their documents
as Tile's panes do, and a press in one makes its document the current
one. A document's OS window that takes the keyboard focus makes its
document the current one, and keys typed there run the main window's
shortcuts. Closing a window closes its document, asking to save unsaved
changes. Consolidate All Windows brings floating documents back under
the tabs, and Tile brings them back into its grid.

Cascade is enabled with two or more documents, Float in Window while the
current document is tabbed, Float All in Windows while any document is
tabbed, and Consolidate All Windows while documents are tiled or
floating. ui.inspect reports the floating documents.
2026-11-03 16:03:39 +07:00
dotCooCoo c79fd5c132 Character Styles: Style Options leaves Figure unset for figure tags no menu entry matches
A style whose features turn on pnum or lnum alone, or pair a figure tag with
one the shown entry turns off (onum with lnum, tnum with pnum), showed a Figure
entry that does not match its tags. Choosing that entry then left the field
unchanged, so the tags could not be cleared. Figure now opens unset for these
lists, and choosing Default Figure writes -tnum, -onum, -pnum and -lnum.
2026-11-03 16:03:39 +07:00
dotCooCoo 84e344c475 Character Styles: Style Options sets character color and OpenType features
Character Style Options and New Character Style get a Character Color
section and an OpenType Features section.

Character Color sets the fill and stroke (None or a document swatch;
a global swatch or a tint of one stays linked, with a Tint from 0% to
100%), the stroke Weight, and Overprint Fill and Overprint Stroke. A
gradient or pattern paint the style already has stays until another
paint is chosen.

OpenType Features sets Figure, Position, Standard and Discretionary
Ligatures, Contextual, Stylistic and Titling Alternates, Swash,
Ordinals, Fractions and Stylistic Sets 1 to 20. A changed control
replaces only its own tags in the style's feature list, so other tags
stay, and Default Figure and Default Position are stored as their
features turned off, distinct from unset.

Empty controls stay unset, and each section's Reset Panel clears only
its own attributes.

charStyle.new, charStyle.edit and charStyle.setAttrs check a changed
fill, stroke, stroke weight or feature list: tints from 0 to 1, links
to global swatches of the document (the color follows the swatch),
weights from 0 to 4000000, and known feature tags, at most 256. Values
the style or the text it is made from already has are kept.
charStyle.duplicate keeps the copied style's values in the same way, so
a style with a language tag that is not in the list can be duplicated.

Editing a global swatch now also updates the fill and stroke stored in
character styles, so their text no longer shows as overriding the style
and text styled later takes the new color. swatch.unused counts a swatch
a character style links to as used.
2026-11-03 16:03:39 +07:00
dotCooCoo a587043373 Blend: a dropped press keeps the object it picked and selected
The press picks the first object and selects it on the button-down. A dropped press now keeps
that pick with the selection, and still puts back the picks of earlier clicks.
2026-11-03 16:03:39 +07:00
dotCooCoo e01823c2eb Gradient: a dropped or canceled point drag keeps the point selected
A drag that takes a selected freeform gradient point out of the object previews its removal,
which deselects it. A dropped press now selects again the point that was selected once the
tool took the press, and Escape reselects a point dragged out and back inside the object.
2026-11-03 16:03:39 +07:00
dotCooCoo 5d0cffbba1 Canvas: a dropped press keeps what earlier clicks picked
Escape also clears state that earlier clicks set up, so taking Escape for a
dropped press lost it in five tools. Each now drops only the press:

- Blend: the object picked first and the blend being added to are put
  back as they were at the press.
- Dimension: a dropped click picks nothing, and the points or lines picked
  before stay; a dropped drag is canceled as before.
- Width: the drag ends and the width points selected at the press are
  selected again.
- Gradient: a dropped click on a freeform gradient leaves the line being
  drawn open.
- Selection: with the pointer loaded with text, a dropped press threads
  nothing and the text stays loaded; a dropped press on a port loads none.
2026-11-03 16:03:39 +07:00
dotCooCoo a230ef854b Docs: a dropped tool press cancels the drag as Escape does
The progress line said a dropped press keeps nothing it began. A Curvature
press that adds a point and a press on another text object keep what they
did on press, as with Escape during the drag, so the line now says the drag
is canceled as Escape cancels it.
2026-11-03 16:03:39 +07:00
dotCooCoo 9932096122 Canvas: a dropped press keeps Type editing and the Crop Image box
A dropped press on the Type tool no longer ends editing and switches tools:
the type being edited stays in editing, with the caret and selection it had
before the press, and the press does not count toward a double or triple
click. A dropped handle or move press with Crop Image puts the box back as it
was at the press and keeps earlier adjustments.
2026-11-03 16:03:39 +07:00
dotCooCoo 641ac7e301 Canvas: a dropped press puts back the tool's options and keeps the path being drawn
A dropped press (a long touch, a canceled touch, a second finger) now
goes to the tool through Tool::drop_press, which takes Escape as during
a drag by default. The Pen, Smart Pen and Curvature tools override it
so the path being drawn goes on: they forget the press, and a Curvature
point the press dragged goes back where it was.

The release that follows runs only the tool's Cancel and Notify actions,
so a Selection marquee selects nothing and a Shift-drag Artboard marquee
toggles nothing. The engine puts back the persistent options the tool
had at the press (the Polygon tool's sides changed with the arrow keys),
and stores none of them.
2026-11-03 16:03:38 +07:00
dotCooCoo 2d6cf9020b Canvas: a dropped press cancels the tool's drag instead of ending it
A long touch, a canceled touch or a second finger drops the tool press in
progress. The tool used to get a plain mouse-up, which it cannot tell from a
release: the options the drag changed (the Polygon tool's sides set with the
arrow keys) were stored, and a shape tool pressed and held still opened its
size dialog.

Session::drop_tool_press undoes the press's interaction, sends the tool
Escape as during a drag, and then a release that returns it to rest without
storing the tool's options. What the tool asks of the UI along the way is
dropped.
2026-11-03 16:03:38 +07:00
dotCooCoo 49477f8be5 Canvas: keep the last move before the button-up when it is elsewhere
When the release frame held moves after the button-up, the last move
before it was dropped as the pointer's position, although the release
event carries its own position. That move's sample was lost from the
stroke. It is now kept unless it is at the button-up's position, and its
tilt and rotation still give the release's pose.
2026-11-03 16:03:38 +07:00
dotCooCoo df61ee2f84 Canvas: moves after the button-up do not join a stroke
A Pencil or brush stroke takes every pointer move of a frame. In the
frame that releases the button, the moves that came after the button-up
were sent as drags before the release, so the stroke ran out to them
and back. The moves now stop at the primary button-up, and the pen pose
for the end of the stroke is the one kept for the last move before it.
2026-11-03 16:03:38 +07:00
dotCooCoo 76f0a8c615 Measure: a Shift-drag snaps its end point on its 45° step
The Measure tool and the Measure & Inspect tool snapped the end point of
a drag without Shift, but with Shift held they only turned the pointer
onto its 45° step and kept its distance: with Snap to Grid on the
measurement ended off the grid, with Smart Guides on it did not land in
line with an anchor, and with no snapping a drag of (37.3, 12.1) measured
39.2 pt at 0°.

Both now project the pointer onto the 45° step nearest its direction and
snap it there as the Line Segment tool snaps a Shift-drawn end, so it
stays on the step: (37.3, 12.1) measures 36 pt at 0° on a 9 pt grid, and
37.3 pt with no snapping. With Snap to Point alone, the anchor the pointer
snaps to is projected onto the step instead, so the end lands on an
anchor on the step and in line with one beside it.
2026-11-03 16:03:38 +07:00
dotCooCoo 58e6ce9f02 Type: a text.setStyle stroke gives unstroked characters 1 pt
A stroke color given without strokeWidth left characters at weight 0, so
nothing was drawn. As paint.setStroke does, a painted stroke now gives a
run with no weight 1 pt, for the characters and for the label styles a
graph keeps; an explicit strokeWidth still sets the weight.
2026-11-03 16:03:38 +07:00
dotCooCoo b1dc20b811 Type: text.setStyle sets the text's stroke as well as its fill
text.setStyle takes stroke (a color or "none") and strokeWidth (the character
stroke's weight in document points, as stroke.set gives it), with keepModel
for both colors. They apply to all the text or to the start..end range, and
to graph label styles, in the same undo step as the command's other keys.
A stroke key was ignored before.
2026-11-03 16:03:38 +07:00
dotCooCoo 6228446e9e Fix #4559: shortcuts can use the ISO key next to left Shift
Squashed from #5276 onto main.
2026-11-03 16:03:38 +07:00
bflatastic a9ce928f06 Format with rustfmt 2026-11-03 16:03:38 +07:00
Dylan M. Taylor cd1afaf1cf DXF import: budget dash lengths, copied text and names, and block items looked at
A drawing could make the importer keep far more than its own size: every dashed path held its
own copy of its linetype, every text object and symbol instance of an array copied its text,
font family or block name, and each placement of a block looked at all of its items, hidden or
not, without charging the work budget.

- Dash lengths kept across the import are capped; past the cap lines are drawn solid.
- Text, font family and block-name bytes copied into objects share one 64 MB budget, and a
  font family longer than 256 bytes falls back to the default.
- Each block placement charges one visit per item it looks at, and stops once the budget is spent.
2026-11-03 16:03:38 +07:00
bflatastic 1bd47ea30d Format with rustfmt 2026-11-03 16:03:38 +07:00
Dylan M. Taylor 8e692d83ed ICC: refuse a profile whose tag table lists a tag twice
moxcms parses every entry of the tag table, so a profile listing one
description, copyright or table tag thousands of times, each pointing at
the same large data, made it decode and copy that data once per entry.
The ICC specification allows each tag signature once; such a table is now
refused before parsing.
2026-11-03 16:03:38 +07:00
bflatastic c59cd281db Format with rustfmt 2026-11-03 16:03:38 +07:00
Dylan M. Taylor 06139d0011 Export for Screens: keep PDF passwords out of remembered settings
A successful Export for Screens stored its params in the document's
export settings, which native saves write, PDF security passwords
included, although the PDF settings themselves are saved without them.
Saved export presets kept them in the preferences the same way. Both now
drop openPassword and permissionsPassword wherever they appear in the
settings, keeping the other security choices.
2026-11-03 16:03:38 +07:00
bflatastic 47bc3dd58c Flatten Transparency: iterate the test's groups directly (clippy needless_range_loop) 2026-11-03 16:03:38 +07:00
bflatastic 6b30867eba Flatten Transparency: use is_multiple_of (clippy) 2026-11-03 15:57:57 +07:00
Dylan M. Taylor e505bccd5e Format with rustfmt 2026-11-03 15:57:52 +07:00
Dylan M. Taylor b032766a66 Flatten Transparency: group overlapping objects without comparing every open pair
The overlap grouping swept left to right comparing each object's bounds
with every box still open at its left edge, so many objects sharing a
horizontal extent (stacked in a column) cost a comparison per pair
before any of the flattener's geometry limits applied, and the grouping
could not be cancelled.

Open boxes are now kept in a tree by their tops that knows the lowest
bottom below each branch, so each box meets only the open boxes it
overlaps; an open box inside a later box of its group, ending no further
right, is closed early. The grouping checks for cancellation as it goes.
Groups are unchanged: a test compares them with every-pair grouping.
2026-11-03 15:57:52 +07:00
bflatastic 4ab16b7760 Clipboard: compare the PNG scale directly (clippy neg_cmp_op_on_partial_ord) 2026-11-03 15:57:51 +07:00
Dylan M. Taylor 15cd642be8 Clipboard: leave out the PNG of art too large to scale to 4096 pixels
Copy scaled its PNG down to fit 4096 pixels a side but never below 0.01
pixels a point, so art over 409,600 pt across produced a PNG far past
that side, up to the general raster export limit of a gigabyte of
pixels, on an ordinary copy. Such art now gets no PNG flavour.
2026-11-03 15:57:05 +07:00
JonRC 400a49666a Pending filters read the file they were made over
A filter over `currentfile` waits until it is first read, and it took
its data from whichever file was running by then. One made over the
program's own file and first read inside a file run with `cvx exec`
read that file instead, and one made over a file being run, first read
in a file that one runs, read the inner file. The pending stream now
records the file it was made over (a chained filter inherits it), and
`materialize` decodes from that file.
2026-11-03 15:57:03 +07:00
JonRC 6fc82a141f Fix #5321: currentfile in a file being run is that file
A file run with `cvx exec` was read whole and run as a separate
program, while `currentfile` stayed the outer EPS. The PLRM has
`currentfile` return the topmost file being run, positioned after the
last token read. Ghostscript's eps2write runs each page's content and
each Type 3 glyph from a SubFileDecode filter, and its `ID` reads the
inline image's samples through `currentfile`, so they were read from
the outer file and then ran as program.

A file being run is now read object by object from its own data, and
while it runs it is `currentfile`. A filter made over it waits to
read, as one over the program's own file does, so it starts where the
program is when the filter is first read.
2026-11-03 15:57:03 +07:00
JonRC d627a776de Cite the PostScript Language Reference by its public URL 2026-11-03 15:57:00 +07:00
JonRC 8fb394ca16 Fix #5318: known answers false for keys no dictionary holds
`known` raised a typecheck for any key that can't be a key here (an
array, a dictionary, a real with a fraction). The PLRM allows any
object but null as a key, and since `put` refuses such keys, no
dictionary holds one: `known` now answers false, and null still raises
the typecheck. Ghostscript's eps2write prolog looks each inline-image
value up in a dictionary of abbreviations, and an image with two
filters has an array there.
2026-11-03 15:57:00 +07:00
JonRC eca414044b Cite the PostScript Language Reference by its public URL 2026-11-03 15:56:58 +07:00
JonRC 097b013ea7 Fix #5316: StandardEncoding and ISOLatin1Encoding are Encoding resources
The standard encodings were names in systemdict only, and the Encoding
category was empty, so `/StandardEncoding /Encoding findresource`
failed with undefinedresource. The PLRM (3rd ed.) has both as
instances of the Encoding category, the same arrays as the names.
Ghostscript's eps2write prolog takes StandardEncoding that way for a
font whose encoding is a Differences dictionary with no BaseEncoding,
so files with its Type 3 text stopped there.
2026-11-03 15:56:58 +07:00