Commit Graph
1149 Commits
Author SHA1 Message Date
Marco Arnulfo e92792e3e8 M1: a press on a corner handle resizes instead of picking the corner-radius handle (fixes #292)
The corner-radius handle sits 10 px in from each corner and answers within 6 px, so a press on the inner half of the 8 px corner square picked it: the drag rounded the corners or, dragged outward, did nothing. Inside the drawn square the corner now resizes both sides (in ratio with Shift or the constrain-proportions link); further in the radius handle still answers.
2026-10-14 14:36:39 +02:00
haranishi d1ac66a803 M9: 日本語のテンプレートと編集操作ラベルを補完 2026-10-14 20:43:30 +09:00
notdezzi 661144021e fix(format): exclusive unique staging files for atomic save/export
Saving and exporting wrote a truncating std::fs::write to a deterministic
staging name (<path>.tmp-figcraft, <file>.figcraft.tmp) before renaming, so an
unrelated file or a planted symlink at that name was clobbered or written
through, and separate writers of one destination shared a staging file they
could corrupt. Both paths now go through figcraft_format::write_file_atomic: a
uniquely named sibling staging file is created exclusively (create_new) and
written through its own handle, renamed over the destination, and removed again
on any failure, so only the staging file a write created is ever touched. Tests
cover pre-existing staging files and symlinks, concurrent writers, and
injected rename and permission failures.
2026-10-14 13:11:28 +02:00
RealtaNuaandClaude Opus 5.5 73b2845ba4 fix(engine): Alt-drag copies snap to their original; Duplicate repeats the step
- 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>
2026-10-14 18:03:48 +07:00
notdezzi 10dc38495f fix(engine): roll back failed commands during interactions too
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.
2026-10-14 12:54:02 +02:00
notdezzi efdd5fc645 fix(format): import explicit variable modes on frames and instances
A collection a saved file references by its asset key (the form library
collections keep) instead of a guid resolved to nothing, so every
explicitVariableModes entry was dropped silently and bound layers rendered
in the collection's default mode (#267). The .fig import now resolves such
references against the file's own nodes (the collection node carries the
same key) for mode entries and the variables' collection link, and reports
modes of collections it can't resolve.
2026-10-14 12:40:17 +02:00
Justin John 61def74383 M0: finish canvas drags released outside 2026-10-14 16:04:34 +05:30
Marco Arnulfo ce366ea325 M6: .fig import: a resized nested instance keeps the non-geometric derived data; resize check with a tolerance (#266) 2026-10-14 12:14:45 +02:00
Marco Arnulfo d8556e8f17 M6: .fig import: size overrides keep the constraint placement, nested instances resized again drop their old derived layout (#266) 2026-10-14 12:12:46 +02:00
Marco Arnulfo 01eb35df25 M6: .fig import lays out the layers of resized instances by their constraints (fixes #266) 2026-10-14 12:00:02 +02:00
RealtaNuaandClaude Opus 5.5 b8e5b8e133 fix(pen): show the bezier handles while drawing
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>
2026-10-14 16:58:51 +07:00
RealtaNuaandClaude Opus 5.5 9c0b441119 fix(ui): disable alignment when the selection can't be aligned
One layer directly on the page has nothing to align to (one layer aligns
to its parent frame), so the six Alignment buttons looked active and did
nothing. They are now disabled there, with a hover explaining why.

- cmd/arrange.rs: align_target (unchanged rule) and pub can_align.
- right.rs: the align rows in add_enabled_ui + on_disabled_hover_text.
- The hover text in every catalog.

Tests: a_layer_alone_on_the_page_has_nothing_to_align_to (engine),
the_align_buttons_are_off_for_a_layer_alone_on_the_page (AccessKit).

Refs #256

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 16:53:33 +07:00
RealtaNuaandClaude Opus 5.5 3f57a4b76a fix(ui): gradient type icons that show their gradient
The colour picker's paint type row borrowed unrelated icons for the
gradient kinds (auto layout's bars, a circle, the rotate arrow, the
instance diamond). Each kind now has its own icon: the Solid swatch as
an outline with that kind's fade inside (linear ramp, radial, a conic
sweep from the top, diamond), drawn with a new per-vertex-alpha mesh
helper.

Test: gradient_icons_fade (each paints a mesh from opaque to clear).

Refs #256

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 16:52:00 +07:00
Marco Arnulfo bd482ea6dd fix(ui): scroll a long pages list in its own area so the layers stay in view (fixes #258) 2026-10-14 11:15:32 +02:00
Marco Arnulfo 64a54ca408 M7: double-clicking a page in the Pages list no longer closes the app (fixes #264) 2026-10-14 11:11:00 +02:00
Yaman DevrimandClaude Opus 5.5 5f3e1e1e6c M9: Turkish catalog reviewed by a native speaker
Drops the draft status from tr.toml (the Preferences window no longer
calls it a draft) and marks Turkish reviewed in ROADMAP.md,
docs/localization-parity.md and docs/translating.md. Turkish stays out of
COMPLETE, so new strings don't have to be translated into it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:47:57 +03:00
ahmed-226 dde193cbf4 M7: a slice's export preview shows the page content under it
A slice paints nothing itself, so thumbnail() rendered an empty pixmap of the slice's own box: expanding Preview for a slice showed only the transparency checkerboard while exporting the same slice wrote the artwork correctly.

Give the preview the export's slice rule (drawn_layers + export_area in cmd/file.rs): draw every top-level layer of the slice's page over its world bounds. Regression tests cover a slice over artwork, a slice with nothing under it, and a plain layer.

Signed-off-by: Ahmed Mahmoud <ahmedmhmouad41@gmail.com>
2026-10-14 10:37:23 +03:00
VanTan 4964910cc9 M7: follow system theme from the first frame 2026-10-14 10:34:14 +03:00
Yaman DevrimandClaude Opus 5.5 06850b8900 M9: Turkish draft catalog
crates/ui-egui/locales/tr.toml translates every UI string (1,577 keys),
command label (557) and menu wording (31), machine-drafted and marked
"draft — needs native review". Buttons and menu items use bare
imperatives (Kaydet, Sil), instructions the polite -in/-ın (seçin,
tıklayın); the percent sign goes before the number (%50). Turkish keeps
a noun singular after a count, so plural entries give only `other`.

Preferences ▸ Language lists Türkçe; System maps tr and tr-TR to it.
The catalog passes the i18n check even when listed as complete.
docs/translating.md, docs/localization-parity.md and ROADMAP.md list
the new draft. Adds my own contributors/people.toml entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:34:10 +03:00
VanTan 7a0bd980c6 M9: add Russian interface translation 2026-10-14 09:33:58 +03:00
Piettro CastroandClaude Opus 5.5 f3cd0509cb fix(engine): read the documented gradient shorthand on every route (#252)
`{"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>
2026-10-14 01:02:45 -05:00
Justin John 94ce2a3611 M13: make native file dialogs asynchronous 2026-10-14 11:04:30 +05:30
bcedfeba10 M7: recent files as thumbnail cards on the home screen (#249)
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>
2026-10-14 12:00:20 +07:00
rcoenenandClaude Opus 5.5 6267ae5638 Vector edit mode: hover, hatch, select and drag filled regions (#243)
With the Move tool, the filled area under the pointer is hatched with "/" lines in the selection colour (1.4 px), clipped by the area's own fill rule so holes stay clear. The hatch goes when the pointer is 3.5 px from a point or 2 px from a segment (screen pixels at any zoom); between those clearances and the 6 px hit radius the area wins. Bezier handles, the transform box's handles and other edited layers' parts come first. Line spacing follows octave folding: 8 px at every power-of-two zoom, growing with the zoom and stepping back at sqrt(2) * 2^n. The pattern is anchored to the vector's origin, so it stays on the shape when panning, and several selected areas share one pattern. The last area listed wins where areas overlap, and only vectors with a visible fill have areas.

A click selects an area with its points; they move with it (drags, nudges, the Design panel) but keep the unselected look and show no handles. Shift adds an area or takes a selected one out (its own points go, points selected on their own or shared with another selected area stay). A press on a selected area drags everything selected, as one undo step; Escape puts it back. Drags show no hover hatch. Any other press without Shift drops the areas and the points selected only through them, and a click on an empty spot such as a hole clears them and stays in edit mode. Inside the transform box around points selected on their own, the box keeps its drag unless the area is already selected. Area indexes are dropped when the network's topology changes; moving points keeps them.

The network gets region walking, an inside test per region by its own fill rule, the region under a point, a region's points and a topology fingerprint (vector_regions.rs). The engine does hit testing and lays out and clips the hatch in screen space (vectoredit/regions.rs); the canvas only draws the lines. vector.edit.select takes regions, and vector.edit.state reports regions, regionVertices and regionHover. Tests: vector_regions_tests (doc) and regions_tests (engine), covering fill rules, overlap, curves, reversed segments, clearances at 100% and 400%, selection, drags, undo and cancel, and the hatch spacing at seven zoom levels, its direction, anchoring, coverage and clipping.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:17:17 +07:00
5e90592e80 M1: report images a native file lost on loading (#207) (#242)
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>
2026-10-14 10:32:44 +07:00
ef2c6cb30e M1: SVG import budgets for SVGZ and embedded SVGs (#190) (#241)
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>
2026-10-14 10:32:30 +07:00
24c5617230 feat(dev-mode): Leptos code with component properties, instance calls and Code Connect (#240)
* 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>
2026-10-14 10:32:14 +07:00
fb8ace5bf4 M1: full screen (#227) (#238)
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>
2026-10-14 10:31:59 +07:00
ce84d1f27f fix(ui): keep wrapped colour picker swatches inside the picker (#180) (#237)
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>
2026-10-14 10:31:46 +07:00
d61ccc633a fix(doc): reject and repair layer cycles and repeated children (#205) (#233)
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>
2026-10-14 10:31:03 +07:00
6d735653a2 fix(render): keep one outline depth budget through nested booleans (#193) (#231)
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>
2026-10-14 09:30:29 +07:00
Giuseppe Cautieroandbflatastic ac65168417 Implement centralized transactional fix for command batch failures (#229)
* 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>
2026-10-14 09:30:16 +07:00
Jeremy YaoandClaude Opus 5.5 f7e726714a fix(text): don't rebuild every time a craft-fonts manifest is missing (#228)
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>
2026-10-14 09:30:03 +07:00
Sir Clangington III. 79a0e58a3c fix(plugin): meter every host call and copied byte (#221)
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.
2026-10-14 07:42:06 +07:00
fede9234 db55720c93 fix(engine): keep proportions when a linked width or height is typed (#222)
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.
2026-10-14 07:31:19 +07:00
YTwsy 0efbd55564 Bound plugin logs before allocation and host forwarding (#216)
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.
2026-10-14 05:49:32 +07:00
Euisuh Jeong 5b52eeeb36 fix(collab): reject reverting an active live document (#214)
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.
2026-10-14 05:47:39 +07:00
Euisuh Jeongandbflatastic 8b7435890e fix(mcp): prevent replay after lost attached replies (#211)
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>
2026-10-14 05:34:33 +07:00
YTwsyandbflatastic 33dcb1aef6 Refuse incomplete SVG/PDF exports beyond the depth limit (#218)
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>
2026-10-14 05:23:28 +07:00
Euisuh Jeong e8d7dc24f2 fix(control): bound frames and expire queued requests (#213)
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.
2026-10-14 05:18:40 +07:00
Euisuh Jeong d30255bf62 fix(engine): preserve the underlying repeat command (#212)
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.
2026-10-14 03:26:16 +07:00
Saad Abdullah 01904ce2fd fix(geom): enforce the path sample limit while flattening (#217)
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.
2026-10-14 03:26:11 +07:00
bflatastic e56856f401 M3: component playground: variant, boolean and text properties 2026-10-13 13:36:54 +07:00
bflatastic e02ad435b3 M2: auto layout: dragging an automatic gap's handle fixes the gap 2026-10-13 13:36:54 +07:00
bflatastic a399ff8934 M3: variables inside styles 2026-10-13 13:36:54 +07:00
bflatastic d7f76246d3 M3: effects on groups: inner shadows and background blur 2026-10-13 13:36:54 +07:00
bflatastic fca70a6063 M1: text made by commands in the app is readable on dark pages 2026-10-13 13:36:54 +07:00
bflatastic 2c35c7bd21 M1: frames and groups: one-step convert 2026-10-13 13:36:54 +07:00
bflatastic 5548284c65 M5: prototyping: delays on mouse triggers, manual overlay offsets 2026-10-13 13:36:54 +07:00
bflatastic a2a0f80252 M2: auto layout: wrapped-line alignment 2026-10-13 13:36:54 +07:00