232 Commits
Author SHA1 Message Date
Marco Arnulfo 83600d3335 Color management: loading the working RGB profile again under its name takes effect at once 2026-10-24 19:36:34 +02:00
Patrick LenzenandClaude Opus 5.5 504b549a78 Fix #3621: bound function-based shading reads per import
Review follow-up. A function-based shading is read once per import (kept
per shading pattern and inks), and all of them draw on one budget of
200,000 function evaluations; past it a shading comes in as its average
colour, then flat grey, each with a warning that names the cause. A
gradient read leaves enough of the budget to average another shading.

A colour that changes in step with the sampling grid is checked across
the middle of the domain before an axis is chosen, so it isn't read
along the wrong one. The grey fallback warning for other shadings names
only them.

Tests: the same shading on 2,000 paths is read once; 40 distinct
shadings stay within the budget; a hostile file (5,000 fills, 20
shadings with 500-step functions) opens in 0.8 s, 253 s before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 19:27:44 +07:00
Fabian GrajalesandClaude Opus 5.5 1adbf259f7 Open .ai files with their swatches: the palette, its colour groups and unused spot colours
The editing data's palette (%AI5_BeginPalette) was skipped, so a .ai file
opened with the default swatches plus only the spot colours its art used:
inks the art doesn't use (a Pantone set kept in the file) and colour groups
were lost. The palette is now read in order (process, spot in CMYK, RGB or
Lab, and global colours, gradients and patterns, colour groups; versioned
content read once) and replaces the default swatches.

Xs/Xz (Registration at a tint, as printer's marks use it) are read as the
Registration swatch: before, art painted with them stopped the editing data
from being read at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 17:46:20 +07:00
vectezaandClaude Opus 5.5 1a6bc267d5 Dimension tool: Linear picks show and snap to Smart Guides
Picking the two points of a Linear dimension snapped to anchors and to the
line under the pointer, but showed no guides and ignored the art around it.
The picks now go through the drawing tools' Smart Guides (DrawSnap), hovering
too, so the guides show before each click: a point slides into line with other
objects' anchors and centres, and the second onto the construction guides
through the first. An anchor of the line under the pointer still wins (it
links the dimension to that path), then a guide, then the closest point of the
line (#2432). The guides go once the dimension is being placed.

Test: linear_picks_show_and_snap_to_smart_guides (fails on the old tool, which
showed no overlays while hovering). docs/dimensions.md updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 17:46:19 +07:00
aiongg 8cc69ed45c CI: one test binary per crate, nextest, line tables only (#3855)
* CI: line tables only for workspace crates, no debug info for dependencies

* CI: one integration-test binary per crate

Each crate's tests/*.rs files become modules of tests/it/main.rs. Files that change
process-wide state (Color Settings, the font database, installed plug-ins, the working
directory), the fuzz sweeps and the test targets the Affinity corpus workflow names stay
their own binaries.

* Docs: name the integration tests' new paths

* CI: xtask test with nextest and doctests, one wasm check, clippy --keep-going

cargo xtask ci runs the tests with cargo nextest (cargo test when it is not installed),
builds them with --lib --bins --tests, runs the doctests as their own step, and checks
the wasm crates in one cargo check --keep-going call.
2026-10-23 21:36:10 -04:00
el-j 71a669635c M2.1: prevent click-through on open path fills and support segment toggle (#3405) 2026-10-24 06:00:23 +07:00
bflatastic c2ad9f8bbb Type: emoji go with type that effects reshape and Expand Appearance
Squashed from #3879 onto main.
2026-10-24 04:49:06 +07:00
Taha-Mahmoodi f365468684 Fix #3233: an opened dimension whose label characters are off the colour mode is redrawn too
Review follow-up (CodeRabbit): a text node's characters carry their own fill
(CharStyle), saved apart from the node's appearance, so conform also checks
the runs of the label. dimension_colours_follow_the_document_colour_model
opens a CMYK document whose label characters alone are RGB; without the
change they stay RGB.
2026-10-23 06:13:00 +07:00
Louise FoxandClaude Opus 5.5 1505384501 Plug-ins: filters keep data with the document (G17)
A filter plug-in had nowhere to keep settings that belong to the whole
document rather than to one object, such as the pens a plotter holds or
a machine profile: it could only ask for them again on every run.

- A filter's output may hold documentData: any JSON of at most 1 MiB
  (objects::MAX_DOCUMENT_DATA) that replaces what the plug-in keeps with
  the document, null removing it, in the same undo step as its objects
  (objects::decode_output). Too much refuses the output and the
  document stays as it was.
- A filter gets it back in _context.documentData (null until it keeps
  something). A filter reads and writes only its own.
- It is the document's foreign data, Document::unknown["pluginData"] by
  plug-in id (vectorcraft_doc::plugin_data), where the perspective grid
  and the brushes keep theirs. So the native file needs no section of
  its own for it, and hosts from before keep it when they save. An
  exporter is given the whole document, so it can read every
  plug-in's. Nothing else lists it, as the reference app's Document
  Info lists no plug-in data.

A live effect isn't given it: its geometry is worked out from the
object alone wherever it is drawn.

Tests: plugin_data.rs (each plug-in its own entry, forgetting what isn't
kept copies nothing, a damaged value holds nothing), objects/tests.rs
(kept, forgotten, refused over the limit), tests_plugins.rs (one undo
step with the objects, read back, not given to another filter, kept by
the native file among its foreign data, refused over the limit with the
document unchanged, null removes it) and tests/plugin_export.rs (an
exporter finds another plug-in's). The size error has its row in each complete
catalog (COMPLETE_MESSAGES).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhvnnnD5fTFupK8WWq35un
2026-10-23 05:40:00 +07:00
Taha-Mahmoodi 3d24c663d2 Fix #3233: dimensions are redrawn in the colour mode after Document Color Mode and on open
Follow-up to #3702 (CodeRabbit): dimensions are only redrawn when their
recipe changes. Document Color Mode converted their art through the colour
profile, so black became a four-ink C67 M67 Y67 K100 again, and CMYK files
saved before #3702 kept RGB dimension art.

dimension::refresh now also redraws when the document's colour mode
changes. dimension::conform, run when a document is opened or reverted
(Session::add_document, replace_document), redraws a dimension whose art
has colours in the wrong model; one that can't be redrawn keeps its art.

dimension_colours_follow_the_document_colour_model covers RGB to CMYK and
back, and a CMYK document with RGB dimension art being opened. Without the
change it finds C0.67 M0.67 Y0.67 K1 after Document Color Mode.
2026-10-23 04:26:05 +07:00
Taha-Mahmoodi 866bf61116 Fix #3233: in a CMYK document dimension lines and labels are CMYK, black on the K plate only 2026-10-23 03:35:07 +07:00
bflatastic 997dd8d2bd SVG import: images in images share the depth cap, SVGZ images one unpacking cap (#3562, #3563)
Squashed from #3676 onto main.
2026-10-23 01:28:52 +07:00
Taha-Mahmoodi bd6d076484 Effects: Sketch › Conté Crayon draws the dark areas in a black crayon and the light areas in a white one on textured paper 2026-10-22 21:18:34 +07:00
dwetscherandClaude Opus 5.5 2d7004f972 AF.5: cap dash patterns at 64 lengths
A line style's `Patn` was copied twice (filtered, then scaled) whatever its
length, before any budget saw it. Patterns longer than 64 lengths now import
as solid lines, with a warning.

Addresses CodeRabbit's third review on #3548.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 18:44:39 +07:00
dwetscherandClaude Opus 5.5 b3ccdf01a7 AF.5: nested overlays innermost first; embedded master pages; text outline budget
- Overlays bake child-first: a group's overlay now lies over its children's
  (they used to be applied after it and win). Each one pays out of the
  import's overlay work budget.
- Master page instances inside embedded documents are left out with a warning:
  their indices count the embedded document's masters, which aren't kept, and
  would show the outer document's.
- A text outline's gradient stops and dashes are charged to the text attribute
  budget, like the fill's.

Addresses CodeRabbit's second review on #3548.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 18:24:57 +07:00
dwetscherandClaude Opus 5.5 f73a3df974 AF.5: bound hostile Affinity files; fuzz the new import paths
- One budget for the whole read (layers, pixels, characters, table
  cells), shared with embedded documents and charged also when their
  reading fails; the first reading is dropped before the second.
- Tables: at most 65 536 cells; lines and cell formats that don't
  match the grid are left out instead of shifted.
- Paragraph formats matched to runs by binary search; font names
  longer than 256 bytes are dropped.
- Overlays: one work budget per import; encoded originals decoded
  within their layer's size; table cell outlines recoloured too.
- Engine: master page copies capped, each page's master found in
  linear time, line gaps looked up once per font, the working CMYK
  table built once, objects past MAX_COORD left out.
- Embedded content stays with its node; pixel masks under artistic
  text are read whole and include stroke reach; gradient overlays on
  hidden layers are kept.
- Render: contexts and raster effect images stay within
  MAX_RASTER_SIDE per side (vello overflowed near u16::MAX; found by
  the new fuzz target).
- Tests: affinity tests/import_limits.rs, feature samples in
  import_fuzz.rs, shared builders in engine/tests/support/.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 18:24:57 +07:00
dwetscherandClaude Opus 5.5 fd50bd6f84 AF.5: Affinity import — strokes, text decoration, text placement, outline effects, CMYK photos
- Strokes set to None no longer import as black 1px strokes.
- Underline and strikethrough are kept.
- Frame text starts at cap height; lines hanging out of a frame make it taller;
  mixed-alignment artistic text lines up within its longest line; line breaks
  inside a paragraph get no paragraph spacing.
- Table cell text taller than its cell is centred and stays visible.
- Outline effects on stroked shapes are drawn around the stroke.
- CMYK pixel layers go through the document's embedded CMYK profile; without
  one, opaque layers stay CMYK.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-22 18:24:57 +07:00
bflatastic 92f295d2a2 Blend: Ease In / Ease Out and separate Color Ease in Blend Options (#1124)
Squashed from #2539 onto main.
2026-10-22 04:40:10 +07:00
bflatastic 10a178f64a Shape Builder: Gap Detection and Tool Options (#2523)
Squashed from #2526 onto main.
2026-10-22 02:53:17 +07:00
bflatastic 755f2b9158 G2: Effect › Artistic › Rough Pastels raster filter
Squashed from #3044 onto main.
2026-10-22 00:23:50 +07:00
bflatastic bbe026607f Variables: Load Variable Library… reads a variable library's text variables and data sets
Squashed from #2853 onto main.
2026-10-22 00:12:39 +07:00
bflatastic 0a3628f62f G2: Effect › Artistic › Dry Brush raster filter
Squashed from #3026 onto main.
2026-10-21 23:38:02 +07:00
Dan Green e8bd0ac410 Distribute: selected ruler guides are spaced with the objects (#1597 in part)
Distribute Objects and Distribute Spacing space the selected ruler
guides across the axis (vertical ones for the horizontal options,
horizontal ones for the vertical options) together with the selected
objects, as items with no size. Guides along the axis stay where they
are. Two or more guides selected without objects are enough, and the
Align panel's Distribute buttons are enabled for them. With Align to
Artboard, the outermost items go to the artboard's edges.

A key guide (select.key {guide}) is one of those items. With a spacing,
it stays put and the other items are spaced from it, as from a key
object with no size; a key guide along the axis leaves everything where
it is. The Align panel sends the Distribute Spacing value with both
rows of Distribute buttons while a guide is the key, and enables the
value for it.
2026-10-21 15:44:27 +07:00
NetterSilverandClaude Haiku 5.5 4444450a44 Keep stroke weight in document space on transformed type
Fixes #3241

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 15:42:35 +07:00
Louise FoxandClaude Opus 5.5 24936cd006 Plug-ins: exporters write file formats of their own (G17)
A plug-in could filter objects or reshape them as a live effect, but
nothing it made could leave the app: pen plotters, cutting machines and
laser cutters read formats VectorCraft doesn't write. An exporter is a
third kind of plug-in ("kind": "exporter", with the file "extensions"
it writes), in the same sandbox and with the same ABI v1 exports.

- It adds a write-only format, plugin.<id>. document.formats lists it
  with its plug-in id, and document.export writes it by that id, or by
  a path ending in one of its extensions that no built-in format has;
  "params" carries its parameters, checked against its manifest as a
  filter's are.
- File › Export › Export As lists it after the built-in formats, with
  Use Artboards greyed out, since the plug-in writes the whole
  document. One with parameters asks for them in a dialog generated
  from its schema (pluginExport) once the file is picked.
- The plug-in is given the native file's JSON (format::json_part:
  images of 8 KiB or more without their data) and _context {mode:
  "export", extension, name}. It returns {"dataBase64", "warnings"?}
  or {"error"}. The host writes the file, and nothing when the plug-in
  declines or returns bad data.
- An export gets 10 G instructions plus 4,000 per byte and 300 s.
  Background Export's Cancel stops it between two slices of that
  budget (Error::Cancelled), and a cancelled live-effect run is not
  cached as a failure.
- plugin.run and Object › Plug-ins say where an exporter is instead of
  calling it a live effect.

engine/tests/plugin_export.rs reads back what an exporter is handed
with wat::echo_exporter, an exporter whose file is its input or its
parameters (its base64 encoder is written in WAT). The document it is
given is document.save's JSON, without the large image's data, and its
parameters arrive defaulted, clamped and with the file's name and
extension. It also covers listing and removal, built-in extensions
winning, selectedOnly, declines, bad base64, missing data, warnings,
and Cancel stopping a plug-in that never returns. It is a test binary
of its own: installed plug-ins are process-wide, and the format-list
unit tests check exact lists. dialogs/tests_plugin_export.rs drives
Export As headlessly; plugins/tests/abi.rs covers the budget, the
parameters and the echo exporter; format/tests/native_v4.rs covers
json_part.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhvnnnD5fTFupK8WWq35un
2026-10-20 08:17:48 +07:00
Martin Vlach f664564e97 Effects: Sketch › Bas Relief, Charcoal, Photocopy and Stamp (#2527)
Squashed from #2533 onto #2525 (Smudge Stick, Sponge, Watercolor): both sets in PIXEL_EFFECTS (45), the catalogs and the docs, whose counts now read 45/57.
2026-10-20 06:43:50 +07:00
Martin Vlach 5251246778 Effects: Artistic: Smudge Stick, Sponge and Watercolor (#2435)
Squashed from #2525 (its merges of main dropped).
2026-10-20 06:32:32 +07:00
Dan Green 6bed87b5fb Libraries panel: the list scrolls; items and groups reorder by dragging, items drag into groups
The list fills the panel down to its bottom bar and scrolls, also while a drag nears its top or
bottom edge (in the dock and in a flyout, as the Layers panel does).

Dragging in the list:
- an item (colour, text style or graphic) dropped on another of its kind lands just before or
  after it, by the pointer's half, an accent line showing where, and joins that item's group;
- dropped elsewhere on a group (its header too, even collapsed) it goes into that group, and on
  the ungrouped items out of its group, the section outlined; with every item grouped, a
  "Drop here to take it out of its group" strip shows while one is dragged;
- a group dragged by its header goes before or after another group.
Colours and graphics still drag onto art and the canvas as before.

Engine: library.moveItem takes `before` / `after` (an item of the same kind) to place the item
in its kind's order, which the panel shows; the new library.moveGroup {group, to} moves a group.
Text styles drag as PanelDrag::LibraryItem, groups as PanelDrag::LibraryGroup, and a library
colour's Paint drag names its library item, so the panel tells it from a swatch dragged in.

Tests: reordering and moving in the engine, the commands in the library fuzz test, and panel
tests that drop styles and group headers and check the list scrolls and scrolls while dragging.
Strings translated in the 23 catalogs; docs/mcp.md and the parity row updated.
2026-10-20 05:28:17 +07:00
MarekSpodBiedry 2ff683d5fe G2: Effect › Sketch › Chrome raster filter
Squashed from #2985.
2026-10-20 04:20:50 +07:00
nrq6993-a11y 917aa29dd5 G2: Effect › Sketch › Reticulation raster filter
Squashed from #2976 onto Chalk & Charcoal and Graphic Pen.
2026-10-20 02:54:06 +07:00
nrq6993-a11y 11adb72d44 G2: Effect › Sketch › Graphic Pen raster filter
Squashed from #2967 onto #2972 (Chalk & Charcoal): both filters in the effect catalog, PIXEL_EFFECTS and the Sketch label and filter tests.
2026-10-20 02:52:33 +07:00
dotCooCoo 926ca62952 Simplify: Object › Path › Simplify opens a slider with Auto-Simplify and More Options
Squashed from #2555 (its merges of main dropped).
2026-10-20 02:06:54 +07:00
dotCooCoo 007f536c79 Effects: effect art redraws when fonts arrive or the color settings change
The renderer kept the art it made for an object's effects while the
object stayed the same. In the web build, type that a geometry effect
outlines, on its own, in a group or in a symbol's art, kept the
substitute font's outlines after its own font loaded. On the desktop,
Refresh Font List left them on a symbol instance whose art holds the
type. Objects with Revolve, Extrude & Bevel, Inflate or Rotate, and
objects and groups with a color adjustment, kept the colors they were
shaded or recolored with after the color settings changed. Live objects
already followed both.

The renderer now keeps that art with the inputs it keeps for the art
of live objects: the font generation when the art holds type, the art
of the symbols it shows, and the color settings. A temporary release
test timed the added key at 11 microseconds a frame for 400 type
objects with a drop shadow and 0.77 ms for 400 symbol instances of 100
paths each with a drop shadow, against warm frames of 5.3 and 72 ms.

Docs: docs/target-app-parity.md, docs/gaps.md.
2026-10-19 22:19:55 +07:00
sungjea kimandClaude Opus 5.5 515569470c Anchor Point tool: Shift-drag on an anchor moves it, handles and all
With the Anchor Point tool, a drag from an anchor pulls out smooth handles.
Shift held at the press now moves the anchor instead, its handles keeping
their offsets; Shift kept down during the drag keeps the move at 45° steps
(the constrain angle), released it frees it. A Shift-click that doesn't
move leaves the anchor as it was. A selected blend's spine points follow
the same rule (object.blend.spine.moveAnchor).

The move goes through a new engine command, path.moveAnchor
{id, subpath?, anchor, x, y}, since path.moveAnchors only acts on the
direct-selected anchors and the Anchor Point tool keeps no anchor selection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 22:19:53 +07:00
dotCooCoo 372887fc6c EPS and .ai import: images with TIFF predictor 2 draw their samples at every bit depth
The PostScript interpreter that reads EPS and PostScript .ai files
accepts a FlateDecode or LZWDecode filter with Predictor 2 at
BitsPerComponent 1, 2, 4, 8 and 16, but undid the horizontal
differencing only at 8 bits. At the other depths the filter gave back
the stored differences, and the image drew them instead of its
samples.

The predictor is now undone at every depth it accepts, as ISO 32000-1
§7.4.4.4 describes: each component adds the same component of the
pixel to its left, within its bit width, and each row starts again.
16-bit components add as big-endian 16-bit numbers. The padding bits
at the end of a row stay as they are, and 8-bit data takes the same
path as before. The PNG predictors are unchanged.

Docs: docs/target-app-parity.md.
2026-10-19 00:03:43 +07:00
dotCooCoo d7e7066251 File formats: Targa (.tga) files open and place
Squashed from #2765 (its merge of main dropped).
2026-10-18 23:50:58 +07:00
dotCooCoo 4803573940 .ai import: opening a file with a variable library warns that its variables weren't read
A .ai can keep its variables and data sets in a variable library in
its editing data. VectorCraft doesn't read that library, and such a
file opened with an empty Variables panel and no warning about it.

The reader now counts the library's variables and data sets in the
editing data's /Document dictionary, and opening the file adds a
warning: "the file's variables and data sets weren't read (2
variables, 3 data sets): the Variables panel doesn't list them". The
same warning appears when the layers can't be read and the file opens
from its PDF part. A PDF that opens from the editing data it carries
gets the warning too. A library with no variables and no data sets
gives no warning.

Docs: docs/target-app-parity.md.
2026-10-18 12:38:06 +07:00
dotCooCoo 24d4777d1a Color: freeform gradients keep CMYK inks when expanded, meshed or edited
Object > Expand and Object > Create Gradient Mesh mixed a freeform
gradient's points in display RGB and turned the result back into the
points' model with the profile-free formula. CMYK inks changed: points
of C50 M20 Y0 K10 gave mesh points of C45 M24 Y0 K22. RGB points were
stored as display sRGB, so with a working RGB space other than sRGB
the mesh showed other colors. Points colored along CMYK stops, a point
paint.freeform.splitLine added between CMYK points and a point
paint.freeform.addPoint added without a color came out as RGB.

Points that share a color model now mix in it: the C, M, Y and K
values mix as inks, and Gray, RGB and Lab components mix as they are.
Points in different models still mix in display RGB. Object > Expand
and Object > Create Gradient Mesh store the result in the working RGB
space, and in a CMYK document they store it as CMYK through the color
settings, as paint.setFill stores an RGB color. Freeform::recolor and
split_line mix with the new Color::lerp_keep_model, which
vectorcraft_doc::blend::lerp_color now calls.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-18 12:36:25 +07:00
dotCooCoo 22ce516e2d Appearance: the inline effect editor shows 3D, Warp, Rasterize and channel choices as dropdowns
The Appearance panel's inline effect editor still showed eight choice
parameters as text fields holding the stored value: Revolve's edge,
Extrude & Bevel's bevel, every Warp style's orientation, Rasterize's
Color Model, Background and Anti-aliasing, and the channel of Levels
and Curves. Their dialogs built these lists in their own bodies, so
effect::choices did not hold them. Nothing checked a channel's value:
a typo was stored, and Levels or Curves drew it as RGB.

effect::own_choices now holds these lists, each label as its dialog
passes it to its dropdown or radio buttons. The dialogs read the lists
from it (Rasterize's Anti-aliasing from the AntiAlias list it is built
from), and the inline editor shows each one as a dropdown with the
dialog's translated labels; Rasterize's Color Model reads CMYK or RGB
by the document's mode. effect.apply and effect.setParams refuse a
Levels or Curves channel other than rgb, red, green or blue (r, g, b
and any letter case are read too) with "`channel` must be one of rgb,
red, green, blue", and the document and its undo history stay as they
were.

Docs: docs/mcp.md (effect params), docs/target-app-parity.md.
2026-10-18 12:34:36 +07:00
dotCooCoo 830cefcaf6 PDF import: an XMP packet nested more than 256 deep is skipped instead of overflowing the stack
When a PDF, PDF-compatible .ai or .ait opens, crates/pdf/src/file_info.rs
reads its File Info from the XMP packet with roxmltree, which parses
nested elements recursively and has no limit on their depth. A packet
with elements nested 10,000 deep (70 KB) overflowed a 1 MB stack, and
one nested 100,000 deep (700 KB) overflowed an 8 MB stack. A stack
overflow aborts the app.

File Info now counts how deep the packet's elements nest before roxmltree
parses it, and leaves out a packet nested more than 256 deep, as it
leaves out a packet that is not XML. The document information
dictionary then gives the File Info.
2026-10-18 12:15:31 +07:00
dotCooCoo 2ccb147403 Variables: Import Data Sets… defines the variables a file's header names
dataset.import refused every header cell that wasn't the name of a
variable the document defines. A data source file could only fill
variables made beforehand, and `#` and `@` in a header were read as part
of a variable's name.

A header cell that names no variable now defines a new, unbound
variable in the import's undo step. `#Shown` defines a visibility
variable `Shown`, `@Photo` defines an image variable `Photo`, and a
cell without a prefix defines a text variable. A prefixed cell whose
name is already a variable of the prefix's kind names it. A cell that
is a variable's exact name keeps naming it, even when the name starts
with `#`, `@` or `%`, so files that dataset.export writes import back
as before. The import fails, naming the column, on a prefixed cell
whose name is a variable of another kind and on a `%` cell that names
no variable (graph data files). While the variables are locked, a
header that would define a variable fails the import with "variables
are locked", as New Variable does. The reply's `defined` lists the new
variables.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-18 01:27:13 +07:00
dotCooCoo 1c4ead3703 Place: Word .docx files place as area type with their formatting
File > Place could not place a Word document: a .docx file was "not a
format VectorCraft reads". The reference app's File > Place reads
Microsoft Word files as text.

File > Place now reads .docx files, written from ECMA-376 Part 1
(WordprocessingML, Transitional and Strict), and sets their text as
area type the way a text or RTF file is placed. The package is read
through its relationships. The formatting comes from the document
defaults, the paragraph style (else the default paragraph style), the
character style (else the default character style), the styles each
is based on, and direct formatting. Bold, italic, caps, small caps,
strikethrough and hidden text are toggle properties: a style that
turns one on reverses the state the styles before it give.

Each run keeps its font (theme fonts from the theme part), size, bold
and italic, underline, strikethrough, all caps, small caps,
superscript or subscript (in Document Setup's proportions), raised or
lowered text and color. Each paragraph keeps its alignment, start,
end, first-line and hanging indents, space before and after, and
direction; the start indent of a right-to-left paragraph is its right
indent. Table cells are separated by tabs and each table row is a
paragraph. Text in insertions, hyperlinks, content controls and
fields' results is read. Hidden and deleted text, field instructions,
pictures, text boxes, headers, footers, notes and list numbers are
left out.

A ZIP archive named .docx, or one that holds word/document.xml, is
read as a Word document. fileio/zip.rs gains a reader for stored and
deflated files that checks each file's CRC. A package with more than
10,000 files, a part that unpacks to more than 16 MiB or nests
elements more than 256 deep, and a document with more than 4 MiB of
text or 100,000 paragraphs or runs are refused with an error. The
Place dialog lists Word files, and file.place, file.place.info and
file.place.queue report the format `docx`. .doc files are still not
read.

The Word document tests in tests_textimport.rs fail without the
change, and text_import_fuzz.rs places random and damaged packages.

Docs: docs/file-format-parity.md, docs/mcp.md,
docs/target-app-parity.md.
2026-10-18 01:21:31 +07:00
dotCooCoo dd466d79e4 TIFF: CCITT Group 3 compressed files open and place
1-bit TIFFs compressed with CCITT Group 3 did not open, place or
relink: the image decoder reads Group 4 only and refuses Compression 2
(modified Huffman) and Compression 3 (ITU-T T.4).

They are now decoded with hayro-ccitt, the CCITT decoder that EPS
import uses. Compression 2 rows start at a byte and have no EOL codes.
Compression 3 rows follow an EOL code and are one-dimensional, or, with
T4Options bit 0, one- or two-dimensional as each row's tag bit says.
Fill bits, FillOrder 2, either PhotometricInterpretation, strips and
tiles are read.

A file whose pixels would take more than the decoding allowance as
RGBA is refused as too large to draw. A strip or tile whose data ends
before its last row is refused as damaged, and so is a tile larger
than the allowance. The strips and tiles together are read from at
most as many bytes as the file holds.

Part of #1466.

Docs: docs/file-format-parity.md, docs/target-app-parity.md.
2026-10-18 01:21:30 +07:00
alpha-rock 039a467dcb M9: add multiple colored lights to 3D effects
Add opaque RGB light color and up to ten independently adjustable lights to
Revolve, Extrude & Bevel, Inflate and Rotate. Keep light selection, Add/Delete,
selected-light gizmos and preview transactions on the existing command path.

Preserve legacy white-light Inflate polygons, paint composition and seam joins,
including the 0.05 seam minimum. Use shared smooth normals for colored Inflate,
and batch solid-fill RGB channel paths behind one surface clip. Keep source
paths editable and SVG/PDF output vector artwork.

Rebase onto main f7de3d6a, retaining both sides of progress docs and all 22
catalogs. Shade freeform points once through the shared paint mapper. Document
release measurements and a reproducible CLI fixture, including the remaining
source-gradient cost and the one-light timing tradeoff.

Validation: full cargo xtask ci passed all 10 steps; 7,978 tests passed, zero
failed and 11 ignored. Four legacy white-light PNGs match unchanged main byte
for byte. Three paired release bench/perf repetitions completed without busy
warnings; all six standard perf runs passed every budget. Two-light solid
Inflate improved from 136.2 to 96.8 ms/frame at 1200x1200, default eight threads.
2026-10-18 01:11:39 +07:00
dotCooCoo 4712fb2c2f Character: variable fonts get a slider for each axis
VectorCraft listed a variable font's named instances as styles, but
type could not be set between or past them. The reference app's
Character panel has a Variable Font control that adjusts a variable
font's axes, such as Weight, Width and Slant.

Type now keeps Variable Font settings by axis tag
(CharStyle::font_axes, saved as fontAxes only when set). text.setFormat
and text.setRangeStyle take fontAxes: each value is clamped to its
axis's range, a tag the font has no axis for is left out, and null
goes back to the style's own setting. Another font or style clears
them, including one from a character style that does not set them. A
character style captured from type sets them, with {} for none.
text.fontList {family, style} lists a font's axes.

Layout, outlines, the substituted-glyph highlight and PDF export use
the font instanced at the settings, and neighboring runs at different
settings are shaped apart, as runs in different fonts are. SVG export
writes them as font-variation-settings, the wght setting as the weight
and the wdth setting as font-stretch, and embeds the font instanced
there; SVG import reads font-variation-settings back. Legacy .ai export
warns that the settings are left out.

The Character panel shows a Variable Font button for a variable font.
It opens a slider and a field for each axis the font does not hide,
labeled with the font's name for the axis, and screen readers get each
slider under that name. A drag is one undo step. Reset Panel leaves
the settings, as it leaves the font and style.

Missing Glyph Protection now keeps the settings and the font version
on the characters that keep their font; before, it kept only the
family and style. A character style applied or redefined with another
font now clears the old font's version, as text.setStyle does, unless
the style sets one. SVG export with embedded fonts warns when faces of
one weight, style and width share characters, which browsers then draw
in one of the faces.

Docs: docs/mcp.md, docs/target-app-parity.md, docs/type-parity.md.
2026-10-17 19:38:46 +07:00
dotCooCoo 3adf5141e7 Fix #2276: the Eyedropper and RGB input give color-managed CMYK values in a process document
In a CMYK document, an RGB color from the Eyedropper, a hex value or
paint.setFill was stored as CMYK by the profile-free formula
(k = 1 - max(r, g, b)), not through the color settings. RGB 83/170/223
came in as C63 M24 Y0 K13. A CMYK image was sampled through the RGB its
decoder shows, so its inks came back only when the formula happened to
round-trip them.

paint_from now converts an RGB color into a CMYK document with
Cms::convert and the settings' intent, as Document Color Mode and Swatch
Options already do. This covers paint.setFill and paint.setStroke,
gradient stops, swatch.new {color} and the Eyedropper. With Generic CMYK,
83/170/223 now separates to C59 M12 Y3 K0, and #ff0000 now separates to
C0 M97 Y100 K0 instead of C0 M100 Y100 K0. keepModel: true still keeps
RGB. paint.sampleColor with a stop stores the color the same way.

The Eyedropper samples a CMYK image's ink amounts (a four-component
JPEG, or an 8- or 16-bit chunky CMYK TIFF) and returns them unchanged
(ImageBlob::sample_cmyk_object), also in an RGB document. It takes an
RGB image's sRGB pixel into the working RGB space, as the screen color
sampler does, so the picked color shows as the pixel when the working
space is not sRGB.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-10-17 16:38:42 +07:00
vangel76andClaude Opus 5.5 e4ad256d7d Gradient mesh: Object > Create Gradient Mesh on an embedded image (#1676)
The mesh covers the image's rectangle with its transform kept; each point takes the average colour and opacity of the picture around it. A linked image is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 16:38:41 +07:00
vangel76andClaude Opus 5.5 c692962a22 Symbols: fuzz nineSlice params of symbol.new and symbol.options (#2342)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 16:38:41 +07:00
VictorandClaude Opus 5.5 276eeacae6 G7: View › Rulers › Change to Global / Artboard Rulers
View › Rulers › Change to Global Rulers (Cmd+Alt+R) was a disabled
stub: the rulers always counted from one document zero point. The
reference app's public help describes two modes: artboard rulers, the
default, count from the active artboard's top-left corner, and each
artboard can have its own zero point; global rulers keep one zero point
for the whole document whichever artboard is active.
https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/about-rulers.html
https://helpx.adobe.com/illustrator/desktop/measure-and-align/grids-and-guides/use-rulers.html

`view.globalRulers {global?}` switches the mode (toggle by default),
undoable and saved with the document (`rulers`). The menu item reads
Change to Artboard Rulers while global rulers are on. A new document has
artboard rulers; a file saved before the mode existed has no `rulers`
key and opens with global rulers, so it measures as it did.

With artboard rulers, dragging the ruler corner sets the active
artboard's zero point, kept as an offset from its corner
(`artboard_ruler_origins`, by artboard id) so it moves with the
artboard, and double-clicking the corner resets it. The global zero
point (`ruler_origin`) stays separate. `document.rulerOrigin`,
`document.setRulerOrigin` and `document.resetRulerOrigin` take
`artboard`; `Document::ruler_origin_for` is the zero point with an
artboard active, falling back to the corner or the global one when a
stored value is out of range.

The rulers, the Transform, Properties and Control bar X/Y, the Info
panel's X/Y (which ignored the zero point before), ruler-guide
readouts and Shift ruler-tick snapping all count from that zero point:
the view's active artboard reaches the tools through `ViewInfo` and
`ToolContext::ruler_origin`. Command coordinates stay absolute.

Tests: engine/tests/ruler_origin.rs (default mode, per-artboard origin
following its artboard, global origin apart, toggle and undo, save and
legacy files, out-of-range values), ui-egui/src/tests_ruler_origin.rs
(position fields and corner drag on the active artboard, menu label and
shortcut), tools/src/rulerguide.rs (guide readouts and tick snapping).
Tests written for the single global zero point now turn global rulers
on first. "Change to Artboard Rulers" is translated in every catalog.

Docs: docs/ui-parity.md, docs/gaps.md (G7), docs/target-app-parity.md,
docs/mcp.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U2JbUtM4MkzWLm9FgYiEsq
2026-10-17 16:38:41 +07:00
VictorandClaude Opus 5.5 d1a6e39b7e G2: Effect › Artistic › Cutout, Neon Glow and Poster Edges
Three original Artistic filters on the pixel pipeline, each with a
catalogue row, the generic live dialog, translations and tests.

- Cutout: colours softened by Edge Simplicity (less with Edge Fidelity),
  split into Number of Levels steps per channel and painted as flat
  pieces in their average colour; slivers along softened edges take the
  nearest large piece's colour.
- Poster Edges: channels posterized to Posterization + 2 levels, black
  lines on the edges of any channel and of the outline, widened by Edge
  Thickness and darkened by Edge Intensity.
- Neon Glow: dimmed greys screened with a glow colour from the light
  areas (positive Glow Size) or the dark ones (negative), as strong as
  Glow Brightness.

All keep coverage and work in document units at any Document Raster
Effects resolution. Photoshop filters 33/57; cargo xtask ci passes all
9 steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 16:17:54 +07:00