Commit Graph
2154 Commits
Author SHA1 Message Date
dotCooCoo 18015f3631 Type: a selected story stays selected when its text leaves the edited frame
When a frame threaded in before the edited frame takes all of the story's
text, the edited frame is left with no text while the story stays
selected. The other frames' text is now highlighted, Delete and Backspace
replace the story, an Alt+arrow step changes the tracking of every frame,
and Copy and Cut take the story. A story whose frames hold no text has
nothing to copy, and Delete in it records no undo step.
2026-11-07 05:15:12 +07:00
dotCooCoo 22bb2ac8a6 Type: a selected story's highlight and typing follow its current thread
With a whole story selected, the highlight and typing read the frames saved
at Select All, while a pick, a fill and the other story-wide edits read the
document's thread. A frame added by text.thread.add took a pick without being
highlighted, and typing left its text in place.

Both now read the thread through Document::story_of. A frame threaded in
during the selection is highlighted and takes the edit, and a frame that
leaves the thread is not highlighted and keeps its text.
2026-11-07 05:15:11 +07:00
dotCooCoo c969b7857d Type: an Eyedropper pick on a selected story styles every frame
appearance.copyFrom and a fill or stroke change on a story selected by
Select All no longer skip locked frames, hidden frames and frames on a
locked artboard. They apply to every frame of the story, as the Character
panel's story edits do. appearance.copyFrom still leaves out a frame that
is its source.
2026-11-07 05:15:11 +07:00
dotCooCoo f534b0061a Type: switching to the Eyedropper keeps a whole-story selection
With a threaded story selected by Select All, switching to the Eyedropper kept
only the edited frame's selection, so a pick styled one frame. The selection now
carries the story's frames, a pick or Shift-click applies to the text of every
frame as one undo step, and switching back to a Type tool restores the story
selection. A selection inside one frame and unthreaded type behave as before.

A pick and a fill or stroke change on a selected story leave locked and hidden
frames and frames on a locked artboard as they are. appearance.copyFrom leaves
out a frame that is its source.

Typing over a selected story no longer removes the text of a frame whose thread
was removed after Select All.
2026-11-07 05:15:10 +07:00
Sven Lafebre 1b62f86b8c Fix #5714: doc comments on the width profile tests 2026-11-07 05:15:10 +07:00
Sven Lafebre 9da56717a0 Fix #5714: widths run smoothly between width points
The width between two width points was a straight line, so a variable-width
stroke turned a corner at every width point.

- Each side of a width profile now follows a monotone cubic (Fritsch-Carlson)
  through its points: no kink at a point, flat at a point wider or narrower
  than both neighbours, never past either point of a span. Points on one line
  stay on it and two-point tapers are unchanged; stacked points still step.
- widest() counts each span's wider left and wider right, since the two sides
  needn't peak together.
- The outliner samples straight runs between width points closely enough to
  follow the curve to the drawing tolerance (at most 64 samples per span and
  4,096 per piece); straight spans get none.
- Tests that expected straight-line widths now expect the curve. The stroke
  gradient coverage test uses Teardrop instead of Wave until #5964 is fixed.
2026-11-07 05:15:09 +07:00
dotCooCoo 0c570dee87 .ai import: paths with an Art brush open with the brush on their stroke
A path with an Art brush comes in as the path, with the brush on its
stroke at its weight, instead of as its drawn look, when the brush
stretches art of solid-filled paths along the path, unflipped, at 100% in
Tints. The art runs on the side of the path the file draws it.

The drawn look stays for other colorizations and widths; Proportional,
between-guides and flipped brushes; type; a path with settings of its own,
a variable width, effects, a swatch stroke or an entry that isn't read;
and art with strokes, gradients, patterns, masks or effects, or no length.

A brushed stroke, Calligraphic ones too, takes the brush the Brushes panel
keeps under its name, and an image with a brush keeps its drawn look.

In every document, Tints and Tints and Shades take art in black ink alone
by its ink amount, and a process or gray stroke color's tints and shades
stay in that color's model.

Place and Expand bring the brushes the art uses, renaming on conflict.

Part of #637.
2026-11-07 05:15:09 +07:00
dotCooCoo ab3ac3d429 .ai import: envelopes map from their stroked box when the file saves its size
When Distort Appearance is on and the size a `.ai` saves for an
envelope's contents (`AIDeformArtWidthKey`, `AIDeformArtHeightKey`)
matches the contents' bounds with strokes, the reader now maps the
contents from that stroked box. It used to map them from the geometric
bounds, half a stroke inside that box, so an envelope of stroked art
could differ from its drawn art and keep the drawn art with a note.

Envelope Options hold the box's margins as `boundsMargin`, shares of the
geometric bounds at the top, bottom, left and right. Native files write
them only when they are not zero, and `object.envelope.info` reports
them then. Leaving Edit Contents measures them again when the strokes
reach differently than when Edit Contents began, and the node holds
what they measured then until it leaves. New envelopes have no
margins, so their results do not change. Contents with no width or no
height, such as one straight line, get no margins.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-07 05:15:08 +07:00
FractalEncrypt 1c271c776c Shape tools: review follow-ups from CodeRabbit
- The 45° move assertion compares dx and dy with a small tolerance instead of
  exact equality (the matrix's floats are rounded), keeping the non-zero check
  and the message.
- The engine test helpers say what they do, so the docstring coverage of the
  functions this pull request touches is complete.
2026-11-07 05:15:05 +07:00
FractalEncrypt 6d3c0fef99 Shape tools: guides take the centre point's pointer too, and more regressions
The Selection tool's cursor now checks the ruler guides before a selected
object's centre point, as a press does. Tests: a ruler guide under the centre
point keeps the pointer and drags; on a box too thin for its centre to clear the
handles the handles take both; a shape-tool move from the centre snaps and shows
the hollow arrow; and Alt pressed mid-drag copies the shape through the engine.
2026-11-07 05:15:04 +07:00
FractalEncrypt c36971afe9 Shape tools: the centre point's cursor comes after the box's handles
The Selection tool's cursor now checks the bounding-box handles before a
selected object's centre point, as a press does (on a thin box the centre lies
within a handle's reach). The shape tools' tests cover a Live Corners widget
that lands on another selected shape's centre (the widget rounds), Alt pressed
mid-drag (the preview copies and the copy cursor shows), and the engine tests
check the copy's place and both gestures' Undo and Redo. The cursor catalog says
which arrow a drag of the selection shows.
2026-11-07 05:15:03 +07:00
FractalEncrypt cd88b15912 Shape tools: review follow-ups (Shift, handle boundary, cursor order, feedback)
- The Selection tool takes a Shift-press on a selected object's centre point
  too: a Shift-drag moves the selection held to 45° steps, and a Shift-click
  there takes the object out of the selection, as a Shift-press on the object
  does.
- One rotate zone for the Selection tool and the shape tools
  (`select::rotate_zone`): a press on a bounding-box handle is not a turn, so
  the shape tools' zone matches the Selection tool's classification exactly.
- The shape tools' hover cursor follows the press order (corner and pie widgets,
  then the centre point, then the rotate zone), and a move of the selection
  shows the Selection tool's feedback: SelectionCopy with Alt, ArrowHollow while
  it snaps.
- Engine-level tests for the whole gesture (`crates/engine/src/tests/shapetools.rs`):
  repeated previews and one undo step, Alt copy, Escape then release, a turn
  about the centre, the corner widget still rounding, and an unfilled shape
  moved by the Selection tool.
- docs: evidence in target-app-parity.md's Shapes bullet and the M2 milestone,
  and the Move cursor's entry names the copy and snap cursors.
2026-11-07 05:15:03 +07:00
FractalEncrypt 1c345d8e6a Shape tools: drawing another shape from the selected one's centre moves it
A press on the centre point of the selected shape now moves the selection, as
asked (#5477), so the Constrain Angle test's repeated draws from the same point
moved the polygon drawn before them. It deselects first: nothing selected, the
press draws.
2026-11-07 05:15:03 +07:00
dotCooCoo 77d5f8bbea Type: tests compare opened documents with and without kept shaping plans
A new test binary opens a document of 13 type objects with the plans a
font face keeps and again while every shaped segment makes its own plan.
The document, its cached bounds and baselines included, and the first
frame's pixels must be the same. `test_fonts::keep_shape_plans` switches
the plans off; it exists only in tests and with the `test-fonts` feature.

The engine's open test now builds objects of one script and direction
that differ in kerning and language, and lays each one out in a font
database of its own, so a plan used for the wrong features fails its
comparison. It no longer sets Arabic, which could fall back to a system
font in one database and not the other.
2026-11-07 05:14:59 +07:00
dotCooCoo 25b42b2246 Type: layout keeps each font face's shaping plans
Every shaped segment built its own harfrust shaping plan, although a
plan depends only on the face, direction, script, language and features.
Opening a document lays out each type object once, so a document of many
short objects built thousands of identical plans.

A font face now keeps up to 32 plans, oldest dropped first, and shapes
with the one that matches. Feature ranges still come from the shape
call, so text with the same features over other ranges shares a plan.
The glyphs, bounds and baselines do not change.

Docs: docs/target-app-parity.md.
2026-11-07 05:14:59 +07:00
Don Omondi d16123efad Legacy .ai export: a flattened image's clip follows its edge smoothly (#5264)
The clipping path around a transparent image flattened for a version 7
.ai followed 2 pt cells, a visible staircase. It now follows the rendered
pixels (in blocks only past 4 million), grown by one pixel so the whole
anti-aliased edge is inside, through the middle of each pixel edge and
simplified with Douglas-Peucker, so slopes and curves are sloped lines.
2026-11-07 05:14:58 +07:00
dotCooCoo 5216632e6b .ai save: exports to a path and timestamped copies name the active artboard
A .ai file that File > Export As, its dialog or a control request writes
to a path now names the view's active artboard, as document.export and
document.serialize do. The UI export records the view's artboard and
takes its document from fileio::export_doc, which applies
DocState::active_artboard to the export's copy; document.export and
document.serialize use the same function.

The app now records the view's active artboard before every command,
including the commands the UI handles itself, so a .ai copy from Save a
Timestamped Copy or Export All Open Documents names it too. Before,
these files named the artboard the document opened on, or the first.
The open document is unchanged.
2026-11-07 04:40:02 +07:00
dotCooCoo d45ebe465b .ai save: the editing copy names the active artboard
The editing copy of a .ai or .ait file that VectorCraft saves or exports
now has `%_<n> /Int (CropAreaActive) ,` after the ArtboardArray in its
recorded part, and marks that artboard as the selected one. The app
records its view's active artboard before each command
(DocState::active_artboard; an index past the last artboard is the last,
as in the Artboards panel); without a view, the artboard the document
opened on (Document::opened_artboard) is written, else the first. A .ai
file read from its editing copy opens on that artboard. Before, the copy
named no active artboard and always marked the first as selected.

Docs: docs/mcp.md, docs/file-format-parity.md, the editingData option text.
2026-11-07 00:17:17 +07:00
Hamza 090667fb26 Languages: Bulgarian, with its quote marks and the fonts' Bulgarian forms
Bulgarian was not among the text languages, so Bulgarian text could not
be tagged and fonts drew their Russian-style Cyrillic. Bulgarian (bg)
is now in the language list (Character panel, character styles,
text.setFormat by name or tag, the Hyphenation preference's default
language), with CLDR's quotes, „…“ at either level. Shaped as bg, the
fonts' locl forms for BGR apply: in the bundled Source Sans 3 Italic,
в, д, ж, з, к, л and ю take their Bulgarian forms.

Fixes #5945
2026-11-07 00:17:16 +07:00
Don Omondi 591fc9b21e Paste: cells copied from a spreadsheet paste as a table of type over gridlines
Tab-separated rows with the same number of cells (quoted cells too)
become a group of point type, one per cell, in columns and rows over
0.5 pt gridlines; other text pastes as before. Fixes #5296.
2026-11-07 00:17:15 +07:00
dotCooCoo 42fe0bead3 Appearance and Eyedropper: reading an object's paint does not measure its bounds
The Eyedropper, the Live Paint check for paint to sample, the Appearance
panel's Characters chips and the colors the Libraries panel adds from
the selection found the paint behind the Fill or Stroke proxy through
Node::proxy_paint, which measures the object's geometric bounds. They
now read it with Node::proxy_paint_only.

- Eyedropper: the test for an empty fill reads the paint without
  measuring. A sample measures the object only for a gradient or a
  pattern, whose color depends on the point; a solid color gives its
  color at once.
- Live Paint: the check for paint to sample reads the paint without
  measuring.
- Appearance panel: the Characters row's two chips read the paint
  without measuring.
- Libraries: adding a color or a pattern from the selection reads the
  paint without measuring; a gradient is still measured for its
  placement.

The paints and colors are the same as before. On an open path of
200,000 anchors, the Live Paint check and the Eyedropper's sample for a
point inside it take 0.004 ms together instead of 26.0 ms in a release
build.

Docs: docs/target-app-parity.md.
2026-11-07 00:17:15 +07:00
Don Omondi b8ec5f4b23 Type: Alt + numeric-keypad codes type their character on Windows; type.insert takes a code (#5108) 2026-11-07 00:17:15 +07:00
Don Omondi 3f7f743ddb 3D effects: Extrude & Bevel, Inflate and Rotate apply to groups
A 3D effect on a group or layer builds a solid from every path,
compound path and type in it, all turned about one shared axis, as the
public docs describe for groups. Fixes #4506.
2026-11-07 00:17:14 +07:00
Don Omondi 5019846d08 Perspective grid: a grid dragged off its preset reports no preset name
perspective.grid.get reports define.name only while the grid still has
the preset's fields, as Define Grid's Preset menu shows [Custom]. Part of #5248.
2026-11-07 00:17:12 +07:00
Cursor AgentandAdithya Shankaran 4e11389bf0 Don't write over a library file that changed since it was saved
The name remembered for a user library file stays only while the file still
holds the bytes that were written. A file a device name could make, which can
no longer be read, is kept too, and the save goes to a new file.

Co-authored-by: Adithya Shankaran <adikshan11@users.noreply.github.com>
2026-11-07 00:17:12 +07:00
Adithya Shankaran 850fd620f9 Don't replace an .ase file whose owner isn't known
After a restart an .ase file called COM¹- could have been saved as COM¹ or as
COM¹-, and the same goes for its numbered files. Such a file is kept until it is
saved here, so a library named like that gets a new file, once per run. Other
names go on replacing their own file.
2026-11-06 23:05:29 +07:00
Adithya Shankaran a17efec894 Remember the name a library file was saved under
An .ase file can't say whose it is, so without this COM¹ saved after COM¹- wrote
over it. The app now remembers while it runs. After a restart COM¹ gets a new
file, which costs a copy and loses nothing.
2026-11-06 23:05:29 +07:00
Adithya Shankaran 857ba3972b Don't copy a library on every save when its format keeps no title
An .ase file reads its name from the file name, so COM¹ saved again looked like
another library and got a new numbered file each time.
2026-11-06 23:05:29 +07:00
Hamza b474525e4b Find Whole Word: a change of style inside a word is not a word boundary
Find and Replace matched each style run on its own, and the whole-word
test looked only at the characters inside the run, so a run's end read
as a word boundary: with cat coloured red in catfish, a Whole Word
search for cat replaced it. The test now looks past the run's ends at
the characters next to it in the neighbouring runs. Matches still stay
inside one run, as the command documents.

Fixes #5901
2026-11-06 23:04:21 +07:00
Hamza d316edaad9 Find and Replace: type inside a locked or hidden group is left alone
all_texts walked every node of a visible, unlocked layer and checked
only each text object's own lock and visibility, so a text object that
was itself shown and unlocked inside a locked or hidden group was found
and replaced. It now stops at a hidden or locked group, as it stops at
a hidden or locked layer: Find and Replace and Find Next with no ids
leave that type alone, however deep the group is.

Fixes #5900
2026-11-06 23:04:06 +07:00
NetterSilverandClaude Opus 5.5 a76e157de7 Keep a symbol's options when pasting it into another document
Fixes #5902

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:08:54 +07:00
Adithya Shankaran 0864db9494 Keep the other library when two names share a file name
A library called COM¹ is saved as COM¹-, which can be the file of a library
really called COM¹-, and the numbered file one of them then gets can be asked
for by a third name. Saving one replaced the other. Each now gets a free file
name, and saving it again goes back to that file.
2026-11-06 16:52:05 +07:00
Don Omondi 74b5966bbd Stroke Weight Up and Down: Cmd/Ctrl+Alt+. and , step the selected strokes' weight (#5797) 2026-11-06 16:19:37 +07:00
Don Omondi 41ceaadc7e SVG import: Gaussian blurs keep color-interpolation-filters; Gaussian Blur can blur in linear light (#5877) 2026-11-06 16:05:55 +07:00
Don Omondi dbbe8a9298 PDF and .ai export: objects' URLs become clickable links (#5785)
Each visible object with a URL (Attributes panel) is written as a Link
annotation with a URI action over its bounds on the page, trimmed to its
clipping groups. Hidden objects, non-printing layers and printed sheets get
none; PDF/X files leave them out with a warning. URIs are percent-encoded
to 7-bit ASCII.
2026-11-06 15:48:02 +07:00
dotCooCoo d58d47312f Legacy .ai export: black-and-white colorized images save as image masks
A colorized image whose pixels are all black or white (or clear) is
written as a 1-bit image mask (ImageMask 1): its color as the fill, a 0
bit for each black pixel and a 1 bit for each white or clear one. The
legacy reader opens it colorized again with the same clear pixels, so
the engine no longer flattens or omits it. Other colorized images are
written with their color in their pixels, over white, with the note
for transparent images.
2026-11-06 15:33:35 +07:00
Don Omondi 149eee2c70 Blend spine: moveAnchor refuses with alongside handle; spine built once per overlay and drag
object.blend.spine.moveAnchor now refuses any supplied `with` (even an
empty list) together with `handle`. Direct Selection builds the blend
spine once when drawing the picked points and once per drag preview,
instead of once per picked point. Part of #5285.
2026-11-06 15:32:52 +07:00
dotCooCoo 8f81e142a5 Reject invalid Tab Stop string parameters 2026-11-06 15:20:17 +07:00
dotCooCoo f48ac15d08 Text: a restyled empty frame keeps its style before an unedited empty frame
When a frame's text ends with a paragraph break and the next frame is empty, restyling that empty frame was undone on reflow: an unedited empty frame after it gave the story's last paragraph its own style again. A restyled empty frame now keeps its style against unedited empty frames that follow it. Unedited empty frames before it, later frames with text and later restyled frames still give the paragraph their style, as before.
2026-11-06 14:56:28 +07:00
Adithya Shankaran a455e72bf8 Automation roots: refuse COM¹ to LPT³ as Windows device names 2026-11-06 14:34:58 +07:00
dotCooCoo b0f383a1ab Text: empty later frames preserve an edited paragraph style 2026-11-06 14:08:37 +07:00
dotCooCoo b5da39d7b5 Commands: true/false flags reject other values everywhere
The remaining flags under crates/engine/src/cmd that read a boolean with
get(..).and_then(Value::as_bool).unwrap_or(..) now go through switch_or,
so a value such as "false" or 1 is an error instead of the default.
Omitting a flag or giving null keeps the old behavior.

- effect.move: copy
- aligned on the appearance.*, stroke.set, transparency.set and effect.*
  commands, checked before any early return
- transparency.viewOpacityMask: on
- view.printTiling: on
- plugin.install: replace and allowHeavy, checked for every plug-in
- edit.clear: guides, and path.deleteAnchors' read of it
- text.create and text.createInPath: vertical (already refused by
  check_switches, so their behavior is unchanged)
2026-11-06 13:47:16 +07:00
dotCooCoo 8a730bab6b Reject invalid New Art Basic parameters 2026-11-06 11:15:35 +07:00
dotCooCoo b26bd3d472 Reject invalid Guide Lock parameters 2026-11-06 10:41:08 +07:00
dotCooCoo 398131b9cc Reject invalid Transparency Grid parameters 2026-11-06 10:12:42 +07:00
dotCooCoo 48a3710fae Reuse selected object locations for large transforms 2026-11-06 09:58:43 +07:00
dotCooCoo b4a4f2aa4f Reject invalid Slice toggle parameters 2026-11-06 09:42:35 +07:00
Don Omondi 97697420f9 Variables: Load Variable Library keeps a bound text variable the library declares (#4136)
Loading a variable library deleted every variable of the document with its
bindings, including a text variable of the same name that the library
declares. Text made dynamic and then given a library's variable name lost its
binding, so selecting the library's data sets changed nothing. Such a
variable now stays bound, and the library's data sets change its text; other
variables are deleted as before.
2026-11-06 07:45:21 +07:00
Don Omondi f987c2bbb0 Intertwine: the object brought over leaves no hairline of the one below (#5337)
The area an Intertwine member is drawn over another in was exactly the
overlap face, so its edge lay on the edge of the member below. Both edges
were anti-aliased, and the member below showed through as a thin line along
them. The area now reaches 2 pt past the face into the parts where only the
raised member paints (where drawing it again changes nothing), so its edge
lies inside that member.
2026-11-06 07:23:35 +07:00
Don Omondi dfe628e906 Blend spine: Shift-click picks several spine points, which drag together (#5285)
Direct Selection kept only one picked point of a blend's spine. Shift-click
now adds a point of the same spine or takes it away, and dragging any picked
point moves them all. object.blend.spine.moveAnchor takes `with`, the other
points to move by as much, with the key objects on them, in one undo step.
2026-11-06 07:23:19 +07:00