Commit Graph
5560 Commits
Author SHA1 Message Date
dotCooCoo ef65f0fed4 Preferences: the settings filter builds without a clippy warning on macOS
Squashed from #6235 onto main.
2026-11-09 04:19:35 +07:00
dotCooCoo 1e21154e4d Symbols: symbol.instance.setAppearance refuses a fill or stroke an object does not draw
Squashed from #6232 onto main.
2026-11-09 04:19:25 +07:00
onion3130 eaab4fd516 Trim to hermetic font test only (Source Sans 3, not Inter) 2026-11-09 04:02:29 +07:00
dotCooCoo 7b8d5ba8f5 Type: Tab and Shift+Tab move a list item down and up a level
Squashed from #5037 onto main.
2026-11-09 03:39:05 +07:00
dotCooCoo 0de0cb7cb9 Image Trace: Ignore Color can leave out only the background
Squashed from #3952 onto main.
2026-11-09 03:38:46 +07:00
dotCooCoo 4724475e1a Effects: hidden layer children and edited freeform gradients follow the Transform and Free Distort effects
A path that keeps its shape because the effect would pass the geometry
budget holds a group's own placed gradients in place. A hidden path in a
layer, which the layer's bounds leave out, no longer holds them, so the
layer's gradient moves with its visible art. The art's own gradients
follow the same rule.

A freeform gradient whose points were edited keeps its vector fitted to
the bounds, so it did not count as placed and its points stayed behind
under a Transform effect and Free Distort unless another placed gradient
was in the same appearance. Its points now move like a placed gradient.
2026-11-09 03:14:41 +07:00
dotCooCoo 11f1d228de Type: Align to Path moves each character by its own font's metrics
Align to Path moved the whole line by the largest ascent and descent
among its characters. Each character now moves by its own font, size
and vertical scale. Ascender runs the top of the font's em box along
the path: the ideographic em box that the font's BASE table gives, or
in a Chinese, Japanese or Korean font the one that its typographic or
hhea box, a one-em box-drawing cross or its ideographs give; else an
em box centered on the cap height. Descender runs the bottom of the font's
bounding box along the path, and Center runs a line at half the height
of the top of the font's H along it. In vertical type, Ascender and
Descender move the characters half an em, and Center leaves them on
the path.

Existing documents with Align to Path set to Ascender, Descender or
Center lay their type out at a new distance from the path.
2026-11-09 03:14:41 +07:00
dotCooCoo 85a933ad4f Keyboard Shortcuts: each Object › Plug-ins filter has a row of its own
Squashed from #6246 onto main.
2026-11-09 03:14:41 +07:00
dotCooCoo 25ecee0c40 Color: Generic CMYK tints gain as CGATS TR 001 gives for SWOP printing
Generic CMYK's tone curves now pass through the CGATS TR 001 tone value
increase of each ink at 40% and 50% (C 20.0, M 17.6, Y 16.8, K 21.6 at
50%), and its 16 Neugebauer primaries mix linearly, so a single ink's
tints gain as TR 001 gives. The 300% ink limit stays. The primaries, the
black-generation curve and the black point are VectorCraft's own values.

- The black point is 90% black under neutral CMY that fills the ink
  limit: sRGB black separates to C75 M68 Y67 K90 instead of C67 M67 Y67
  K100.
- Black follows a curve over the gray component of the black-free
  solve, raised by how far that solve stays lighter than the target.
  Grays from sRGB 177 up carry no black, and sRGB 128 gray separates to
  C50.5 M42.2 Y42.0 K7.5 instead of C38.8 M26.3 Y29.9 K24.4.
- Display of CMYK fills, swatches and imported CMYK values and the
  separation of RGB and Lab colors use the same model.

Paper white still separates to C0 M0 Y0 K0, but other separations
change, and so does the display of CMYK colors with ink. Tests that held
numbers of the old model are updated.
2026-11-09 03:14:41 +07:00
bflatastic c2f2c8ae9b Find Font: a composite font's row is listed by its name alone 2026-11-09 03:14:41 +07:00
dotCooCoo c94269ff72 Type: Composite Fonts… sets each class of character in a font of its own
Squashed from #6244 onto main.
2026-11-09 03:08:23 +07:00
dotCooCoo b9d999fe26 Asset Export: the 3D mesh triangle limit bounds memory across objects
The 2,000,000-triangle limit on a 3D object's meshes is now checked as
each mesh is built, so an object past it is refused before the rest of
its meshes are built:

- Revolve counts each fill's and stroke's mesh as it builds it, instead
  of building every one first.
- Inflate still solves every shape's membrane at once (the eye depends
  on the highest puff), but builds each solid only when its mesh is
  reached (inflate_meshes), so a refused object leaves the later
  solids unbuilt.
- All the objects of one 3D asset share one budget
  (three_d_meshes_of), so assets.export3D refuses an asset whose
  objects pass the limit together.
2026-11-09 03:08:23 +07:00
bflatastic faec1f4290 Links: the moved-document tests compare the found path as a path, so Windows separators match 2026-11-09 03:08:23 +07:00
dotCooCoo faa7b8ef6a Links: a missing linked file keeps its relative folder when a moved document is saved
Squashed from #6231 onto main.
2026-11-09 03:08:23 +07:00
bflatastic d4f2cf5394 Plug-ins: the fuzz tests install each case's module in place of the one before 2026-11-09 03:08:22 +07:00
dotCooCoo 218c711181 Plug-ins: a plug-in keeps settings and a cache of its own
Squashed from #6228 onto main.
2026-11-09 02:54:33 +07:00
dotCooCoo 1f38a16329 Commands: an ids list with an entry that is not an id is refused instead of dropping the entry
Squashed from #6179 onto main.
2026-11-09 02:54:32 +07:00
bflatastic 02d877d043 .ai import: the unread-PDF-part test expects the cut-short note for its large image 2026-11-09 02:54:32 +07:00
dotCooCoo b5adb679e4 .ai import: images past 2^25 pixels in the editing data keep the layers
Squashed from #5143 onto main.
2026-11-09 02:43:13 +07:00
Don Omondi 21e6464e6f EPS import: scaling a named descendant counts its dictionary's size; test Type 3 descendants and that unreadable Type 42 data is read once 2026-11-09 02:24:54 +07:00
Don Omondi 70353f003e EPS import: reading sfnts data is charged before the copy, whatever its result
Joining a Type 42 font's strings is charged per byte (as other bulk
copies are) before it runs, so data that turns out unreadable, read
again after leaving the cache, no longer copies for free.
2026-11-09 02:24:54 +07:00
Don Omondi e61f0940d0 EPS import: composite fonts' Type 42 review fixes
widthshow matches a composite character by font number times 256 plus
its code; measuring Type 42 glyphs counts against the operation budget;
the Type 42 data cache keeps the least recently used order, remembers
fonts whose data can't be read and counts each font's data against the
memory budget once; composite font arrays are borrowed, not copied.
2026-11-09 02:24:54 +07:00
Don Omondi 3bb43b2fe6 EPS import: composite fonts draw their Type 42 descendants' glyphs
A composite (Type 0) font with the 8/8 mapping picks a descendant font
and a code in it with each pair of bytes (PLRM 3rd ed., 5.10). The reader
set its strings as type in a font of the composite font's name, the
bytes read as Latin-1 and at a thousand times the size, so EPS from
pdftops of a PDF with embedded TrueType fonts showed nothing, or one
black shape, where its type was.

Characters now go to their descendants: a Type 42 font's glyphs are drawn
from the TrueType outlines its sfnts strings carry (Adobe Technical Note
5012), by the glyph index CharStrings gives their names, spaced by their
own widths or the show operator's numbers; charpath and stringwidth read
them too. A Type 3 descendant runs its procedures, and a descendant found
by name sets its character as type at the composite font's size.
2026-11-09 02:24:54 +07:00
NetterSilverandClaude Haiku 5.5 b142814bbd Reject a plus sign in type.insert unicode hex digits
Found while testing crates/engine/src/cmd/typemenu.rs:614 (unicode_char)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 02:24:50 +07:00
Drew Dibble bc87fd1b7e 3D effects: merged contours keep their parts, and edge cases are consistent
The boolean's answer went through orient(), which judges each ring's
side from its first point. The boolean often starts a contour where it
touches another, so a triangle whose tip rests on a square's edge was
dropped, and an even-odd hole touching its outline was filled. The
boolean already runs holes against their outlines, so its rings are now
only turned as a whole.

Other fixes:
- A bowtie drawn as one contour has no area of its own. It was dropped
  before the meet check and built nothing, although it fills both lobes.
  Sampling now keeps rings with no area, and orient() drops them after
  the check. Flat rings, every point on one line, fill nothing and are
  still dropped, so a stray flat contour doesn't send separate art
  through the boolean.
- An open subpath was closed along its end handles when the profile went
  through the boolean, but straight otherwise, as the renderer fills it.
  It is now closed straight before the boolean too.
- An empty union was taken as a refusal. Contours that cancel (a square
  drawn twice under even-odd) built a square. They now build nothing.
- Contours meet within twice the boolean's own sweep tolerance,
  computed by its rule from every anchor and handle of the path it is
  given. pathops snaps anchors onto nearby heights before the sweep
  joins edges within that tolerance, so contours up to about 1.4 times
  it apart can come out as one; within twice it, every contour it
  might join goes through it, even far from the origin, and the few it
  leaves apart cost a boolean that builds the same shape.
- A one-anchor subpath draws nothing, so it is skipped, as the renderer
  and the boolean skip it.
- A profile the boolean refuses reuses the rings already sampled.

Tests:
- touching parts and holes;
- an open subpath with handles, alone and beside crossing bars;
- a flat contour beside separate squares;
- contours joined far from the origin, including by handles alone,
  and contours a few millionths apart near it kept apart;
- triangles snapped together and joined at 1.4 times the tolerance,
  the same triangles left apart and kept whole, and the factor pinned
  between 1.9 and 2.2 times, with the worst case just past twice the
  tolerance left apart by pathops;
- a 24-bar grid: too complex for Extrude and Inflate, whole under Rotate;
- the merged rings turned as a whole;
- a one-anchor loop;
- the bowtie and the cancelling squares under all three effects;
- a 3,001-point star that the boolean refuses (TooComplex), built as
  drawn.

Docs: the pathops README's Limits section now says only the try_
functions refuse, lists OpenPath and the caught panic, and describes
unite_all's answers, and which calls take a precision. three-d.md and effects-parity.md (the 3D row, its
history and status line) describe the merge and its limits.
2026-11-09 02:24:47 +07:00
Drew Dibble 62a01689f0 3D effects: resolve a profile only when its contours meet
The boolean that merges crossing contours cost about as much as the rest
of a plain extrusion, and it ran on every profile. Most art has no
contours that meet, so the profile is now sampled first (its point limit
checked there, before any boolean) and resolved only when two edges
cross, touch, run along each other or fold back, in one contour or
across two. Edges are swept by their left ends, within a pair budget past
which the boolean decides. Touching counts: squares sharing an edge
inflate as one puff, not two with a seam.

On a release build, Extrude of 120 separate contours takes 0.31 ms
instead of 4.18 ms; profiles that do cross cost what they did.

Tests: the meeting cases, squares sharing an edge, the fallback for a
profile the boolean refuses, Inflate's extent and area for the crossing
k, and the coincident circles' area. The hairline test now holds a
crossing pair so that its profile still goes through the boolean.

Docs: the merge is described once for all three effects, linking to a new
Limits section in the pathops README instead of repeating its limits,
and the point limit notes that a merged outline can have more points.
2026-11-09 02:24:47 +07:00
Drew Dibble e1108210f8 3D effects: keep narrow contours when resolving overlaps
try_normalize refits its result at Pathfinder's default precision and
drops every contour thinner than 0.01 pt on average as a sliver. A
hairline beside a larger shape is art, not debris, and resolved() only
fell back to the input when nothing at all was left, so the hairline lost
its cap, walls and surface.

Resolve with try_unite_all_with at a precision far below any art, which
still drops the debris a boolean leaves where curves coincide: a circle
drawn twice, exactly or 1e-7 pt off, stays one outline.
2026-11-09 02:24:47 +07:00
Drew Dibble 9bd8b69613 3D effects: resolve overlapping contours before building a solid
Extrude & Bevel, Inflate and Rotate broke on shapes whose contours cross.
Type often draws a glyph that way: Roboto Slab's "k" has its arm and leg
as one contour that crosses both the stem and the leg's foot serif.

rings() decided each contour's role (outline, hole, or no edge at all
under nonzero) from the contour's first point only. A contour that starts
inside another was dropped or flipped into a hole, cutting part of the
glyph away. A contour that starts outside was kept whole, so its walls and
bevels ran through the overlap and the overlapping parts drew as separate
boxes.

The profile now goes through vectorcraft_pathops::try_normalize first, so
rings() only ever sees simple contours that do not cross. A path the
boolean refuses is used as it was.
2026-11-09 02:24:47 +07:00
Sven Lafebre b34de477a8 #6135: test opening a .ai from readable editing data
The direct-open test only had the editing data keys, so it opened from
the PDF part. It now has two artboards and a layer that exist only in
readable editing data. The keys-only case stays as its own test.
2026-11-09 02:03:10 +07:00
Sven Lafebre 1e7e1d33e9 Fix #6135: opening a .ai with editing data skips the Import PDF dialog
The editing data of a .ai or .ait holds every artboard, so a multi-page
file has no pages to pick. Opening one now goes straight to it, as a
file carrying its VectorCraft document already does. The dialog stays
for PDFs, a .ai that is only a PDF, a password, and Place.
2026-11-09 02:03:10 +07:00
el-j d499fb25da Fix #6205: support Registration and named swatches in AI art styles
Resolve missing Registration (Xs, XS, Xz, XZ) and named color (Xx, XX)
operators in AI ArtStyle filter definitions so objects styled with
Registration swatches correctly resolve their paint instead of falling
back to the previously set graphics state color.

Also exclude Registration paints from color-model inference when read via
Xx/XX, and correctly extract tint (penultimate operand) for name-free Xz.
2026-11-09 02:03:09 +07:00
NetterSilverandClaude Haiku 5.5 b1fd06090c Don't treat a pattern named "Name +N" as a numbered copy on import
Found while testing crates/engine/src/cmd/patternlib.rs:99 (existing_pattern)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 02:03:09 +07:00
bflatastic ca7605fe20 Canvas: record the opening fit without a branch in the canvas frame 2026-11-09 02:03:09 +07:00
bflatastic 2d945f92a1 Canvas: the opening fit's conditions in a helper, keeping the canvas frame under the complexity limit 2026-11-09 02:03:09 +07:00
Louise Fox 1b85e7fee2 Canvas: a document opens at the zoom of the settled window
A document without a saved view was fitted once, to the canvas of the first frame that shows it, and the window's layout can still move the canvas's edges over the next frames. Two starts of the app then opened one file at zooms a hair apart (4.9812 and 4.9883 in a CI probe of v0.21.0, the canvas and the view's centre alike).

The opening fit now follows the canvas until the view is moved or the user presses a button or a key, types, scrolls or zooms (CanvasCache::opening_fit). After that a canvas that changes keeps the view, as before.
2026-11-09 02:03:08 +07:00
Louise Fox f794f4665c Canvas: a test of the opening fit when the first frame's canvas is narrower than the settled one
A document without a saved view is fitted to the canvas of the first frame that shows it. The test lays that frame's canvas out 2 points narrower than the next, and expects the zoom the settled canvas gives. It fails until the opening fit follows the canvas.
2026-11-09 02:03:08 +07:00
SBaksa d329dd05e1 Export for Screens: a leading dot in an artboard name is a dash
A name like .draft would otherwise write a hidden file on Unix-like systems.
2026-11-09 02:03:08 +07:00
SBaksa 0b3ea7c1d2 Export for Screens: an artboard name keeps its dots (#6143)
The artboard and asset file stems came from unique_file_names, which turned
every character except letters, digits, - and _ into a dash, so the dot of
example.artboard became a dash. Export for Screens now uses a variant that keeps
dots (a name of only dots takes the Artboard-N fallback); safe_file_name still
drops trailing periods and guards device names.
Test: an_artboard_name_keeps_its_dots.
2026-11-09 02:03:08 +07:00
Francisco Herrera a3ed60650e Layers: row keys follow the click and release after the last row (#5870) 2026-11-09 02:03:08 +07:00
Francisco Herrera 5b98ed3b7d Layers: Delete clears the selection after the last row is deleted (#5870) 2026-11-09 02:03:07 +07:00
Alex d8d3f43642 Text styles: double-click opens Character Style Options in a scrolling window
Two problems in the Character Styles panel and its options dialog.

Opening the options took the panel menu's item, which is easy to miss; a double-click on a style's
row did nothing beyond applying the style a second time. A double-click on a user character style's
row now opens its Character Style Options (`ui.charStyleOptions`), as the menu item does. A single
click still applies the style, the second click of the double-click does not apply it again (the
Normal style's included), and with Shift, Cmd/Ctrl or Alt held the double-click only follows the
click rules. The built-in Normal style opens no dialog. Double-clicking a paragraph style's row similarly opens
its options, which are the rename dialog the Paragraph Styles panel menu opens (`ui.paramDialog`
running `paraStyle.rename`): paragraph styles have no full options window yet.

The dialog then grew with its content: the General section lists every attribute of the style, so
the window covered the screen and its buttons could sit below it. The content now scrolls both ways
inside a window at most 420 points tall (less on a short screen) and never wider than the points
left beside the section list, with one scroll offset per section. The dialog keeps a fixed width of
720 points, enough for the widest section's controls, in every section. The content's height is the
space the footer leaves, measured from the footer drawn last frame, so a large interface scale's
taller footer is reserved too and OK and Cancel stay visible. The section list stays outside the
scroll area, so scrolling a long section never moves the navigation; the section is read after the
list handles a click, so a newly picked section gets its own scroll offset in the same frame.

Tests:
`panels::text_styles::tests::double_clicking_a_character_style_row_opens_its_options` selects text,
double-clicks a character style's row and checks the dialog opens with the style's name, one apply
ran from the first click and the second click applied nothing; a paragraph style's row opens none.
`double_clicking_the_normal_style_opens_no_options` checks the Normal style opens none and is
applied once.
`dialogs::char_style_options::tests::the_sections_scroll_in_a_window_shorter_than_the_general_list`
draws the dialog on a 900 by 560 point screen and checks OK and Cancel stay on it, the dialog keeps
its 720-point width in two sections, and the General list is taller than its scroll area.
`every_section_and_the_buttons_fit_the_smallest_window` checks the app's minimum 800 by 500 point
window in all five sections: every section label, OK and Cancel are on the screen.
`a_scrolled_section_keeps_its_offset_after_switching_away_and_back` scrolls General, clicks Basic
Character Formats and then General again, and checks General''s content returns to the same place:
the section is read after the list handles the click, so no other section stores its offset.
`every_section_and_the_buttons_fit_the_smallest_window_at_twice_the_scale` checks the same in the
400 by 250 points the minimum window holds at 2x UI scale, before and after a wheel scrolls the
content.
Docs: type-parity (a panel row and the revision history), ui-parity (a panel row and the revision
history), target-app-parity (two evidence bullets and the revision history) and roadmap (the
revision history), with the status lines updated.
2026-11-09 02:03:07 +07:00
bflatastic 8199f30c74 Export for Screens: the asset-name test sets main's 3D format field 2026-11-09 02:03:07 +07:00
NetterSilverandClaude Haiku 5.5 c085185296 Ignore non-digit suffixes in next_asset_name
Found while testing crates/doc/src/assets.rs:43 (Document::next_asset_name)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 02:03:06 +07:00
Ahmed Gomaa 23c5f6ff7c Preferences: colour wells and sliders take their row's label
`label_inputs_by` skips controls that already have a name, so colour wells stayed "Color" and sliders kept their internal text. Name them from the visible row label, on both halves of a slider across focus changes, and keep action button names.
2026-11-09 02:03:06 +07:00
Ahmed Gomaa a89dcc1a5d Preferences: inputs expose their labels to accessibility tools
The Preferences labeled() helper painted the visible label and then drew
the input beside it without associating them. Call label_inputs_by so
screen readers and the test harness can find each field by its label.
2026-11-09 02:03:06 +07:00
Ahmed Gomaa 93a039d524 UI tests: gradient and rotation controls are found in release builds
Gradient and rotate-view harnesses looked up widgets by egui Id debug
source strings (for example ".with(…)" and "rot-combo"). Those names
exist only with debug assertions, so the same tests panic in --release.
Resolve widgets from stable scope salts / AccessKit labels instead, which
work in both profiles.
2026-11-09 02:03:06 +07:00
Sven Lafebre afe8294c7d Test: anchors picked with a Cmd-lent Direct Selection over other tools are what Copy takes (#6123)
With the Rectangle and Ellipse tools, Cmd lends the selection tool chosen
last, and Copy already took the anchors Direct Selection picked. The
Curvature tool, which Cmd lends Direct Selection, copied the whole path
before this PR, as the Pen did.
2026-11-09 02:03:05 +07:00
Sven Lafebre 748ac5cf6e Pen: keep a lent pick across tool resets; a gesture that picks nothing keeps the Pen's anchor (#6123)
The remembered selection is now tied to the document it was made in, by its
uid, instead of being cleared on every tool reset: another view of the same
document, or closing an inactive tab, kept the anchors picked but made Copy
take the whole path.

A lent gesture that leaves the selection as it found it (a Cmd+Shift marquee
over nothing) is no longer recorded as a pick, so the Pen's last anchor stays
its own and Copy takes the whole path.

Also records #6123 in target-app-parity.md.
2026-11-09 02:03:05 +07:00
Sven Lafebre 91a0ba4231 Pen: Copy and Cut take the anchors a Cmd-lent Direct Selection picked (#6123)
With the Pen or Curvature tool, Copy and Cut ignore the anchor selection,
so that the last anchor the Pen leaves selected doesn't make Copy take just
that anchor (#2122). Anchors picked with the Direct Selection that Cmd/Ctrl
lends those tools were ignored too, and the whole path was copied.

The session now remembers the selection the lent tool leaves when it gives
the tool back. While that is still the selection, Copy and Cut take the
picked anchors and segments, as with Direct Selection chosen.
2026-11-09 02:03:05 +07:00
NetterSilverandClaude Haiku 5.5 194364304b Strip CSS comments from style attributes and rule declarations
Found while testing crates/svg/src/import/css.rs:20 (parse_decls / split_decls)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 02:03:05 +07:00