Commit Graph
8941 Commits
Author SHA1 Message Date
dotCooCoo bbf4834f12 .ai import: a hidden clipping path's fill leaves with it
The fill read apart from a clipping path that was read on %_ lines, and
the copies of its subpaths, are recorded as read on those lines too, so
the copy of a look's art that is removed takes them along. The file
format notes list a compound clipping path whose subpaths paint
differently among the exceptions to a clipping path keeping its fill.
2026-11-08 11:22:37 +07:00
dotCooCoo 589fd863c4 .ai import: a compound path's plain subpath keeps its fill
In a compound clipping path whose subpaths paint differently, the fill
of a subpath that does not clip was read apart with the clipping
subpaths' fills. Only the clipping subpaths' fills are read apart now,
and a subpath that does not clip keeps its fill, as before.
2026-11-08 11:22:37 +07:00
dotCooCoo b33cdfd14f .ai import: a clipping path's fill read apart keeps its own ids
A fill read apart from a clipping path took a copy of the clipping
path's opacity mask whose art kept the original's ids, so the document
held two objects with one id. The copy of the mask's art, and the
subpaths of a fill read apart from a compound clipping path, now get
ids of their own and stay linked to the same swatches.
2026-11-08 11:22:37 +07:00
dotCooCoo 72959e6e80 .ai import: a multi-layer mask's fill paints once
Each layer a multi-layer mask covers gets a copy of the mask's path, so
a fill kept on that path painted in every layer, over the art the mask
covers in the layers before. The mask's fill is read as a path of its
own again, where the mask's path is, and the copies have no fill. A
fill read apart from a compound clipping path is a compound path whose
subpaths do not clip and have ids of their own. A fill read apart, its
subpaths and their points count toward the importer's object limit.

Docs: docs/file-format-parity.md.
2026-11-08 11:22:36 +07:00
dotCooCoo 6ab82ca71e .ai import: a filled clipping path keeps its fill
The reader took the fill off a clipping path in a file's editing data and
added a copy of the path, holding only that fill, under the art it clips.
A clipping path paints its own fill under the clipped art and its stroke
over it, so the reader now keeps the fill on the clipping path. A filled
clipping path opens as one path instead of two, in a clipping group, a
clipping layer and each level of nested clipping paths. Its note, tags and
gradient stroke (`1 BB`) are on that path as before.

A compound clipping path whose subpaths all paint alike keeps their fill.
When its subpaths paint differently, the fills of the filled subpaths are
still a compound path of their own under the clipped art, so a subpath
the file does not fill stays unfilled. Clipping paths in a compound path
that holds its drawn look still have their fills read as paths of their
own under the art, each with its clipping path's opacity mask.

Docs: docs/file-format-parity.md.
2026-11-08 11:22:36 +07:00
dotCooCoo 97df62547b Tests: the hex paste dialog tests release Enter after each press
A dialog's OK ignores a repeat of Enter, and an egui context keeps a key
down until it is released. The Color Picker, Swatch Options and Define
Grid tests pressed Enter in one context without releasing it, so every
press after the first was a repeat and the dialog stayed open. Each test
now releases Enter after pressing it.
2026-11-08 11:22:35 +07:00
dotCooCoo 0b6a6d44c6 Color fields: a hex paste never goes before a leading #, and a replacing paste drops earlier inserts
With the caret before a # that stays, bare digits are not inserted ahead of it; a full code replaces the text instead. A paste that replaces the field's digits also removes bare-digit pastes kept earlier in the same frame.
2026-11-08 11:22:35 +07:00
dotCooCoo a386b3e955 Color fields: a pasted hex code replaces the field's digits
A click put the caret among the six digits and the text box holds seven
characters, so only the first character of a pasted code went in. The
field then held no new color, and Enter closed the dialog with the old
color. A press on a field drawn before the hex field also dropped what
was typed, while Tab and Enter applied it.

The hex fields of the Color Picker, Swatch Options, Recolor Artwork, the
Color panel, Document Setup, Duotone, Color Themes and Perspective Grid
now share one set of rules:

- A click or keyboard focus selects the digits.
- A code of six digits, with or without #, replaces the digits wherever
  the caret is.
- A code of three digits also replaces them, unless the paste holds only
  those digits and exactly three digits stay. Those digits then edit the
  text in place.
- A paste of only one, two, four or five digits edits the text in place
  when it does not cover all of the text and the result has at most six
  digits.
- Any other paste leaves the field as it was.
- A press elsewhere commits the text in the frame of the press.
- A commit returns the digits without # and writes a three-digit code out
  as six, so Duotone reads a typed # and Perspective Grid, whose parser
  reads six digits only, takes a three-digit code.

Part of #5013
2026-11-08 11:22:35 +07:00
dotCooCoo 7d1f2062e3 Actions: Insert Stop pauses playback and shows a message
Squashed from #6028 onto main.
2026-11-08 11:22:34 +07:00
dotCooCoo fe0f3d919c .ai save: the editing copy keeps variables, data sets and bindings
Squashed from #6024 onto main.
2026-11-08 11:22:34 +07:00
dotCooCoo 57dbbbb41b Docs: .ai brush art spot color limits
The file-format entry states the 20,000-name limit across brush art and per section, the first-definition rule, and that colors past the limit still count toward the color mode.
2026-11-08 11:22:33 +07:00
dotCooCoo 8a34c89d7c .ai import: remove the duplicated editing-copy paragraph 2026-11-08 11:22:33 +07:00
dotCooCoo d180ad456e .ai import: brush art brings at most 20,000 color names, each once
Each brush art section keeps only the first definition of each color name, and at most 20,000 names. The art of the brushes that come in brings at most 20,000 different color names in all, every section's color operators before any section's gradient stops, so art that no brush uses or that a later section replaces takes no places. Colors left out still count toward the color mode.

A test also checks that a global process color which only a brush's art defines with its color operator stays a process color when a gradient stop names it.
2026-11-08 11:22:32 +07:00
dotCooCoo cbfca36c85 .ai import: spot colors in a kept brush's art become swatches
The reader kept a brush's art but dropped the colors it read from it, so
a spot color that only a Scatter or Art brush's art used made no swatch,
and the art's process colors did not count toward the document's color
mode.

The reader now keeps each brush art section's colors with its art. When
the document is finished, the colors of the art that a kept brush uses
are added after the rest of the file's, in the order the art was read:
its spot colors make swatches and its colors count toward the color
mode. Art that no kept brush uses (a pattern brush's tiles, a section
that a later one of the same name replaced, a section inside a symbol's
definition) brings none of its colors, and a spot color that the page's
art, a pattern or a symbol also defines keeps the value defined there.

Part of #637.
2026-11-08 11:22:32 +07:00
dotCooCoo 77b72da70d .ai import: Stair Step spacing is fitted from its glyphs' origins
Stair Step's glyphs stay upright, so their matrices never show the path
turning and the spacing fit found no turn at all. The fit now takes the
path through each glyph's origin, which lies on the path, and measures
how far it turns over a step from the circle through three origins in a
row.
2026-11-08 11:22:32 +07:00
dotCooCoo 2fbd60a6dd .ai import: 3D Ribbon spacing holds where the path turns back
3D Ribbon's level baseline faces the way the path runs, so where the path
runs left its baseline and foot are turned round. The spacing fit read
that as a half turn and left the pair out, so a short run on either side
of a vertical tangent came in with no spacing. The fit now turns those
glyphs back first. With an alignment, the two glyphs either side of that
point lie on opposite sides of the path, and that pair is left out.
2026-11-08 11:22:31 +07:00
dotCooCoo 2faec2fa11 .ai import: Skew, Stair Step and Gravity measure the path's turn square to it
The spacing fitted from a type-on-a-path layout measured each turn against
the glyph's own foot. Skew's feet stay upright, so on a tilted or leftward
path the turn picked up a false angle and the fitted spacing was wrong (a
straight path at 30 degrees gave -9.7 instead of 0). Effects other than
Rainbow and 3D Ribbon now measure the turn against the baseline turned a
right angle.

The file-format doc states that a frame whose options set key 6 to true
keeps the default effect, alignment and Flip.
2026-11-08 11:22:31 +07:00
dotCooCoo bc5eb7aa47 Docs: state the limits of .ai type on a path import
Align to Path takes its distance from the path from VectorCraft's font metrics, and the fit under Gravity does not reproduce the file's layout.
2026-11-08 11:22:31 +07:00
dotCooCoo 53f6aafe87 .ai import: type on a path keeps its effect, alignment and flip
Type on a path from a file's text document opened as Rainbow on its
baseline, not flipped, whatever Type on a Path Options the frame kept.
Each frame now keeps its effect (Rainbow, Skew, 3D Ribbon, Stair Step,
Gravity), Align to Path (Ascender, Descender, Center, Baseline) and Flip.
A code or flag that can't be read leaves that option at its default.

Flipped type opens with the frame's path reversed once, after a copied
object takes its own art path, and an open path's end bracket stays so
the type starts where the file's range ends.

Spacing is still fitted from the file's layout. The fit now measures along
the path when Align to Path moves the baselines off it, and it follows the
glyphs' feet under 3D Ribbon, whose baselines stay level.
2026-11-08 11:22:30 +07:00
dotCooCoo bc19dd64db Simplify: Convert to Straight Lines keeps the tip of a path that turns back past its end
`rdp` measured each sample to the infinite line through the ends of the
line that would replace it. A sample beyond either end could be far from
that line segment and still be dropped: a line to (100, 0), a 0.2 pt
U-turn and a line back to (50, 0.2) became one line at a tolerance of
1 pt, leaving the tip 50 pt away. `rdp` now measures to the segment, so a
sample beyond an end is measured to that end, and a zero-length chord
measures to its point as before.

The test `a_path_that_turns_back_past_its_end_converted_to_straight_lines_keeps_its_tip`
covers that path.
2026-11-08 11:22:30 +07:00
dotCooCoo 489552c14b Simplify: the slider's Escape test previews level 0, which changes the path
The test's path zigzags by 0.1 pt. At level 100 the tolerance is 1/32 pt,
and a fit now stays within it, so the preview at that level leaves the
path as it was. Level 0's 32 pt tolerance takes the zigzag out.
2026-11-08 11:22:30 +07:00
dotCooCoo 4a7d732197 Simplify: smoothing a densely sampled path takes time in proportion to its samples
Object > Path > Smooth weighed every sample within three spreads of each
point, so the work grew with the square of the anchor count on traced
paths: 360 steps per sample for 1,000 anchors and 2,949 for 16,000.
Consecutive samples within 1/16 of a spread of each other are now
weighed together as their count at their mean place, found through a
tree of sums over the run's samples. A traced outline takes 203 to 286
steps per sample from 1,000 to 64,000 anchors, and every smoothed point
stays within a thousandth of a spread of weighing the samples one by
one (300 random runs).
2026-11-08 11:22:29 +07:00
dotCooCoo 199c4c3467 Simplify: fitted curves stay within the tolerance of the path, and serpentines simplify faster
The curve fitter split a piece over the tolerance at its worst point and
accepted any piece at depth 41 whatever its error. A worst point near the
end of a piece peels off a few points, so a long path refitted nearly all
of its points at every level. A serpentine of 200,000 anchors kept pieces
with a fitted error of up to about 1,598 pt at a tolerance of 1 pt.

Simplify also took at most 256 samples per segment, so the lines between
the samples on a long arc could be farther from it than the tolerance.

- `fit_rec` moves a split that is near an end of its piece to two fifths
  of the piece from that end, so the depth is logarithmic, and it keeps no
  piece over the tolerance. A piece of 32 points or more is first fitted
  to about 16 of them, and it is split when that fit is over the tolerance.
- A curve is also rejected when it is over the tolerance from the lines
  between the points it fits, with samples close enough that it stays
  within 1% of the tolerance of the chord between them. A pair of points
  that would need more than 1,024 samples counts as too far.
- `join_pieces` joins neighboring pieces when one curve fits them, up to
  33 pieces per curve.
- `sample_steps` takes enough samples of a curved segment to keep it within
  0.5% of the tolerance of the lines between them. A segment that would
  need more than 65,536 samples stays as it is, and so does a segment
  after a run's first 16,777,216 samples or 2,097,152 result segments. With
  Convert to Straight Lines such a segment becomes evenly spaced lines, up
  to 65,536. A fit holds at most 1,048,576 samples, so a longer run is
  fitted in parts.
- Simplify fits at 98% of the tolerance, since turning a nearly straight
  curve into a line moves it by up to 1.5%.
- Above slider level 0, Object > Path > Smooth fits a whole closed path
  with no corner along one direction across its start, when no segment
  stays as it is and the path is fitted as one part. A direct-selected
  section does not get this. The start can still have a kink: a first or
  last fitted curve that is nearly straight becomes a line, which leaves
  along its chord, and a refit with more anchors than the path is
  discarded, so the path stays as it is.

A closed subpath that refits to one curve is still dropped, as on main, so
it can end up farther than the tolerance from the result. docs/mcp.md
states this.

The anchors of every simplified path can differ from before, and so can
the results of the Pencil, Blob Brush and Smooth, which use the same fit.
2026-11-08 11:22:29 +07:00
dotCooCoo a0bd9bbbad Artboards: duplicate, copy, cut, move and rearrange journal whether locked and hidden art goes
Squashed from #5994 onto main.
2026-11-08 11:22:29 +07:00
dotCooCoo b8233f7c19 .ai save: a reopened file opens on the artboard active when it was saved
Squashed from #5975 onto main.
2026-11-08 11:22:28 +07:00
dotCooCoo 74863fa778 Engine: execute refuses keys a command doesn't name, in every build
Session::execute ran check_params only in the engine's own tests, so a
direct execute in a release build ran a misspelled key with the
command's default. It now checks every call, the commands a command runs
included, as MCP and the control port already did. A command.batch or
an action.batch is checked before its first step runs, and each step is
checked again as it runs, so an effect.setParams step is checked
against the effects the steps before it added.

Saved steps play through Session::replay, which takes keys a command no
longer reads, so an action recorded by an older version still plays and
Session::replay still plays an older journal. The Actions panel's Play
(Accelerated, Step by Step and Pause For, its function keys and
ui.action.play) and the Batch dialog use it.

Docs: the cmd/params.rs module doc, the Parameter keys section of
docs/mcp.md and a line in docs/target-app-parity.md describe both
paths.
2026-11-08 11:22:28 +07:00
BFlat a7733acc7e Merge pull request #5984 from BOXELS/fix/5980-nssavepanel-null
#5980: macOS save sheet uses alloc/init instead of +savePanel
2026-11-08 12:00:58 +08:00
Luis 21249399c6 Release: refresh contributor credits for each release (#5997) (#5998)
About > Contributors still showed an old snapshot, because nothing
regenerated contributors/contributors.json before a release. Opt in to
the shared refresh from craft-common-actions: a read-only credits job
regenerates the snapshot for the release commit, and the shared release
workflow installs it before every source build. The snapshot is not
committed back.
2026-11-08 02:07:42 +01:00
BFlat ff9deee46d Merge pull request #6015 from helenkwok/web/page-api
vectorcraft-web: window.vectorcraft takes its request functions from craft-web-api
2026-11-07 10:32:48 +08:00
BFlat 2b13421da5 Merge pull request #6012 from dotCooCoo/fix/ai-legacy-vertical-type
.ai import: legacy type keeps its writing direction, Tate-chu-yoko runs and upright Roman characters
2026-11-07 10:32:31 +08:00
BFlat c54679d4a7 Merge pull request #5985 from dotCooCoo/fix/text-story-current-thread
Type: story-wide panel changes follow the story's current thread
2026-11-07 10:32:10 +08:00
Helen Kwok f9bf685cbd vectorcraft-web: window.vectorcraft takes its request functions from craft-web-api
request, execute, commands and screenshot move onto the shared crate (storytold/craft-libs#413): a timeout on every call, the screenshot's PNG also in data, and docInspect and uiInspect beside them. A call the page gave up on gives up its request if the app has not started it, so the app skips it. openFile, captureDownloads, files, readFile and inspect stay VectorCraft's own, on the same object.
2026-11-07 09:18:00 +07:00
dotCooCoo fe9d62e05e .ai import: legacy type keeps its writing direction, Tate-chu-yoko runs and upright Roman characters
The legacy reader skipped `TV`, `Tv`, `Ty` and `TY` of section 15.3.4 of
the version 7 file format specification, so type set with `1 TV` opened
horizontal.

- `TV` sets a text object's writing direction. VectorCraft keeps one
  direction for each text object, the one in force at its first
  character, with a warning when a later character has another.
- `Ty` marks a Kumi block as a Tate-chu-yoko run, which is VectorCraft's
  own mapping, and `TY` does the same in horizontal type. A `TY` block in
  vertical type opens unmarked. Two blocks with nothing between them open
  as one block.
- `Tv` sets Standard Vertical Roman Alignment.

All four are modal, and an operand other than 0 or 1 leaves the value as
it was.

Docs: docs/file-format-parity.md.
2026-11-07 09:17:53 +07:00
dotCooCoo 1c2a8c7f7a Type: Justification after Select All covers a frame whose thread broke
After Select All in threaded text, the Justification dialog kept the edited
frame's selected range when that frame was no longer threaded at OK. A thread
broken after the frame moves the rest of the story into it, so the paragraphs
past the old range kept their values. The dialog now drops the range and
changes every paragraph in that frame.
2026-11-07 09:17:48 +07:00
dotCooCoo 24a42c5bb0 Type: Tabs indent markers apply to every paragraph when threaded text reflows
With mixed indents, the Tabs panel sends one text.setFormat per paragraph,
addressed by frame and byte offset. Threaded text reflows after each step,
so an indent that made earlier paragraphs wrap onto more lines moved text
into the next frame, and the later steps landed on the wrong paragraphs or
on none. The steps now run from the story's last paragraph to its first, so
each step's offset comes before every change already made.
2026-11-07 09:17:48 +07:00
dotCooCoo c592b31a1e Type: Tabs indent markers and Justification follow the whole story
After Select All in threaded text, the Tabs panel's indent markers read
the indents of the edited frame alone. When another frame's paragraphs
had a different left indent, a marker drag changed them by the edited
frame's values, or left them alone. The markers now take every
paragraph of every frame in the story, so each paragraph keeps its own
left indent or first-line position.

The Justification dialog kept the frames of the story from the moment
it opened. OK now acts on the frames in the edited frame's thread when
it is pressed, so a frame that left the thread while the dialog was
open is not changed, and a frame that joined it is.
2026-11-07 09:17:48 +07:00
dotCooCoo 40a46e539d Type: story-wide panel changes follow the story's current thread
After Select All in threaded text, the panels read the frames from the
list saved at Select All. A frame that had left the thread still took
Paragraph, Tabs, Justification and Text Styles changes, a frame added
later did not, and when the first saved frame had left, Character,
OpenType and font changes went to that frame alone.

text_story now reads the frames of the edited frame's thread from the
document. Locked and hidden frames stay in the story.
2026-11-07 09:17:46 +07:00
BFlat 845306373b Merge pull request #6017 from dotCooCoo/fix/ai-drawn-look-count
.ai import: drawn looks in pattern and brush art count in the note
2026-11-07 10:15:26 +08:00
BFlat 936f6034da Merge pull request #6014 from mockydevs/fix/ai-import-eid-heading
EPS/.ai editing data: a transparency set inside a clipping group stays after it
2026-11-07 10:15:10 +08:00
BFlat 7e3601d11c Merge pull request #6011 from mockydevs/fix/ai-import-blend-straight-spine
EPS/.ai editing data: blends with no spine written come in live
2026-11-07 10:14:54 +08:00
BFlat 7db6d41ec0 Merge pull request #5992 from mockydevs/fix/ai-symbols-before-art-styles
EPS/.ai editing data: symbols defined before the art styles take them
2026-11-07 10:14:38 +08:00
dotCooCoo 0d91749c74 .ai import: drawn looks in pattern and brush art count in the note
An object read as its drawn look counted in the import's "came in as
their drawn look" note when it was on the page or in a kept symbol, but
not when it was in a pattern's art or a brush's art. A file whose only
such objects were in a pattern or a brush opened without the note.

- A kept pattern's drawn looks count, once for each pattern name: a
  definition that a later one of the same name replaces no longer counts.
- A brush's art counts once a brush that is kept uses it; art no kept
  brush uses adds nothing.
2026-11-07 09:00:23 +07:00
Don Omondi ed00a8870e EPS/.ai editing data: a transparency set inside a clipping group stays after it
Q restores the painting state but not the transparency (Xy): the writer
sets Xy only when the next object's state differs from the last one it
wrote. Restored at Q, type after a clipping group following a Screen glow
drew in Screen and nearly vanished on a light background.
2026-11-07 09:00:20 +07:00
Don Omondi 95ac8d7cc7 EPS/.ai editing data: blends with no spine written come in live
A blend whose spine was never edited has only its keys in the editing
data, no spine part. Its spine is the straight lines between the keys'
centres in the record's order; read as such, the blend comes in live
instead of as its drawn steps.
2026-11-07 09:00:15 +07:00
Don Omondi 2626791d7d EPS/.ai editing data: symbols defined before the art styles take them
Illustrator 24 writes its symbol definitions ahead of %AI9_BeginArtStyles,
so a styled object inside a symbol found no style and came in as its drawn
look. The pre-pass that finds swatch names now also keeps the first
top-level art styles section, which the reader parses before the art.
2026-11-07 08:59:55 +07:00
BFlat d2b82d6b32 Merge pull request #6005 from dotCooCoo/fix/prefs-set-folder-check
Preferences: prefs.set refuses a missing or unusable recovery or plug-ins folder
2026-11-07 09:57:12 +08:00
BFlat 4802247106 Merge pull request #6004 from dotCooCoo/fix/dialog-enter-repeat
Dialogs: a held Enter or Escape stops at the dialog it opened or closed
2026-11-07 09:56:52 +08:00
BFlat 0864d63ee2 Merge pull request #5988 from dotCooCoo/fix/offset-contour-check-index
Offset Path: the spurious-contour check tests only the source segments near each contour
2026-11-07 09:56:37 +08:00
BFlat 3637aebc5f Merge pull request #5983 from dotCooCoo/feat/artboard-context-menu
Artboards: the Artboard tool's canvas menu cuts, copies, pastes, renames, rearranges and exports artboards
2026-11-07 09:56:22 +08:00
dotCooCoo 9b9803aa0f Tests: the unusable-folder prefs test restores folder modes when it fails
set_refuses_a_recovery_or_plugins_folder_it_cannot_use takes write or
list permission away from two folders. A guard now gives each folder mode
0o755 when it is dropped, also when an assertion panics, so the test
folder is removed afterwards.
2026-11-07 08:38:20 +07:00