Found while testing crates/color/src/palette_io.rs:373
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/tools/src/xform/measure.rs:20 (shown at :66, reported at :80)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/doc/src/xml_id.rs:80 (strip_unique), triggered by from_object_name at :32
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Adds .coderabbit.yaml so CodeRabbit no longer reviews or comments on PRs opened by dependabot[bot].
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
One pattern fill tiles at most 250,000 times (PatternDef::offsets_covering),
but Scene::fill writes each tile's own pattern fills again, down to the
nesting limit of 8, with nothing bounding the total. A pattern whose tile
art is filled with another pattern (or with itself) multiplies the tile
count at every level: a small document makes EMF export (the engine's
"emf" format, also reachable over MCP and the control channel) run out
of memory or never finish, and the unsaved document is lost with it.
The scene now keeps one tile budget for the whole walk. A fill whose
tiles don't fit in what is left, and every pattern fill after it, is
written as one colour like WMF writes patterns, with a warning.
The test tiles squares filled with dots (over half a million fills before)
and a pattern filled with itself, and checks both stay under the budget.
Reported-by: BS.ehlas.cz
One Repeat makes at most 250,000 instances (a 500 x 500 grid; radial
counts stop at 1,000), but its source may hold another Repeat and every
instance draws that one again: expand() clones the inner Repeat once per
instance, and the renderer, the exporters (expand_deep) and hit testing
evaluate each copy. Nothing bounds the product, so a document under 1 KB
with a 500 x 500 grid of a 500 x 500 grid asks for 62.5 billion
instances: opening it freezes the app or runs it out of memory, and the
unsaved work in other documents is lost with it.
A Repeat now counts the instances the Repeats nested in its source make
in all and keeps only as many of its own as fit in 250,000, the budget
pattern fills already keep (at least one, so the source still shows).
A Repeat with nothing nested keeps all its instances, as before.
The test nests a 500 x 500 grid in a 500 x 500 grid and in a mirror,
counts the instances from the transforms (62.5 billion before) and
checks they stay within the budget, while one grid and a small nest keep
all of theirs.
Reported-by: BS.ehlas.cz
GradientMesh::remove_point_lines (the Mesh tool's Alt-click,
object.mesh.deletePoint) only checked that the clicked point exists,
then indexed the points above and below or left and right of it by the
mesh's rows and columns. A gradient mesh read from a document is not
checked against its rows and columns, so one with fewer points than
they need (rows 2, columns 1 and 3 points instead of 6) indexes past
the end and panics: the engine guard rolls the command back with an
internal error, and the web app, which has no guard, aborts.
It now returns false for a mesh that isn't valid, as locate (and with
it adding mesh lines), the outline and the mesh lines already do, and
the command reports that nothing could be deleted.
The test Alt-clicks an interior row's point and an interior column's
point of two such meshes and checks nothing is deleted and the mesh is
unchanged.
Reported-by: BS.ehlas.cz
GradientMesh::is_valid and the two mesh envelope checks (envelope_grid
and the envelope's surface) compare the number of points with
(rows + 1) × (cols + 1), multiplying the document's u32 rows and
columns unchecked. With u32::MAX rows and columns the product is 2^64:
debug builds panic on the overflow and release builds wrap it to 0, so
a mesh or a mesh envelope without points passes as valid. Selecting
it, Outline view, hit testing and export then index points that aren't
there (the mesh lines, outline and edges), tessellating it asks for an
impossible capacity, and a mesh envelope with handles to fill in loops
2^64 times making them until memory runs out. On wasm32, where usize
has 32 bits, 65,535 rows and columns are enough, and a panic there
aborts the web app.
The three checks now share grid_len, which counts the points with
checked arithmetic and gives None when the count doesn't fit in a
usize: a grid that can't be counted matches no point list, so it is
malformed like any other wrong count.
The test checks that a u32::MAX × u32::MAX mesh without points isn't
valid and has no lines, outline or quads, that envelope_grid refuses
those counts, and that a mesh envelope with them leaves art in place.
Reported-by: BS.ehlas.cz
A live-effect plug-in was given only its object's geometry. An effect
that draws as wide as the stroke it sits on, or tells its fills apart,
had to ask for them again as parameters, which then went stale when
the paint changed.
A live effect's input object now holds the object's fills and strokes
too, read only and in the form a filter gets them (one encoder,
objects::paint_lists, for both), and _context.item says where the
effect sits: "object", or {"fill": i} / {"stroke": i} for an effect in
a fill's or a stroke's own effects. effects::GeomContext carries the
object's appearance and the item, and the plug-in cache keys on the
paints, so a change of paint runs the plug-in again. Hosts from before
this give geometry only; nothing in the output changes.
Expand worked a fill's geometry effects out in the object's context, so
a plug-in effect there would be told "object" and a gradient fill was
expanded into a shape the canvas doesn't draw: fill_shape now gives them
the fill's context, as drawing does (expand.rs).
Siblings named with a declared prefix, the other half of such context,
are left out: the effected geometry is worked out from the object alone
wherever it is drawn, measured, hit-tested or expanded.
wat::when_given is a plug-in whose output depends on whether its input
or parameters hold a text. tests_plugins.rs uses it to check that an
effect sees the stroke's width and the fill, runs again for a thinner
stroke, and on a second stroke is told so; effect.rs checks the item
counting; a gradient fill under such an effect is drawn and expanded
into the same triangle. CRAFT_PLUGIN_PAINTS_WASM=<folder> saves a test
effect to try in the app.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhvnnnD5fTFupK8WWq35un
A plug-in's dialog labelled each field with its parameter's name spaced
out ("Line_spacing_mm:"), showed numbers without their unit and listed
at most 64 parameters in one column. A plug-in with many settings, such
as a generator's or an exporter's, could neither explain nor sort them,
and its dialog showed settings that don't apply to the choices made.
Each parameter may now say how dialogs show it, in keys that hosts
from before them ignore (plugins::dialog):
- label; unit, after the value ("0.25 mm"); help, on the label; and
helpUrl, an https:// page opened from a button after the field.
- group: the parameters outside any group come first, then each group
as a section that folds, in the order the groups first appear, the
fields lined up down the dialog.
- visibleWhen: {"pattern": ["lines"]} shows a parameter only
while each checkbox or choice it names, declared before it, has one
of the values listed and is shown itself. Manifest::visible decides
every parameter in one pass, in declaration order. A hidden parameter
still goes to the plug-in with its value, so a run never depends on
what the dialog showed.
Two types join number, int, bool and choice: string, a text field cut
to maxLength, and numberList, numbers typed separated by commas or
spaces, each clamped, minItems to maxItems of them. A plug-in declares
up to 256 parameters, at most 64 outside groups. The filter, live-effect
and exporter dialogs use every key. The Appearance panel's editor of a
plug-in effect uses the labels, units, help and conditions, and reads a
typed number list as the dialog does.
dialog.rs tests parsing, the round trip of a manifest written out,
sections, visibility (also of 256 overlapping conditions, which a
recursive check would take forever on) and the keys refused;
manifest.rs the new types and limits. tests_plugin_fields.rs draws a
dialog with kittest: labels and units, sections in order with their
fields lined up, visibleWhen hiding and showing rows, and the help
button opening its page.
appearance.rs checks the editor's labels, units, conditions and a
typed number list, and form.rs units after numbers (a unit starting
with a digit is spaced from it) and typed lists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhvnnnD5fTFupK8WWq35un
# Conflicts:
# docs/gaps.md
# docs/target-app-parity.md
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
With Include SVG Code on, Copy offered the SVG markup as plain text with every placed image
embedded as base64: a 13 MB photo made 17 MB of text. A paste reads the text first (eframe on
every paste key; on Linux and macOS it is the one format written, and holds_ours reads it again),
so each paste moved it through the system clipboard on the UI thread, and on GNOME the app
stopped responding.
The SVG markup now goes out as text only while the images it embeds total at most 1 MiB; above
that it is offered as image/svg+xml and PNG. Type keeps its text, and vector art keeps its SVG
text at any size (only embedded image bytes count).
Windows 11, Ryzen 5 2600X, 13 MB PNG, real Ctrl+C: the longest UI stall fell from 823-1021 ms
to 133 ms. Not run on Linux.
Match the Selection tool: Shift on an already selected hit removes it on
release without a drag, including one path of a compound path.
Co-authored-by: Cursor <cursoragent@cursor.com>
Export Selection… and the Asset Export panel open Export for Screens on the
Assets tab with the new assets checked, but new assets land at the end of
the list, so in a document with many assets the checked one sat below the
list's visible part and had to be scrolled to by hand. The list now scrolls
to the first checked asset when the dialog opens on them. It keeps asking
until the row is in view (the dialog's first frame only measures it), then
stops, so the list scrolls freely afterwards.