Commit Graph
506 Commits
Author SHA1 Message Date
bflatastic a3e8c86331 Merge fix/620 2026-10-17 14:40:55 +07:00
bflatastic 9f01b21a54 Merge fix/619 2026-10-17 14:40:49 +07:00
bflatastic d8df6f6895 Merge main into #666 2026-10-17 14:36:15 +07:00
bflatastic b5cba7c885 Merge fix/711 2026-10-17 14:35:20 +07:00
bflatastic 4c1f2b978b Merge fix/697 2026-10-17 14:35:17 +07:00
bflatastic 1328092a7e Merge fix/612 2026-10-17 14:35:15 +07:00
bflatastic 45c68177d0 Merge fix/695 2026-10-17 14:35:14 +07:00
bflatastic 3353d2d5d3 #647: edit.pasteDesignLayers, named neutrally; design data capped at 256 MB before decoding, data accepts wrapped or unpadded base64 2026-10-17 14:34:57 +07:00
bflatastic 92714667f6 Merge main into #647 2026-10-17 14:27:33 +07:00
bflatastic 313a38c0a8 Merge fix/630 2026-10-17 14:04:55 +07:00
bflatastic baa58b21fa #620: file.open with base64 data returns the summary too 2026-10-17 13:59:03 +07:00
bflatastic 24eec5d6bb Merge main into #620 2026-10-17 13:59:02 +07:00
bflatastic 3134849b81 Merge main into #619 2026-10-17 13:49:02 +07:00
bflatastic 9b4e45aa0b #711: move the vector.offset tests into their own module 2026-10-17 13:40:55 +07:00
bflatastic e4f2c7fc4c Merge main into #711 2026-10-17 13:38:42 +07:00
bflatastic 102f553f2c Merge main into #697 2026-10-17 13:35:58 +07:00
BFlat 69806c9edd Plugins: refuse file and network access through path params and collab (#723)
Plugins can no longer reach the disk or network through path parameters (variable.export/codeConnect.export/component.playground writes, codeConnect.import/variable.import reads) or through collab.* and window.*; found and fixed by @lucasmartins-ai, split out of #696.
2026-10-17 13:35:00 +07:00
bflatastic fbe953e9b0 Merge main into #612 2026-10-17 13:33:11 +07:00
bflatastic b9d587e80e Merge main into #695 2026-10-17 13:27:11 +07:00
bflatastic fa4821bcea #627: settle bound paints once per open, not also in the native loader
DocState::new already settles every opened document (native files, branches and live joins
included); the .fig/REST path keeps its settlement in link_instances for file.import into an
open document.
2026-10-17 13:27:11 +07:00
bflatastic b0e9e87129 Merge main into #627 2026-10-17 13:21:48 +07:00
bflatastic 52df73c44c Merge main into #626 2026-10-17 13:10:59 +07:00
BFlat 40c744da10 M1: convert frames to sections and sections to frames (#719) (#721)
Frames on the page or in a section convert to sections (object.convertToSection) and sections back to frames (object.convertToFrame), in place, one undo step each, from the canvas context menu; nested frames, instances, main components and sections holding sections are refused.
2026-10-17 13:03:49 +07:00
Ismael Trejo ce472a251c fix(doc): thin strokes take a double-click a few pixels away (#646)
A 1 px Line or stroke-only path only counted as hit within 2 screen
pixels, so a double-click slightly off it cleared the selection and
never entered vector edit mode, and Cmd/Ctrl could not bend it.
Strokes with nothing filled now reach 5 screen pixels; filled shapes
keep the plain tolerance.
2026-10-16 16:40:27 -07:00
NetterSilverandClaude Haiku 5.5 9e2596e975 Skip layers with children in vector.offset
Found while testing crates/engine/src/cmd/structure.rs:575

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 22:09:53 +00:00
Ahmed5Emad dc97118c6f Actions palette rework: Figma-shaped panel over the toolbar
Tabs (All, Assets, Plugins & widgets), Recents kept in the preferences
(newest first, capped at 5), commands grouped by top-level menu, this
file's components (placed at the view centre) and styles (applied to
the selection), installed plugins and widgets with letter tiles. Rows
carry right-side shortcut/kind hints; Enter runs the first visible row.

The panel floats 8 px above the toolbar pill with its exact width and
centre (toolbar::main_width, canvas-centred), at a fixed size so tab
switches never resize it. Tabs keep their frames when idle so the
active pill never moves its neighbours.

Engine: Prefs.recent_actions (persisted, skipped when empty). UI:
invoke/menu/plugin picks record recents; gone entries drop out.
Locales: new keys in en + es, retired the old search hint everywhere.
Docs: ui-parity row describes the new surface (palette_tests).
2026-10-16 22:58:36 +03:00
RealtaNuaandClaude Opus 5.5 d27f45e063 fix(format): write a list's start on every line; report it in get_node (#686)
The .fig export now writes a numbered list's start on each of its lines,
so a reader that takes it per line or per list counts the same. Bullets
no longer keep a start. The field is named start_offset (it holds the
offset: 1 shows "2."). get_node reports listStartOffsets. The parity doc
lists what's still not read from .fig lists.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 00:58:20 +07:00
NetterSilverandClaude Haiku 5.5 9aeb9fb752 Cap stroke width in the outliner so huge weights don't panic
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>
2026-10-16 19:58:00 +02:00
RealtaNuaandClaude Opus 5.5 60fa813eb2 fix(format): numbered lists from .fig files keep their start number (#686)
Figma stores where a numbered list starts counting on each text line
(listStartOffset; a list shown as "2." has offset 1). The import dropped
it, so every list counted from 1.

A paragraph's list settings now carry a start number (written only when
set, so older files open and save unchanged). Numbering counts on from
it where a list starts; items that go on counting ignore it. The .fig
import reads it, the .fig export writes it on a list's first line, and
plain paragraphs drop it. Toggling a list off ignores differing starts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-17 00:27:34 +07:00
RealtaNuaandClaude Opus 5.5 569a3b3669 fix(engine): a Shift-click on a selected widget doesn't also click it (#498)
With Shift let go before the button, the release took the layer out of
the selection and also ran the widget's click. The take-out now skips
the widget click, as a Shift-click always has.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 23:11:00 +07:00
RealtaNuaandClaude Opus 5.5 8303cfa56f fix(engine): Shift-drag on a selected layer moves the selection (#498)
Shift on a selected layer took it out of the selection on press, so the
drag that followed had nothing to move, and with several layers selected
the grabbed one stayed behind. It now leaves the selection on release,
and only when the press didn't become a drag. Where no move can start
(a version being viewed), it still leaves on press.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 22:52:41 +07:00
NetterSilverandClaude Haiku 5.5 0e25cf4c26 Refuse pages in edit.copy and edit.cut
Found while testing crates/engine/src/cmd/edit.rs:499 (copy_to_clipboard, called from copy at :536 and cut at :542)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 13:47:07 +00:00
phonedu 9daf39e24c M1: a param that names a layer says so when it isn't one
`node.create` read its `parent` as `id(p, "parent").unwrap_or(page)`, so a `parent` that isn't a
layer id — a `{"$step": n}` nothing had resolved, a name, a negative number — quietly meant "no
parent" and the layer landed on the page, with the call reporting success. The same shape sat on
`image.place` and `instance.create`, on `index` (a string index appended where it was asked to
insert), and on the `ids` of every command that targets layers (an entry that wasn't an id was
dropped and the rest ran). A number that names nothing was reported; anything that isn't a number at
all was not.

`params::opt_id` and `params::opt_index` now say what the commands mean: missing and `null` are
"none", and anything else has to be a layer id (or a whole, non-negative number) and is named when it
isn't. `req_targets` refuses an `ids` entry that isn't an id, before falling back to the selection. A
number that names no layer still reports `no such node`, and a boolean is still a number to `f64_of`,
as it is for `x`, `width` and the rest.

This turns a silently wrong result into an error on purpose, so a caller that passed something that
isn't an id and relied on the fallback now hears about it.
2026-10-16 19:53:43 +08:00
Ahmed5Emad 1aa499c84f Merge remote-tracking branch 'origin/main' into feat/font-hover-preview
# Conflicts:
#	crates/ui-egui/src/style_picker.rs
#	crates/ui-egui/src/typography.rs
2026-10-16 14:34:02 +03:00
phonedu 0f4c79b629 M0.9: a script runs as one undo step and resolves $step, like command.batch
`figcraft-cli run` ran each step of a script on its own, so a script's `{"$step": n}` reached
`parent` unresolved and the layer landed on the page while every step reported success — the same
commands through `command.batch` (the MCP `batch` tool, the control channel, plugins) put it inside
the frame. One `edit.undo` took back one step of a script rather than the script, and a failing step
left the earlier ones applied.

`Session::batch_step` is now the one path both take — one step of a batch, its `$step` references
resolved against the results before it — and `run_script` wraps its steps in `Session::grouped`: the
whole script is one undo step, and a step that fails takes the script back. The result lines come out
once the script is through, so none of them reports work a later failure took back; a failure prints
one line with `rolledBack: true` and exits 1. `docs/cli.md` documents the reference and the output.
2026-10-16 19:33:59 +08:00
Hamza 805a121e15 Hold the duplicate step for a dragged and a nudged copy (#567)
Duplicate repeats the last duplicate's offset, but of the ways to move the copy
before the second Cmd+D only `node.set` and Alt-drag were covered. Dragging it
with the mouse is how almost everyone does it, and is what #567 describes; an
arrow-key nudge is how a small offset is usually made exact. Both work, because
the step is measured from where the copy ended up rather than recorded from the
gesture, but nothing held them there.

The drag test also asserts the drag moved the copy rather than making another
one, left it selected, and left the original in place: the ways the feature
could break without the step arithmetic breaking.

Both fail if the step lookup in `edit.duplicate` is stubbed out.
2026-10-16 16:30:58 +05:00
Ahmed5Emad e1d9f24312 Merge remote-tracking branch 'origin/main' into fix/layout-add-keeps-frame-size 2026-10-16 14:27:19 +03:00
Ahmed5Emad 2cac36a4e2 Live font preview on hover in font picker (#653)
Hovering a family in the Typography font dropdown previews it on the
canvas via text.preview; closing without picking restores the original,
clicking applies it as one undo step.
2026-10-16 12:31:50 +03:00
AganzeFelicite b962603707 M4: points dropped on a point merge
In vector edit mode a point dragged onto another point of its path
snapped there but stayed a separate point, so a path couldn't be closed
or joined by dragging. A single dragged point dropped within the point
hit radius (6 px) of another point now merges into it, in the same undo
step as the move, and that point stays selected.

Closes #401
2026-10-16 10:46:36 +02:00
NetterSilverandClaude Haiku 5.5 a911ab9614 Reject negative or fractional index in fill.set and stroke.set
Found while testing crates/engine/src/cmd/edit.rs:1180

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 08:29:55 +00:00
Andrii VolkovandClaude Opus 5.5 afdb21bad6 fix(ui): drag the line between pages and layers to size the pages list
The pages list took a fixed 40% of the left panel (#272). The line under
it now drags to make the list taller or shorter: at least three rows, and
always leaving the layers their header and three rows. A double-click puts
back the default. The main window keeps the height in the preferences, as
it does the panel widths, saved once the drag is over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YQxjkNSUJhTkTfY74aNeki
2026-10-16 11:24:39 +03:00
AganzeFelicite e8b181de39 M4: Flatten keeps open paths
Flatten (Cmd+E, Alt+Shift+F) and Join selection (Cmd+J) built the result
from each layer's fill outline, so open paths, which have none, were
dropped: two pen paths flattened to "nothing to flatten", and next to a
closed shape they vanished. Vectors now bring their own network (open
paths, unfilled loops and each region's winding stay), mapped into the
parent's space; other layers still flatten to their outline.

Closes #377
2026-10-16 10:23:57 +02:00
Skyfuller1303 a4156d2910 M3: re-read a library file whose stamp is as recent as the read
A library file is cached by its length and modification time. File systems keep the time in coarse ticks, so a same-length rewrite right after a read can keep the stamp, and library.updates then reports no change. A stamp now proves the file unchanged only once its modification time lies at least two seconds before the read; otherwise the file is read again.
2026-10-16 09:37:07 +02:00
JimKnoxxandClaude Opus 5.5 ab10e37d43 M1: paste layers copied in Figma
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>
2026-10-16 08:20:49 +02:00
Ivan Shishkanov 5be9a8caef M1: cache smart-selection geometry and skip unchanged drag edits 2026-10-16 08:06:26 +03:00
Ivan Shishkanov 1ac0e475c1 M3: adapt Selection colors to current main 2026-10-16 07:58:58 +03:00
NetterSilverandClaude Haiku 5.5 8538dd740f Refuse edits that leave a layer with non-finite geometry
Found while testing crates/engine/src/cmd/arrange.rs:132

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-16 03:38:36 +02:00
Skyfuller1303 4b29d8c34d M1: apply instance overrides by key and order, settle bound paints, keep stroke outline styles
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.
2026-10-16 03:32:38 +02:00
Skyfuller1303 d331db00aa M1: keep explicit instance identity and cached library components in .fig import
A .fig import now links the layers of an instance to the layers of the
component they came from by the ids the file stores, instead of leaving it to
the engine to guess from names and places. Renamed slots and layers, siblings
that share a name and swapped mains keep their source link and the override
they carry.

Components that live only on the file's hidden internal pages (cached copies of
library components) are kept in the hidden library store when a visible layer
uses them, so their instances stay instances. The read-only guard for library
copies now compares the copies themselves rather than the dirty set, which an
import leaves full until the first edit.

Rebasing an instance (after detaching an outer instance or restoring a main)
follows the source chain inside the right enclosing instance, so contextual
links survive it. The last change of a GUID wins and a removed change deletes
it. Ordinary file JSON never reads the private provenance.

Importing such a file into an open document, or pasting its layers into another one, copies the cached components along (import) or leaves them behind as a missing main (paste), so no link points at a layer of the target.
2026-10-16 03:32:36 +02:00
BFlat 9c2d61f6ae Merge pull request #601 from Skyfuller1303/feat/drop-to-open
M7: drop files anywhere to open them
2026-10-16 07:41:02 +07:00