Commit Graph
3811 Commits
Author SHA1 Message Date
dotCooCoo e492c4298c Blend: a click picks again when the Blend tool's first pick is gone
Squashed from #4447 onto main.
2026-10-29 19:21:32 +07:00
bflatastic c4d23f4af7 i18n: drop two stale Georgian rows the #4169 squash brought back 2026-10-29 19:21:32 +07:00
Taha-Mahmoodi b41599f657 Fix #3089: a stroked line with an effect expands to a stroked line, then Expand outlines it
Squashed from #4169 onto main.
2026-10-29 18:54:59 +07:00
dotCooCoo 3cfdb58b90 Transparency: pattern editing mode and opacity mask editing can't be entered from each other
Squashed from #4458 onto main.
2026-10-29 18:54:45 +07:00
dotCooCoo c8a7420b93 Place: an image whose pixels can't be read is refused instead of placed blank
Squashed from #4257 onto main.
2026-10-29 18:54:38 +07:00
dotCooCoo e764d770db Image Trace: Document Library fills link to the document's global swatches
Squashed from #4396 onto main.
2026-10-29 18:50:56 +07:00
dotCooCoo 3aebccaf06 Zoom tool: Space held while dragging moves the zoom marquee
Squashed from #4372 onto main.
2026-10-29 18:50:45 +07:00
dotCooCoo 74b408e8da Blend: choosing the Blend tool again finishes the blend
Squashed from #4454 onto main.
2026-10-29 18:50:34 +07:00
dotCooCoo 6fd042e4be Tests: the MCP concurrency, folder search and artboard export tests pass on a loaded machine
Squashed from #4473 onto main.
2026-10-29 18:50:24 +07:00
el-j 31a482bade perf(engine): rerun objects_on_path when dimension refresh mutates artwork 2026-10-29 18:46:48 +07:00
el-j 3e70c38d7b perf(engine): gate post-edit Shaper and Text-Wrap maintenance via edited-root diffing (#4554)
- In `shaper::refresh`, replace full document walks with `changed_shaper_groups`,
  diffing `before` and `after` layers and short-circuiting on pointer-equal (`Arc::ptr_eq`)
  subtrees in O(1).
- Return whether Shaper groups rebuilt or modified artwork (`Result<bool>`),
  allowing `Session::edit` to skip the second `cmd::objects_on_path::refresh`
  call when no artwork was modified.
- In `textwrap::refresh`, check pointer equality on layers against `before.layers`,
  skipping untouched layers in O(1) and checking only modified layers.
2026-10-29 18:46:47 +07:00
ham9zah 0cd82d8d7d Import: the page's type of a right-to-left or turned label is its text object's
A file's PDF part draws a right-to-left paragraph's words in the order
they show ("mm 120.5"), while its text object keeps them in the order
they are typed (" 120.5 mm "). The page's type was matched to the text
objects by their letters in order, so it matched none and stayed beside
the label as a second copy. It is now matched word by word.

A point type label turned a quarter (a vertical dimension's) lay outside
the box its text object was looked for in, which was always level; that
box now runs along the type's baseline.
2026-10-29 18:46:47 +07:00
ham9zah 2dcfbb0bf0 Import: a right-to-left paragraph of the editing data lays out right to left
The text document's paragraph key 33 (from the paragraph or the
document's defaults) said a paragraph runs right to left, but the reader
only used it to swap left and right alignment: the paragraph's direction
stayed unset, so it went by its first strong letter, and a label with no
Arabic or Hebrew letters (" 120.5 mm ") laid out left to right instead of
reading "mm 120.5". The paragraph style now gets that direction.
2026-10-29 18:46:47 +07:00
ham9zah 027884b0a4 Import: read one alternative of versioned content, not two
Versioned content can hold three alternatives: newer files write a
Dimension tool group, the same group again after
`%AI17_Alternate_Content_if_version_gt:…` for a version in between, then
plain art after `%AI17_Alternate_Content`. The reader skipped from the
plain `Alternate_Content` marker only, so the middle alternative was read
as art too and those objects came in twice.

The reader now reads the first alternative and skips from the next one,
whatever its marker, to the end of the block, nested blocks included. A
block that doesn't end is read whole with a note instead of failing the
file. The palette reader tracked alternatives with one flag that a block
nested in an alternative reset; it now tracks the depth.
2026-10-29 18:46:47 +07:00
HassanKhan 1c1ca30a11 Fix #4283: a wheel notch zooms at once without Animated Zoom
Squashed from #4313 onto main.
2026-10-29 18:46:47 +07:00
nevaremandClaude Opus 5.5 87fc61df79 Selection: a selected object's masked art is reached the way a click goes
The check of a selected object's masked art looked at its own place
only. A click goes down from the layer (or the isolated container) and
never reaches a Shaper group's original shapes outside Construction
mode, however deep the object lies in them; and below a layer whose
Preview is off it hits art in outline. A press inside such a path's
fill, or on a path in a Shaper group's hidden shapes, started a move.

The way down to the selected object is now checked as a click goes:
a Shaper group's part a click can't reach on it rules the object out,
and a Preview-off layer on it (below the isolated container) makes the
object count by its outline.

Tests also cover Shift with Direct and Group Selection (a marquee) and
an object a click hits over the masked art (it is selected).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EjfPR4a1jPek7r3nyFFNYv
2026-10-29 18:46:47 +07:00
nevaremandClaude Opus 5.5 f793547a7f Selection: a selected object's masked art counts only where a click reaches it
The selected object itself was checked for being hidden, locked or a
template, but not as a click checks it: a selected guide path was
grabbed where a mask hid it while guides were locked or hidden, though
a click on it there hits nothing.

The selected object now passes the same test a click applies to it
before its art is tried: guides while guides are locked or hidden, and
a Shaper group's hidden part, don't count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EjfPR4a1jPek7r3nyFFNYv
2026-10-29 18:46:47 +07:00
nevaremandClaude Opus 5.5 6ecea97fdb Selection: a selected group's masked art counts only where a click hits
A selected group was grabbed by any of its contents under the pointer
where an outer mask hides them, including a hidden or locked child and
art the group's own clipping mask leaves out: none of these is drawn or
hit by a click.

A container's art now counts as a click would hit it: its contents as
the hit test tries them, through its own mask and without its hidden,
locked or template contents. The check for locked objects shares it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EjfPR4a1jPek7r3nyFFNYv
2026-10-29 18:46:47 +07:00
nevaremandClaude Opus 5.5 4cd9afe67c Selection: a press where a mask hides a selected object's art grabs it
An image in a clipping mask, picked in the Layers panel, shows its
whole frame, but a press on the part the mask hides hit nothing: the
Selection, Direct Selection and Group Selection tools started a marquee
there, which dropped the selection, so the image could be dragged only
by the part that shows.

Such a press now grabs the selection by that art, as a press on its
visible part does: a drag moves the selected object (the image, not its
clip group) and a click keeps it selected. With nothing of it selected
there, or with Shift, a press there starts a marquee as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EjfPR4a1jPek7r3nyFFNYv
2026-10-29 18:46:47 +07:00
nevaremandClaude Opus 5.5 a7db76bdfb Properties: selected ruler guides show their position in a Transform section
With ruler guides selected on their own (a click on one), the
Properties panel showed the Document sections, so a guide's position
could be typed only in the Transform panel.

The panel now has a Guides heading and a Transform section with the
Transform panel's guide fields (#1597): X moves the vertical guides and
Y the horizontal ones, together. With art selected too, it shows the
art's sections as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EjfPR4a1jPek7r3nyFFNYv
2026-10-29 18:46:47 +07:00
dotCooCoo d93657c631 Object: Hide Unselected Artwork keeps a selected layer's art and groups that hold locked guides
With a layer selected, Object > Hide > Unselected Artwork hid every object
on that layer, since the selected layer was dropped from the objects to
keep. The objects of a selected layer now count as selected.

While Lock Guides is on, a group that held a guide path was hidden whole,
which hid the guide with it. Guide paths now count as selected, so the
command keeps such a group visible and hides its other members one by one.
2026-10-29 18:46:47 +07:00
NetterSilverandClaude Opus 5.5 9c01829cbb Keep Smooth's closed-path start corner within the tolerance
Found while testing crates/pathops/src/edit.rs:342-364 (smooth_level calls smooth_seam after the fit)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 18:46:46 +07:00
bflatastic e962197260 Tests: put the Rearrange Artboards toggle test's comment back on it 2026-10-29 18:46:46 +07:00
Max MittelstädtandGPT-6-Luna 2b75c63431 Retain spacing when rearranging artboards
Fixes #4558

Co-Authored-By: GPT-6-Luna <noreply@openai.com>
2026-10-29 18:45:57 +07:00
dotCooCoo cfdeda35c4 Tests: the non-sRGB Drop Shadow test fails when its child test does not run
svg_darkness_uses_rendered_shadow_in_non_srgb_working_space runs itself
again in a child process with --exact and checked only the exit status.
libtest exits 0 when --exact matches no test, so a rename of the test
would have left the parent passing without running the non-sRGB check.
The parent now also requires "1 passed" in the child's output, as
layout_cache_follows_fonts_added_later in crates/tools does.
2026-10-29 18:45:54 +07:00
Max MittelstädtandGPT-6-Luna 2b37302fe5 Preserve comment markers inside CSS strings
Found while testing crates/svg/src/import/css.rs:346

Co-Authored-By: GPT-6-Luna <noreply@openai.com>
2026-10-29 18:45:51 +07:00
bflatastic e0c92a4d90 Keep a letter and its marks in one font when falling back (#4491) 2026-10-29 16:27:18 +07:00
bflatastic ef086103cc Tests: the pan-fill canvas test runs again when another test moves the placed-document clock 2026-10-29 16:27:12 +07:00
NetterSilverandClaude Haiku 5.5 c90cea9e19 Round stepper button steps like arrow-key steps
Fixes #4490

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 15:16:20 +07:00
dotCooCoo a4ca347692 Shaper: dragging a shape out of a Shaper Group's bounds takes it out of the group
Squashed from #4415 onto main.
2026-10-29 15:16:17 +07:00
dotCooCoo b860a7b9af Direct Selection: a drag from a fill moves only the selected anchors of partly selected paths
Squashed from #4443 onto main.
2026-10-29 15:16:11 +07:00
dotCooCoo 6d66eaf3dc Object: Cmd/Ctrl+Alt+Shift+3 hides every unselected object
Squashed from #4418 onto main.
2026-10-29 15:16:05 +07:00
nevaremandClaude Opus 5.5 69d684ed36 Shapes: the size dialogs' link applies every change of its state
The link applied its state only when its button was clicked, so turning
it off and on again through `ui.dialog.set` kept the old proportions,
and the next dialog didn't open with the state set that way. A link
turned on at a side of zero also took new proportions on the next frame
once that side was a size again.

The dialog now keeps the link state it last applied and applies any
change from it, however it was made: turned on, the link keeps the
sizes the dialog shows then; turned off, it forgets them; either way
the next dialog opens with the new state. Nothing else recalculates the
proportions, so a link turned on at a side of zero stays unconstrained
until it is turned off and on again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EjfPR4a1jPek7r3nyFFNYv
2026-10-29 14:36:42 +07:00
nevaremandClaude Opus 5.5 7d18b4481c Shapes: the Rectangle and Ellipse dialogs link Width and Height
A click with the Rectangle, Rounded Rectangle or Ellipse tool opens a
dialog with Width and Height. It now has a Constrain Width and Height
Proportions link beside the two rows, drawn like the Transform panel's:
while it is on, typing one side sets the other at the proportions the
shape had when the link was turned on. The next of these dialogs opens
with the link as the last one left it.

Width now comes before Height in these dialogs: the fields were listed
by name, so Height came first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EjfPR4a1jPek7r3nyFFNYv
2026-10-29 14:36:42 +07:00
dotCooCoo dfa9f883c0 Fix Drop Shadow Darkness in source color space 2026-10-29 14:36:41 +07:00
dotCooCoo e31e04e545 Part of #3137: Color Guide takes its base from colors chosen in Color, Swatches and the Color Picker
The Color Guide kept the base color it took when the panel first showed,
and only the base chip changed it. In a new document the current fill is
white, so every harmony and variation was white or gray until the base
chip was clicked with another current color.

A color chosen in the Color panel (spectrum, sliders, fields, chips and
Recent Colors), in the Swatches panel or a swatch library panel, or with
OK in the Color Picker for a fill or stroke now becomes the guide's base
color, and the guide shows its harmonies. A color applied from the guide
itself leaves the base as it is, and so does selecting artwork. The base
chip (Set base color to the current color) still makes the current color
the base. A new base clears the cells selected for Save Colors as
Swatches.

The base moves from egui's temporary memory to UiState::color_guide_base,
which is not saved, so the Color panel, the Swatches panels and the Color
Picker can set it.
2026-10-29 14:36:35 +07:00
NetterSilverandClaude Haiku 5.5 520f390c9c Map hyphenation exception break points to the original word's chars
Found while testing crates/text/src/hyphen.rs:74-90 (parse_exception)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 14:36:33 +07:00
dotCooCoo c1c07efc95 Blob Brush: keep the first Shift axis without moving earlier points
Squashed from #4525 onto main.
2026-10-29 14:36:32 +07:00
dotCooCoo bb869dd9df Swatches: global swatch edits also update graph designs and the art of brushes
Squashed from #4489 onto main.
2026-10-29 14:36:04 +07:00
dotCooCoo b9f9099d91 Flare: preview options and restore defaults with Alt or Option
Squashed from #4446 onto main.
2026-10-29 14:35:57 +07:00
bflatastic efc1501ac9 Path: Object › Path › Smooth smooths the selected paths with a slider and Auto-Smooth (#4115) 2026-10-29 13:51:51 +07:00
Don Omondi 1196d9a335 fix(eps): import bool_or after main's explicit command imports
Main replaced the glob import of the command helpers in fileio/eps.rs
with an explicit list; the EPS Save path's selectedOnly check uses
bool_or, so it is added to that list.
2026-10-29 13:00:49 +07:00
Don Omondi c6070f633e Support original-format EPS Save and Save As 2026-10-29 13:00:47 +07:00
dotCooCoo 95ce4d5cb4 Canvas: a pan or zoom-out fills the uncovered area with low-resolution art
Squashed from #4256 onto main.
2026-10-29 13:00:47 +07:00
bflatastic 828d784c8c Shape Builder: import replace_leaves in buildcmds/shape_builder.rs (#4299 made the imports explicit) 2026-10-29 13:00:46 +07:00
dotCooCoo f30819b7eb Shape Builder: a merge replaces only the paths it uses, where they are
Squashed from #4094 onto main.
2026-10-29 13:00:46 +07:00
Muhammad Saad 18e916ddf5 Pattern Brush: decide straightness from the track, not from samples (#3926 review)
Messhias is right that a sampled check cannot promise a run is straight, and my
own write-up overstated what the grid did: I said the fit was "used only where
it reproduces the real map", which five sample positions cannot establish. A
bend narrower than the sample spacing passes between the samples, and the grid
then approves an affine that is simply wrong.

It is not a theoretical gap. A zigzag of amplitude 4 pt and half-period 20 pt,
turning 11.3 degrees (under CORNER_DEG, so the vertices are not corners and
their tangents blend flat), lands its y = 0 vertices exactly on the five sample
positions. All 25 grid points then agree with the affine to the last digit while
the art between them is thrown 4.000 pt off, against the 0.0613 pt tolerance the
grid believed it was enforcing: 65 times over, measured, not estimated.

`Track::is_straight(s0, s1)` reads the answer off the track instead. A bend in
the span is two segment directions that differ, so no bend can hide in it
whatever its size. It also checks the vertex tangents, because `frame` blends
them across a segment unless the vertex is a corner, so a neighbour outside the
span can bend the tangent inside it: directions alone would call `d, d, e`
straight over the first two segments and be wrong. It fails closed on a closed
track's seam, a non-finite bound and a track with no segments.

`AlongMap::is_linear` covers the other half: Between Guides bends the arc-length
map itself, which no amount of looking at the track would reveal.

The grid stays, demoted to a second opinion that can reject and never admit, and
`affine_fit`'s doc comment now says that rather than claiming to be the gate.

Tests. `a_bend_shorter_than_the_sample_grid_is_still_a_bend` builds the zigzag
above, asserts the sampled check accepts it, measures how far out the approved
affine is, and then asserts the track-based gate refuses it; end to end through
`bend` it asserts the aliased bend is subdivided while a genuinely straight run
of the same span keeps the tile's four anchors. Restoring the sampled gate reds
it with `the aliased bend is subdivided, not affine-mapped: 4 anchors`, so the
old code really did lay a flat tile along a waving track.

`straightness_is_read_off_the_track_not_sampled` covers the predicate directly:
collinear segments and their extrapolation, a 0.2 pt detour, a span around it
and a span before it, the blended-tangent case above, the closed seam and a NaN
bound. Writing it caught a bug in my own guard: `f64::min` discards a NaN rather
than propagating it, so `is_straight(NAN, 10.0)` was reporting straight until
the finiteness check moved ahead of the min/max.
2026-10-29 13:00:45 +07:00
Muhammad Saad 955660bee0 Pattern Brush: make the straight-run route match warp_node exactly (#3926 review)
FredApps noted a real divergence: the affine route kept whatever the tile held
(it only required bounds) while warp_node drops every node kind but paths,
groups, layers and compound paths, and it did not clear `live` on paths. For a
tile of paths and groups the result was the same, so nothing was visibly wrong,
but "this changes the curves and nothing else" was a wider claim than the code
made: a symbol instance arrived untouched on a straight run and vanished on a
bend.

affine_node now mirrors warp_node's walk through `prepared` and applies one
transform at the end: the same kinds are kept and the rest dropped, `live` is
cleared on every path, an empty path or a group left with no children is
dropped, and strokes are scaled by `width_scale` rather than by the transform's
own factor. The two routes now differ in the tile's curves and in nothing else,
which is what the change was always meant to be.

Test: a_straight_run_keeps_the_same_node_kinds_as_a_bend puts a symbol instance
beside a path in one tile and asserts both routes return the same kinds. On the
old code it reds with `left: ["group", "path", "symbol"] right: ["group",
"path"]`.
2026-10-29 13:00:45 +07:00
Muhammad Saad d1b9bc2b3d Pattern Brush: keep the tile's curves on a straight run (#3926)
Bending flattens every path it maps: warp_path runs kurbo::flatten and
emits SubPath::polyline, so a circle tile is drawn as a polygon. That
shows in the live stroke, not only after Expand Brush Strokes, and it
happens even on a perfectly straight path where the tile needs nothing
more than an affine.

Bending is only non-linear where the track bends. On a straight run the
arc-length map and the normal offset are both linear, so affine_fit
derives the affine from three corners of the art and then checks it
against a five by five grid, including those corners. Within the
tolerance the flattening would have used, the art is transformed
instead, which keeps its Beziers. Outside it, nothing changes: the old
path runs and the result is what it was.

Strokes are scaled by the same factor the bent path uses rather than by
the affine's own, so curves are the only thing this changes.

Corner tiles already took this route, which is why they kept their
curves while the tiles between them did not.

Two existing tests moved with it, both because they read anchor counts
that only held while straight runs were flattened:

  pattern_places_corner_tiles told corner tiles from side tiles by
  "four anchors", true only because the side rectangles arrived as
  polylines. It now tells them apart by a kept handle, which is what
  actually distinguishes an ellipse from a rectangle, and additionally
  asserts the side tiles keep their own four anchors.

  pattern_output_is_capped_across_subpaths_not_only_per_run counted on
  sixteen tracks passing the #3594 anchor budget. A tile is about four
  times cheaper in anchors now, so the budget engages later; the test
  derives how many tracks it needs from what one costs.
2026-10-29 13:00:44 +07:00
krosk93 7a79935c10 i18n: add Catalan (ca) localization
Squashed from #4453 onto main.
2026-10-29 13:00:44 +07:00