Found while testing crates/engine/src/cmd/props.rs:361 (strokeWeight is only clamped with .max(0.0)); the panic is raised when the renderer draws the stroke (render crate / tiny-skia stroker)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Figma puts copied layers in the clipboard's HTML as a base64 .fig canvas
between (figma) markers. edit.pasteFromFigma reads it with the .fig import,
turns the layers into the session clipboard and pastes them as edit.paste
does (selected frame, in place or view centre), with the import's warnings.
The desktop app reads the clipboard's HTML (arboard) and Paste prefers a
Figma copy; the browser's paste event carries plain text only.
Tested on copies written by our own .fig export; not yet on real copies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Override rows of an instance can name a layer of its main by the key the layer
had before the main was edited. The key now resolves to the layer's current id
when it is unambiguous and the current id does not exist already. Rows of an
enclosing instance win over those of the instances nested in it, whatever order
the wire lists them in, and an explicit "no style" on an override detaches the
style instead of falling back to the older field.
Paints bound to variables take the colours of the active modes when a document
loads, before the first edit, without touching other pending changes.
A stroke-only vector that imports as the outline of its stroke now moves the
stroke's shared style and colour variables to the fill it becomes, and vectors
whose single segments carry their own stroke style say so in a warning.
Save as now offers .figcraft and .fig as separate file types. A .fig
destination, chosen in the dialog or passed to file.saveAs and file.save,
writes the existing experimental .fig export of the document that is shown
and reports how much was left out. The file's own path, unsaved changes,
versions, branches and undo history are not touched, and a shown earlier
version stays shown while the copy is written or a Save as choice is
pending.
Plain Save no longer writes over a file whose saved path ends in .fig;
earlier builds could write native data under that name. It asks for a
destination instead.
Pasting or dropping a file runs file.import with the whole file as Base64
data, and every successful command was journaled with its parameters, so
each imported file stayed in memory a second time for the session. Byte
imports, including ones wrapped in command.batch, are no longer journaled.
Imports by path and all other commands are unchanged.
page.duplicate copies every layer with duplicate_all_into (navigation between
copied frames points at the copies) and builds the old-to-new list for flows
from that copy, instead of copying each layer separately.
Main's case-insensitive replace loop (from an earlier PR) took the place of the
loop this change edited, so the replacement text is normalised once, where
the parameter is read.
The outer batch passes a command.batch step on as written, so the inner
steps' `$step` references point at the inner batch's steps. The docs say
that `$step` is a reserved key in step params.
Inside command.batch, `{"$step": n}` anywhere in a step's params is
replaced by the `id` in step n's result before the step runs
(`{"$step": n, "field": "..."}` picks another field). A nested tree,
such as a frame with children, now takes one batch and one undo step,
not one round-trip per level with ids copied between calls by hand.
Malformed references (forward or self, negative or fractional index,
missing or non-string field, extra keys) fail the batch with BadParams
naming the step, and the existing group machinery rolls it back. The
params walk is depth-capped. Steps are journalled with resolved params.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
page.add gave every added page the interface theme's colour. Figma's
documentation says added pages inherit the current page's background;
the theme only decides a new file's page. Read the active page's colour
before creating the new page, in the command, so the app, CLI, control
channel and MCP agree.
Refs #295
Preferences > Save files automatically (pref.autoSave, off by default;
hidden in the browser, which has no files to save to). With it on, each
open file that has a path is saved 3 s after it changes, never during a
gesture, a live text preview or while a version is shown. A failed save
says why in a toast and tries again a minute later.
Version history checkpoints (version.autosave, every 30 minutes) only ran
for files with unsaved changes, so a file that saves itself would stop
getting them; with auto-save on, edits made since the file was opened
count.
Closes#296
- A copy Alt-dragged off a layer snaps to that layer too: its edges,
centres and equal spacing (Targets::add_box with the originals' box).
Before, the originals were left out of the snap targets, so a copy
couldn't line up with or space evenly from the layer it came from.
- Duplicate (Cmd+D) repeats the last duplicate's page offset while its
copies are what it duplicates: Alt-drag a copy, or Duplicate and move
the copy, then each Cmd+D steps out the next one as far again. The
step is measured when Duplicate runs. It lasts as long as the
selection (undo reuses ids), copies placed by an auto layout set no
step, and explicit dx/dy win.
- measure_tests: an Alt-dragged copy between its original and another
layer now snaps to equal spacing (45/45 instead of 50/40).
Tests: duplicate_spacing_tests (snap to the original, equal spacing,
repeat after moving a copy and after an Alt-drag, only for the last
copies, undo then a new layer, rotated frame, auto layout).
Refs #256
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A command that fails part-way (node.set applies geometry keys before
values like visible, which can fail validation) mutated the document
even when an interaction was under way, because the rollback in
Session::execute was gated on interaction.is_none(); drop the gate so
the command-entry snapshot is restored during interactions as well,
which is the interaction's last valid preview and leaves the gesture
itself intact.
The Pen showed only its anchors while a path was drawn; handles appeared
only in vector edit mode afterwards. Now:
- The point being dragged shows its in and out handles, and the point
before it its out handle (the handles shaping the segments at the pen),
drawn like vector edit mode's (one shared canvas helper).
- Moving on to the next point, the rubber band is the segment a click
would make: it leaves the last anchor along its out handle instead of
going straight.
- A press no longer leaves the previous straight rubber band on screen
over the segment being dragged.
Overlay gains pen_handles and rubber_handle (shown in ui.inspect).
Tests: pen_overlay_tests (handles during and after a drag, none and a
straight band for corner points, cleared on finish); canvas_fixes_tests
(handle lines and dots painted mid-drag, curved band after release).
Refs #256
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`{"type":"linearGradient","stops":["#hex",…]}` was only expanded by the MCP
tools, so `figcraft-cli run` and the control channel's `engine.execute`
refused it ("`fills` must be paints or colours"). The expansion now lives in
the engine (`params::normalize_paint`, used by `paint_of`), so node.set,
node.create props, fill.add/set, strokes and paint styles all take it; MCP
calls the engine's copy instead of its own.
Tests: engine (every paint command, MCP-equal defaults, junk still refused),
control channel `engine.execute`, and `figcraft-cli run` end to end.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Recents section shows recent files as cards: the picture each file
carries (thumbnail.png in .figcraft and .fig, an image file itself) fitted
on its first page's colour, a divider, and a footer with the file's icon,
name and "Edited ... ago" (or "File not found"); the full path on hover.
Pictures are read off the frame's thread, only for cards in view, cached
by path and modification time, capped in bytes and pixels; missing,
damaged or oversized ones give a placeholder. They fade in, and cards
show a skeleton until their file has been looked at.
A sort menu (Alphabetical, Date created, Last modified; either order) and
a grid/list toggle sit above them. The list is a Name, Last modified and
Created table whose headers sort. View (per section) and sort are kept in
the preferences and set with ui.set {homeRecentsView, homeRecentsSort,
homeRecentsOrder}. A click selects, a double-click or Enter opens, arrow
keys move the selection; a right-click offers Open, Show in folder and
Remove from recents (file.openRecent {remove}).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
A `.figcraft` image member that is damaged, too large or missing was skipped
without a word, and saving the file again dropped it for good. The load now
lists each lost image (up to 20, then a count) with its hash and the reason,
file.open puts it in `repairs` and sets a session message, and the app shows
that as a toast.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
* Agent Host changes for agents/fix-command-batch-history-restore
* Implement centralized transactional fix for command batch failures - #1
* The batch rollback tests compile, are formatted and expect the tab that was in front
The new module used a private helper of plugins_tests and an unused import; the plugin test
asserted tab 0 was in front, but it opens a second file first, which is the one in front
when the plugin starts and the one the rollback puts back.
---------
Co-authored-by: bflatastic <bombayapirat@gmail.com>
file.revert is refused while the file is in a live session (hosting, joined, publishing or reconnecting): reverting replaced the document's identity and broke its session. The refusal changes nothing, and reverting works again after leaving. Fixes#198.
SVG and PDF exports check the visible subtree's depth before bounds or drawing and refuse, with an actionable message, a document nested past 256 levels instead of silently leaving the deep layers out; nothing partial is written, and an inner layer can still be exported. The CLI and MCP report the same error. Fixes#208.
Also: on Windows, figcraft and figcraft-cli link with an 8 MB main-thread stack (Windows gives 1 MB where Linux and macOS give 8), so an export at the depth limit no longer overflows it.
Co-authored-by: bflatastic <bflatastic@users.noreply.github.com>
edit.repeat no longer records itself as the last command, so repeating keeps applying the command it repeats instead of recursing, and it refuses when there is nothing to repeat. Fixes#181.