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>
usvg unpacks SVGZ with no limit and parses SVGs embedded in `<image>` data
URIs with no shared depth or byte budget, and the nesting check ran on the
compressed bytes. FigCraft now unpacks SVGZ itself within 64 MiB, checks the
real document, and resolves embedded SVGs with its own data resolver: one
32 MiB budget of unpacked bytes and at most 4 levels, an embedded document
past a budget is left out. Rasters still resolve; linked files stay unread.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
* feat(dev-mode): Leptos code for a layer tree (#223)
inspect.code takes language: leptos, and the Inspect panel lists it: the
HTML markup inside a #[component] fn's view!, with the stylesheet after it
as for React. Text is Rust string literals (line breaks as <br/>) so the
macro parses it; px or rem like the other web languages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(dev-mode): Leptos components take their properties, instances are calls (#223)
Leptos joins React in component code: a component or set is a #[component]
fn with #[prop] defaults (variant enums with as_str, bool, String, ViewFn
for instance swaps, Option<ViewFn> for slots), layers bound to booleans
show through .then, variants differ by data-* CSS rules, and instances
(also inside layer trees) are calls passing the values they change. Names
are snake_case, Rust keywords get a trailing _. Code Connect takes leptos
too (use <path>::<Name>; view! markup). Markup now takes the language
instead of jsx/leptos flags; React and Leptos share the stylesheet pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Justin John <babyjjbabyjj549@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
View > Full screen (`view.fullscreen {on?}`, F11 on Windows and Linux,
Ctrl+Cmd+F on macOS) makes the window fill the screen, taskbar included,
and leaves it again. FigCraft's own menu item, translated in every catalog.
Tests: the key and the command toggle the window, junk parameters are
ignored; the shortcut table pins the new binding (49 pinned).
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
add_space(16) only indented the first row of a horizontal_wrapped, so with
many colours on a page the later rows started at the picker's edge and ran
past its right side. The swatch rows now sit in a frame inset 16 points on
both sides.
Co-authored-by: Justin John <babyjjbabyjj549@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Snapshots hold more than the document (recent batches, deletions), so near
the cap compaction could write one over max_file_bytes, which both readers
then refused: the file vanished on restart. Reading now allows twice the
cap, and compaction refuses a snapshot beyond that, keeping the old
snapshot and log.
Co-authored-by: Justin John <babyjjbabyjj549@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
state.json holds online owner tokens. The temporary file is now created
0600 (and narrowed if a crash left one behind) before anything is written,
and the rename carries that mode to state.json, so an older
world-readable file is replaced on the next save.
Co-authored-by: Justin John <babyjjbabyjj549@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
validate() now reports children listed twice, pages with a parent, and
nodes not reachable from a page (cycles, detached subtrees). repair()
drops repeated children, clears page parents and removes every node not
reachable from a page in one linear pass, replacing the orphan loop.
Co-authored-by: Justin John <babyjjbabyjj549@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Docs: roadmap entries for the community fixes landed in #211, #213, #214, #216 and #218
Their ROADMAP.md lines were left out when the five PRs were rebased (all five edited the
same header line); the wording is the contributors' own.
* Docs: the header lists all five community fixes
The AppImage ships figcraft-cli in usr/bin, but AppRun only ever launched the
app, so the CLI (and its MCP server) could not be reached without extracting
the image. `FigCraft.AppImage cli mcp` now execs `figcraft-cli mcp`, skipping
desktop integration because the CLI is headless.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Boolean operands restarted subtree_outline at depth 0, so booleans nested
in booleans bypassed the 64-level limit and could overflow the stack.
Co-authored-by: Justin John <babyjjbabyjj549@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.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>
crates/text/build.rs asked cargo to watch both craft-fonts manifests
whether or not they existed. Cargo takes a watched path that doesn't
exist as changed, so figcraft-text and every crate above it were rebuilt
on every build: with no craft-fonts checkout, and also with a checkout
of craft-fonts' main branch, which has no fonts/latin-manifest.txt.
Watch a manifest only when it exists, and its folder while it is
missing, so a manifest added later is still noticed. With no checkout
at all there is nothing to watch; the build warning and the README now
say how to pick one up after a first build.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
On Windows, eframe offered wgpu every backend and wgpu ranked Vulkan
first. On a laptop with two AMD GPUs whose drivers are years apart, the
Vulkan driver faults (0xc0000005 inside amdvlk64.dll) right after the
first frame, so the app died as its window opened. An access violation
in a driver thread is not something the app can catch.
Offer wgpu Direct3D 12 on Windows, with OpenGL behind it for hardware
without a Direct3D 12 driver. WGPU_BACKEND still names the backend on
every system, and other systems keep eframe's defaults.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Every plugin import now counts as a host call (reply_len, reply_read and set_output were free), and bytes copied out of or into the plugin's memory spend fuel, reserved before the copy or allocation, including when call overhead is set to zero. Fixes#192.
The log byte budget test from #216 runs with the default fuel, since copying a megabyte of log bytes is itself charged now.
With a layer's proportions locked, setting only its width or only its height through node.set (the W and H fields) scales the other side by the layer's own ratio; setting both in one edit leaves them as typed, an unlinked layer is unchanged, and a zero side has no ratio to keep. Fixes#220.
Plugin log lines are admitted before guest memory is copied: a line over 8 KiB, more than 1,000 lines or more than 1 MiB of log bytes in a run fails the run with a limit error, and the same budgets bound what is forwarded to the host logger. Fixes#191.
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.
The attached MCP backend no longer re-sends a request when its reply is lost: the app may already have applied it, so the error says the operation may have completed and to inspect the document before retrying; the next call reconnects, and a failed reconnect says the request was not sent. Fixes#194.
Also: a connection the app already closed (it drops ones idle past its socket timeout) is noticed before the next request is written and replaced then, so a pause between calls is not an error.
Co-authored-by: bflatastic <bflatastic@users.noreply.github.com>
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>
MCP and control-channel lines are bounded while they are read (32 MiB; an oversized frame gets an error and the stream closes without being drained). The loopback control listener caps connections at 8, applies 60 s socket read and write timeouts, caps both request queues at 8 and discards requests whose client stopped waiting. Fixes#201.
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.
The sample budget is enforced while a path is flattened, not after: the element count is checked up front, each curve is flattened on its own, and flattening stops as soon as the budget is exceeded. Fixes#204.
The sessions test waited for a new layer to arrive and then asserted on its name, which the host sets in a second batch; it now waits for the name. Test only.
A nested instance opened from a .fig or file JSON document is first linked to its
own component; matching it to the outer main's layer then went by that component's
name instead of the layer's, so nested instances of variants (or under a name of
their own) and their layers stayed unlinked, and a second export lost what they
changed. They go by their own name until they mirror a layer.
A variable width is written only as the same reach on both sides of the path (a
point with unequal sides drew nothing elsewhere). A stroke wider on one side, on a
shape that shows nothing else, is written as a vector filled with the area it
paints, with the stroke itself kept as plugin data that the import puts back.
Bound layer and text fields also name the number of the field they set. Docs: text,
effect and grid styles and symmetric variable widths are checked; non-colour
bindings come across as values.
Variable-width points say each side's reach as a share of the stroke weight (in
pixels they drew far too wide elsewhere). Progress docs: instance sizes, variables
with modes and shared styles in the .fig export, and what was checked by opening
written files in other software.
Instances write what they change from their main as an override addressed by the
main's id (size, name, paints), what follows from that for their layers as derived
data (size, place, outline, colours their variables give), and a scale factor when
they were scaled as a whole; layers of a nested instance swapped for another
component keep their overrides. Variable collections, modes and variables are nodes
on a hidden page; paints, gradient stops, effects, layout grids and layer fields
name the variables bound to them, and frames and pages the modes they set. Shared
styles are nodes there too, linked from the layers that use them. Variable-width
strokes say their reach on each side of the path. The import reads all of it back
(style definitions and unused styles, variable order and scopes, bindings on paint
lists, stops, effects and grids, page modes, text bindings, width profiles).
FIGCRAFT_DEFAULT_SERVER, set when compiling, names another server or, set to nothing, ships a
build with no default: online sharing then stays off until the user enters a server in
Preferences. Normal builds and the release workflow are unchanged.
The recovery copy of one of your online files is no longer discarded when
the file reopens from its server. It is kept until the outcome is known:
- the server has everything in it: the copy goes;
- the copy has changes that never reached the server and nothing else
changed there: they are applied on top as one edit and sent to everyone;
- anything else (others changed the file, the server forgot the client):
the copy opens beside the file as "<name> (recovered copy)", with a toast;
- the server can't be reached: the copy opens as an unsaved document.
The note beside a copy is written after it and records who the app was in
the file and the server's last change it had. The app joins again as that
client (the first hello resumes; a new sync client numbers its batches
after the ones the server processed), so the layers it made are its own to
send. The old copy is removed only once this run has copied what it became.
collab.recoverFile; a tooltip on the tab mark; docs.
A file put on a server without the server's key stays its owner's after its
tab closes or the app quits. The app records it with the preferences (server,
id, name, owner token, the owner's links, what others join with; at most 200;
validated when read back) and never shows the token: not in command results,
ui.inspect, logs or debug output.
- Home > Online lists your online files: open one as its owner (a new tab, or
its tab comes to the front), start and stop sharing, copy the code or link,
rename on the server, remove from the server (confirmed), forget here. One
request per server when the section opens; offline and gone states.
- collab.files, collab.openFile, collab.forgetFile; collab.manageFile takes
its server and token from the record and gains share / unshare.
- Server: Admin owned (a compatible addition within protocol version 2) says
what became of the files a list of owner tokens owns. Older servers leave
the list as recorded.
- Tabs mark files in a live session (shared, or open only to you); toasts
about a file that isn't in front name it; after a crash a recorded file
reopens from its server as its owner.
- Draft catalogs (de, fr, ja, ko, pt-BR, zh-Hans) translate the 71 strings
and 5 command labels they lacked and the new ones; es and es-419 follow.
- Docs: collab-protocol, server, mcp, control-protocol, parity pages, gaps,
ROADMAP.
The README gains a Privacy and online sharing section (what Share > Online uploads and where, how long it stays, encrypted in transit but not end-to-end, codes and links are the access, local network sharing is direct and unencrypted, no guarantees), its 'Offline and private' blurb no longer says 'no cloud', and docs/server.md's Security section says the same for operators.
- Scrubbing the label: a step a point, x10 with Shift, x0.1 with Alt, each
stretch at its own rate, counted from the press; on past the screen edge
where the platform reports the mouse's motion.
- Arrow keys: pixel fields by the nudge preferences (Shift: the big nudge),
other fields by their own step and ten of it; also with the keyboard on
the field; the stepped value stays selected.
- Typing: brackets and units in arithmetic; +10, *2, /4 and (outside
percentage fields) 50% are relative to the value.
- A field left as it was commits nothing (no undo step, no rounded display
written back); Tab commits and moves on; Escape restores.
- Mixed values: a relative edit or an arrow key goes to each layer from its
own value in one undo step (X, Y, W, H, rotation, opacity, corner radius,
stroke weight).
- Fields take keys in a window that isn't in front (control channel).
- Docs for the everyday editing partials: ui-parity rows, ROADMAP,
target-app-parity, gaps, mcp, control protocol, the parity checklist
(211/211).
component.hideWhenPublishing {ids?, on?} keeps this file's components and
sets out of the library it publishes. The model's sign of it is the name,
so the command writes it: hiding puts a "." before the name, showing takes
the leading "." and "_" away (one undo step, saved and synced like a
rename). The Assets panel's context menu on a local component toggles it
(checked while hidden) and the tile shows a crossed eye; also under
Libraries in the main menu.
- Cursors: the engine names a cursor for every tool, modifier and hover
target (rotate turned to its corner, pen states, eyedropper, comment,
copy on Alt, not-allowed while an instance refuses the edit); the UI shows
the system's or draws its own; guides and handles keep their cursors;
ui.inspect reports it.
- Panels: each edge drags its panel within limits, a double-click resets,
widths are per window and kept in the preferences (view.panelWidths); a
small window keeps its canvas.
- Outline mode: Include hidden layers and Include object bounds
(view.outlinesHiddenLayers, view.outlinesObjectBounds), drawn and
clickable; light lines on a dark page; check marks in the View menu.
- A file or page made in the dark theme has a #1E1E1E page (sessions
without an interface keep #F5F5F5); text drawn on a dark page is white.
- Tests for Enter and Escape on a selected text.
Apple's notarization ticket service dropped the connection in two release runs in a row
(once while validating the stapled DMG, once while stapling), failing the macOS job after a
successful build, signing and notarization. Stapling and its validation now try up to five
times, 15 s apart.