* M6: a pasted image the clipboard doesn't carry shows as missing and is named (#1646)
The placeholder an import stores for an image it lacks is now striped, not flat grey; the fill keeps the image's name; .fig and paste give a fixed, translated note per image; edit.pasteDesignLayers returns missingImages, the paste's message names those layers, and a paste with notes opens the import report.
* M6: the missing-image placeholder's stripes stay clean when scaled, and a .fig gives one note (#1646)
With map: true, edit.duplicate also returns map: each original layer's id, descendants included (nested frames, groups, instances and their sublayers), paired with its copy's id, so a batch can edit inside a copy through a step reference instead of inspecting it. Without map the result is unchanged.
`run_command {"command": "inspect.code", "args": {...}}` ran the command
without its params, on the selection. MCP tool arguments are now checked
against each tool's input schema (and the objects it describes inside,
such as a batch's steps; open objects like `params` take anything), and
a step key other than `command`, `params` and `as` is refused in
command.batch, figcraft-cli scripts and the MCP batch tool.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Before commands can refuse names their docs don't list (#1413), the docs
have to list what the code reads. These are the top-level names the run
code (and every helper it hands the params to) reads that its doc left
out, all checked against the code; nothing changes behaviour:
- creation: node.create (characters, points, closed), frame.create,
shape.*, text.create and vector.create (index, name, select),
image.place (name)
- aliases the code already took: comment.* (id | link), annotation.add
and annotation.edit (label | text), text.set (characters | text),
text.listStyle (style | list), fill.add and stroke.add (paint | color),
proto.interaction.add/set (durationMs | duration_ms, delayMs | delay_ms)
- options: file.export and export.update (contentsOnly,
includeBoundingBox, svgOutlineText, svgSimplifyStroke), comment.next
(frame, resolved), layout.suggest (direction), variable.create
(description), vector.edit.widthPoint (side), collab.share (key),
collab.manageFile (access, label, link, name), fill.pattern (visible,
blend)
- docs whose shape hid a name: proto.interaction.add (a stray `}` closed
the object before delayMs; dest was only in a trailer),
proto.interaction.set (patch fields in a second object), collab.join
(a top-level comma inside the link examples), pref.uiScale (commas in
the value's description)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
node.get, the control channel's inspect methods and the MCP get_node / inspect_document tools
take fields: [node keys], applied at every level, or outline: true (id, name, type, characters
cut to 120 characters, mainComponentName, visible). Ids and structure are always kept, properties
that aren't selected aren't computed, and the default output is unchanged. Part of #996.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
Lowercasing a name ending in Σ gives ς, while a query Σ on its own lowercases to σ, so the
substring check missed it (CodeRabbit). Both sides now fold ς to σ.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
command.batch and the MCP batch tool take onError: "abort" (the default, unchanged) or
"continue". In continue mode each step runs inside a rollback-only checkpoint split out of
Session::grouped: a failed step is rolled back on its own, later steps still run, and the result
is one {step, command, ok, result|error} object per step. What was applied is one undo step; a
reference to a failed step fails too; malformed batches are still rejected before any step runs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
node.find {root?, name?, type?, text?, component?, limit?} returns {ids, total} for the strict
descendants of root (or the current page) that match every filter given: name, text and main
component name as case-insensitive substrings, type as an exact type name. It changes neither the
selection, the page nor the undo history. Part of #996; fields/outline are left for later.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
* E21: require release font coverage and stabilize capture deadline tests
* E21: separate capture, IME and field-save regression phases
* M0: correct the migration qualification reference
* M-egui037: document input lifecycle and UI regression contracts
* M0: correct egui 0.37 status docs and stabilize join-code regression
---------
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
color_alpha is the part of a solid paint's opacity that is its typed colour's alpha, not a flag:
binding a colour variable takes only that part out, unbinding puts the variable's alpha back in,
and REST import records it. A 50% paint bound to a 50% variable stays at 25% bound or not.
Board files (File > New board) with sticky notes, shapes with text, connectors that follow what they join (straight or elbow, arrowheads, labels), marker, highlighter and eraser, and stamps; a board toolbar and a contextual bar; the Design panel hidden on boards, Prototype and Dev Mode refused there. Boards are ordinary FigCraft files built from ordinary layers, so saving, undo, export, MCP and live sessions work unchanged. File tabs, recent files and Open files show a pen nib for design files and a note for boards (original icons drawn in code).
node.set took no `path`, so redrawing a vector meant deleting and recreating it and
losing its id, place in the layer list, paints, styles, bindings and step references.
`path` now takes SVG path data for vector layers and replaces the geometry through
the same reader node.create uses (path_network, shared with create). width/height in
the same call scale it as create does. The path's own box goes at the layer's current
origin, so rotation and flips stay; with x/y in the same call it goes at x/y plus the
box's offset, which is where create puts it.
On several layers, layers that aren't vectors are skipped with a warning, as auto
layout keys already are (the skip logic is shared now). A single non-vector is
refused, and so is a vector inside an instance: there is no geometry override, so
the next sync would undo it. Data that doesn't parse, draws nothing or has
non-finite bounds is refused cleanly; create now refuses non-finite bounds too.
Tests cover keeping the layer, placement and scaling against create, rotation,
undo/redo, invalid data, non-vectors, instances, boolean operands, edit mode, and a
junk-path sweep. The engine-wide junk sweep has node.set path entries.
Fixes#1001
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A batch step can name itself with "as" and later steps refer to it with
{"$ref": "name"}, alongside positional {"$step": n}. "field" now takes a
slash path into the result as well as a key, so {"$ref": "copy",
"field": "ids/0"} reaches the first id edit.duplicate returned (the
convention variable.bind uses for fills/0/color; a leading "/" as in
JSON Pointer also works, and a key holding "/" still matches first).
Resolution moves to crates/engine/src/batch.rs (BatchStep, BatchScope),
shared by command.batch, the CLI script runner, the control channel and
the MCP batch tool, which now passes "as" through. Duplicate names fail
before any step runs; unknown, same-step or later names, bad paths and
out-of-range indexes fail the batch with the step, name and full path.
$ref is reserved like $step; nested batches keep their own names.
Fixes#994.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`node.reparent {ids, parent, index}` read `index` against the parent's
children as they were before the move, so within the same parent moving a
layer up one place did nothing and asking for two places moved it one.
`index` now counts the parent's children after the move, the way
`node.reorder` already does, in the same parent or a new one; several
layers still land together in their stacking order.
The layers panel was the only caller passing an index other than 0. It
drops on a gap between rows counted before the move, so it now subtracts
the dragged layers below that gap and drags behave as before.
Documents `index` in the command's params and in docs/mcp.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Duplicate and Alt-drag copied a main component as a second main. Within
its file the copy is now an instance of it (components::copy_in_file,
shared by edit.duplicate and the Alt-drag copy), as Copy then Paste
already was. A variant duplicated in its set stays a new variant.
An explicit edit of a bound width, height, opacity, gap, padding, corner
radius, stroke weight, arc or text field reported ok, but normalize wrote
the variable's value straight back. Now the edit sticks and that field's
binding is dropped, as paint edits already do.
- `styles::bound_before` / `detach_edited`: compare each layer-level
bound field's stored value before and after the edit (`bound_field`),
so keys written unchanged keep their bindings and fields moved along
with another (proportions, min/max) lose theirs. A binding the edit
itself made (`boundVariables`) stays. List and component property
bindings are left to their own rules.
- Hooked into node.set, transform.resize, object.opacityKeys, text.style
and `Session::preview` (canvas drags of the selected layers).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
node.set refuses unknown properties, but the effects inside it were read
by serde, which drops keys it doesn't know. A drop shadow given
offsetX/offsetY (the names variable bindings use for its offset) was
stored with the default offset [0, 4] and the call said ok. effect.add
and effect.set dropped them the same way.
The three now refuse what the effect doesn't read and say what it
takes: "effects[0]: unknown properties: offsetX, offsetY (a dropShadow
takes blend, color, offset, radius, showBehind, spread, type, visible)".
A key counts as read when setting it to null makes the effect fail to
parse or come out different, so serde's aliases count as read without a
list of them here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ExportFormat::VectorDrawable ("XML", .xml) and ExportSetting.vd_compat
(bestEffort / exact / ignore, stored only when not the default) wire the
figcraft-vectordrawable writer into file.export, file.exportLayers and
export.add/update: a vdCompat param (junk refused), svgIdAttribute writes
android:name, and the results carry the writer's warnings (per file for
export layers). The Export section lists XML with an "Android Vector
Drawable" tooltip and its id attribute and compatibility options, File >
Export... gets an XML preset, and the toasts say when content was changed
or left out. CLI: --format xml / .xml outputs, --vd-compat, warnings as
note: lines. MCP: format xml returns the XML as text plus its warnings.
A .fig export writes XML settings as SVG with a note.
Docs: file-format-parity (row and revision), ui-parity and roadmap
revisions, architecture (the crate), cli and mcp.
fill.set, stroke.set and node.set fill/fills/stroke/strokes reported ok
while normalize put the bound colour straight back, and node.set
{fills: []} brought the bound paint back. They now drop a paint's
colour, opacity or stop binding where the edit changed that value, as
selection.setPaint already did (its comparison moves to
lists::rebind_edited_paint, which also lets stop bindings follow a
removed stop). boundVariables applies last in props::apply, so a
binding given with the paints in one call stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>