Commit Graph
4092 Commits
Author SHA1 Message Date
Dylan M. Taylor d87f9effe1 refactor(engine): simplify asset and code generation helpers 2026-10-28 16:00:36 +00:00
Dylan M. Taylor 5e16683963 refactor(engine): simplify canvas and board handlers 2026-10-28 16:00:36 +00:00
Dylan M. Taylor 23f57c8660 refactor(engine): simplify command handling for strict clippy 2026-10-28 16:00:36 +00:00
Dylan M. Taylor bafda992a7 docs: clarify crate lint placement 2026-10-28 16:00:36 +00:00
Dylan M. Taylor 4268bd859f refactor(fig): split mapping and lint test helpers 2026-10-28 16:00:36 +00:00
Dylan M. Taylor 6ebcf6b511 refactor(text): split shaping and layout finishing 2026-10-28 16:00:36 +00:00
Dylan M. Taylor c618e402fa refactor(format): replace wildcard imports 2026-10-28 16:00:36 +00:00
Dylan M. Taylor 99afdd99ab docs: document workspace lint inheritance 2026-10-28 16:00:36 +00:00
Dylan M. Taylor 8591d44909 refactor(doc): declare node property dependencies 2026-10-28 16:00:36 +00:00
Dylan M. Taylor 10bb163626 fix(xtask): apply workspace lint policy 2026-10-28 16:00:35 +00:00
Dylan M. Taylor e1a223bb1f refactor(layout): split flow layout phases 2026-10-28 16:00:35 +00:00
Dylan M. Taylor c420613efa chore(clippy): enforce strict workspace lint policy 2026-10-28 16:00:35 +00:00
watchakorn-18kandClaude Sonnet 5.5 38ab0eaa6b docs(ui): document the right-click label pick and its tests
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015wxxFg3gana4zaUKnyirTC
2026-10-28 22:27:29 +07:00
watchakorn-18kandClaude Sonnet 5.5 df6062d7fb fix(ui): right-click on a frame's name label picks that frame
The context menu looked only at what is under the pointer on the page, so a name label drawn over
its section selected the section. Copy link to selection then gave every frame in that section the
section's link. Labels now win, as they do on a press.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015wxxFg3gana4zaUKnyirTC
2026-10-28 22:14:06 +07:00
NetterSilverandClaude Haiku 5.5 877ac1fd5e Reject non-page ids in page.duplicate
Found while testing crates/engine/src/cmd/page.rs:96 (page.duplicate, `let src = id(p, "id").unwrap_or(s.page()?)`)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 15:38:32 +01:00
samhyunandClaude Fable 5.1 3773a60801 M8: the browser's wait after a preview never exceeds the render it may save
With a fixed 120 ms wait, a page whose full renders take 15–100 ms was held coarse for 120 ms
after a key zoom or a page switch, where before it was refined in the next frame. The wait is
now the last full render's time when that is shorter than `SETTLE_MS`: a wait never costs more
than the render it may save, and on a heavy page (seconds a render) it is 120 ms as before.

Also from review: the test that the wait counts from the preview no longer times a preview
render (a margin of a few milliseconds under load); a sleep between the request and the poll
stands in for the render, with the whole settle as margin. `Inline::in_flight` says that a
preview's wait counts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-28 22:53:07 +09:00
phonedu 8e6b3bf449 M12: MCP comment feedback and live change events
Comments become the task queue for an attached or headless MCP client: comment.prompt returns a bounded Markdown brief, the Comments panel gains Copy for AI, and the MCP server exposes fix_comments, watch_comments, figcraft://comments and wait_for_changes. Mutating comment.* commands append to a bounded in-memory event log; the control channel's events.poll long-polls it without the UI thread, and a successful batch commits its events only if the whole batch lands. No model, network dependency, asset or file-format change.
2026-10-28 21:42:31 +08:00
samhyunandClaude Fable 5.1 8c951810ab M8: a test for same-zoom requests before a zoom refines; the gaps status line
From the review of #1468: a pan or an edit at the new zoom before its full render has run
renders from scratch too, so it shows a preview and restarts the wait instead of running a
blocking full render between it and the next step; once refined, requests at that zoom build
on the kept image and are refined at once. The new test pins both down. The status line of
`docs/gaps.md` now carries the date of the gap 7 entry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-28 22:29:55 +09:00
samhyunandClaude Fable 5.1 f24a8312f7 M8: the browser refines a zoom only once it has paused
The browser renders on the UI thread. A zoom showed its coarse preview and then, in the very
next frame, ran the full render, so every wheel step of a continuous zoom had a blocking full
render between it and the next: on a heavy page each one took seconds, and the zoom stuttered
for as long as it went on.

The browser's worker (`Inline`) now refines a preview only once no newer request has come for
`SETTLE_MS` (120 ms) after the preview was shown: the zoom has paused. Each wheel step still
shows its preview at once, and the worker asks for a repaint when the wait is over. Pans and
edits have no preview and are refined at once, as before. The desktop has a render thread of
its own and is unchanged. A zoom passing through a scale also no longer spends a full render
there.

The wait counts from the preview being handed over, not from the request: rendering the
preview can itself take longer than the wait (the page is blocked meanwhile, so no newer
request could have come), and counted from the request the full render would follow in the
very next frame, as before.

Measured in Chrome on a synthetic page of 20 images, 60 shadowed rectangles and two
background blurs (a full render takes about 3.7 s, a preview about 250 ms), 20 wheel steps in
and 20 out, 50 ms apart, three runs each: the frame interval's p95 went from 3.7 s to 240 ms,
the frames over 100 ms from 79–80 (one full render per step) to 39–41 (one preview per step),
and the zoom took 10–12 s instead of 141 s. The one full render after the pause still takes as
long as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-28 22:10:27 +09:00
NetterSilverandClaude Haiku 5.5 2199dcb2da Reject non-numeric paddingHorizontal/paddingVertical in node.set
Found while testing crates/engine/src/cmd/props.rs:623-634 (the "paddingHorizontal" and "paddingVertical" arms)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 13:04:10 +00:00
hussain-hz 636096fad3 M10: add Arabic draft interface catalog
Translate 2508 interface entries from English, keep count-neutral plurals, and gate the web catalog on Arabic font readiness. Record AI assistance and pending native review.

AI-assisted-by: OpenAI Codex
2026-10-28 15:16:31 +03:00
Skyfuller1303 faa96926f4 M4: keep stroked whole-shape merges and bound region paints per file
Merging every region of one stroked shape is a no-op again: only fill
parts count, so a vector with several painted regions still merges.

The region paint import budget now covers the whole file instead of
resetting for each layer, so instances of a main with large region paint
stacks cannot multiply the work.
2026-10-28 11:59:53 +01:00
Skyfuller1303 c785e96b1c Merge remote-tracking branch 'origin/main' into feat/fig-vector-region-fills
# Conflicts:
#	crates/format/src/fig/export.rs
#	crates/format/src/fig/map.rs
2026-10-28 10:12:00 +01:00
Skyfuller1303 4b13e4e3cf M7: line the Home tab's divider up with the icon rail
The Home tab started at x=8 and was 44 wide, so its divider sat at x=52 while the icon rail below is 56 wide. Without inset window buttons the Home tab now ends at the rail's width (shared RAIL_WIDTH constant), so the divider continues the rail's edge. The inset (macOS) title bar keeps its 44-wide Home tab.
2026-10-28 10:00:02 +01:00
MortenHustedandClaude Opus 5.5 8fadd5d8e5 #1460: file.exportSvg's doc says which of file.export's options it takes
"(as file.export)" read as all of them, but the PDF and XML options are
refused there, so it now says they are file.export's SVG options.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:46:27 +01:00
MortenHustedandClaude Opus 5.5 3567bef862 #1460: commands and tools that pass their params on list what they pass
file.placeImage runs image.place's code and file.exportSvg file.export's,
so their docs now list the names those read (width, height, target, mode;
scale or size and the export options). MCP's create_node passes points,
closed and select to node.create, and pointer and key read ctrl, so their
schemas list them too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:35:23 +01:00
Skyfuller1303 219a9ea0b1 M4: require FILL styles for vector region fill links 2026-10-28 09:29:22 +01:00
MortenHustedandClaude Opus 5.5 40b9288f70 M1: opening a file that is already open says when it changed on disk, and can read it again
`file.open` on a path already open in a tab switched to it and returned
its index, even when another process had changed the file since, so the
caller kept looking at the old copy with nothing saying so (#997). The
tab's disk stamp from #1330 (`DocState::on_disk`) now tells:

- the result says `stale: true` when the file on disk differs from what
  the tab read or last saved;
- `reload: true` reads the file again into the tab, as Revert to saved
  does (refused while the tab has unsaved changes or is in a live
  session), and the result says `reloaded: true`.

MCP's `open_file` takes `reload` and passes it on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:19:56 +01:00
MortenHustedandClaude Opus 5.5 27c11858e1 M0.9: MCP tools and batch steps refuse names they don't take (#1413)
`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>
2026-10-28 09:06:59 +01:00
MortenHustedandClaude Opus 5.5 ff4fe00294 M0.9: commands refuse parameter names they don't take (#1413)
A misspelt parameter name was ignored and the command reported success,
answering as if the parameter hadn't been given (`comment.list
{"filterr": "open"}` listed resolved threads). `Session::execute` now
refuses names the command's params doc doesn't list, naming them and
what the command takes, before the command runs, so every path into a
command (CLI, batches, control channel, MCP, plugins, the UI) gets the
check.

The names come from the existing docs, parsed once with the registry:
each top-level item's first word and the words joined to it by `|`, `/`
or `+`, bracketed text and what follows `:`, `=` or `→` being
description, alternative groups (`{x, y} | {points}`) included. `ids`
and `id` are always taken (`params::targets` reads either); node.set,
whose doc is its props list, keeps its own check.

The junk sweep runs each junk object as given and cut down to the names
a command takes, so the junk values still reach the parsers. Tests that
passed names their command never read (comment.resolve's author,
collab.host's link, node.get's depth) pass only what it takes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 09:06:58 +01:00
MortenHustedandClaude Opus 5.5 64e648ae7e M0.9: command docs list every parameter their commands read
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>
2026-10-28 09:06:33 +01:00
MortenHustedandClaude Opus 5.5 81769cbe74 #1458: test layoutMode beside the keys that tune it too
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:51:02 +01:00
MortenHustedandClaude Opus 5.5 f98da27882 #1458: name the key that turns auto layout off
`{"layoutMode": "NONE", "wrap": true}` was refused with a message
naming `layout`, a key the call didn't have. The error now names the
key the call used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:49:34 +01:00
MortenHustedandClaude Opus 5.5 2ff69183d7 M7: CSS system font names draw in macOS's System Font
`system-ui`, `-apple-system`, `BlinkMacSystemFont` and
`.AppleSystemUIFont` join the names `apple_alias` resolves to the
installed System Font, as SF Pro and SF UI already do. SVG import keeps
a family the SVG lists when a font answers to its name, so on a Mac
`font-family="-apple-system, system-ui, sans-serif"` keeps
`-apple-system` and draws in System Font; elsewhere the generic
stand-ins apply as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:35:46 +01:00
MortenHustedandClaude Opus 5.5 b45c41a02c M2: auto layout settings in one call don't depend on how their keys are spelled
`apply_order` gave `layout` the rank of every other key, so the props
map's alphabetical order decided: `gap` applied before an object
`layout`, which then replaced the whole auto layout (the gap went back
to its default), and `padding` applied after `layout: null` and turned
auto layout back on.

`layout`/`layoutMode` now apply right after the geometry keys and
before the keys that tune them, and auto layout keys beside a `layout`
of null or "none" are refused, naming them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:35:33 +01:00
NetterSilverandClaude Opus 5.5 2d02f289ec Keep session joins working when a document has out-of-range ids
Fixes #1067

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 02:16:00 +01:00
Davit MatchakhelidzeandClaude Opus 5.5 a7ea510c65 i18n(ka): მხარდაუჭერელის in "Ignore unsupported"
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMTAbGNRxnMEzRvYnQGphF
2026-10-28 05:11:34 +04:00
Davit MatchakhelidzeandClaude Opus 5.5 610f40bd78 i18n(ka): Georgian for 76 strings added since the language landed
Click = დაწკაპეთ in the voting hint, as in the catalog's other 24 click hints.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMTAbGNRxnMEzRvYnQGphF
2026-10-28 04:00:21 +04:00
aiongg 90054a89c4 Merge pull request #1399 from Jeremy8776/feat/install-scope-policy
Windows installation policy: FigCraft
2026-10-27 17:09:27 -04:00
aiongg 2ed7aee28d ci: use storytold/craft-common-actions at the merged installer-policy commit 2026-10-27 20:51:59 +00:00
Guillermo Bueno fe7872708f M4: draw bounded dots for zero-length round dashes 2026-10-27 21:11:32 +01:00
Guillermo Bueno 54eced7ffb M6: address offline Penpot review regressions 2026-10-27 20:48:08 +01:00
Jeremy 738b218837 fix(installer): check the exact architecture-specific machine folder
The pre-signing check matched any folder named Program Files, so it rejected
a valid MSI when Windows resolves Program Files elsewhere, accepted the 64-bit
root for an x86 package and accepted a different app name. Build the expected
folder from the package architecture's resolved root and the policy name.
2026-10-27 19:08:54 +00:00
Filip Strajnar 6cc466a878 M6: credit Filip Strajnar for the FigJam import fix 2026-10-27 20:04:54 +01:00
Guillermo Bueno 7ea161f33f M6: merge main and isolate offline Penpot import/export 2026-10-27 18:59:49 +01:00
Filip Strajnar 4ea2d29754 M6: preserve FigJam fallback outlines and sticky bodies 2026-10-27 18:47:27 +01:00
aiongg 8b591f703d Merge pull request #1437 from Silvertree2010/perf/export-speed
Speed up raster export: faster blur and spread passes
2026-10-27 13:40:56 -04:00
NetterSilverandClaude Opus 5.5 adb1b43033 Speed up raster export: faster blur and spread passes
Shadows and layer blurs dominated PNG/JPG/WebP export time (gdb stack
sampling of the example files: about 45% in fx::box_blur_h, 13% in
fx::morph). Both now produce the same bytes, faster:

- fx::blur runs its three box passes per row while the row is in cache,
  works on whole pixels instead of bounds-checked bytes, skips fully
  transparent rows, transposes in cache-friendly column blocks, and
  splits rows and transposes across rayon on desktop for pixmaps of
  64K pixels and more, only when not already on a rayon worker (a band
  of render_view stays on its thread: no nested parallelism).
- fx::morph (shadow spread on layers that aren't one outline) uses the
  van Herk / Gil-Werman running max/min: constant work per pixel instead
  of 2r + 1.

Integer arithmetic and pass order are unchanged; new tests compare both
against the previous implementations byte for byte, including sizes on
the parallel path.

An ignored timing test exports every top-level frame of examples/ as
PNG, JPG and WebP at 2x through export_file:
cargo test --release -p figcraft-engine --lib export_timings -- --ignored --nocapture

Ryzen, 16 threads, Linux; median of 7 (min):
  PNG   2195 ms (2182) ->  992 ms (989)   -55%
  JPG   2880 ms (2560) -> 1550 ms (1535)  -46%
  WebP  1964 ms (1901) -> 1022 ms (1018)  -48%
  total 7035 ms (6677) -> 3567 ms (3555)  -49%
All 80 exported files (PNG, JPG, WebP, PDF, SVG) are byte-identical.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 17:43:23 +01:00
Jeremy 2893518c84 fix(installer): resolve the wizard scope and folder correctly
Real msiexec runs (per-user and all-users, silent and wizard) found two defects
that the metadata fixtures cannot see:

- "Only for me" published ALLUSERS=2, so FindRelatedProducts treated the run as
  per-machine and skipped the existing per-user copy. An interactive upgrade
  left two registered products and uninstall left files behind. The page now
  leaves ALLUSERS unset for a per-user choice.
- Windows Installer resolves Program Files once, for the context it started in,
  so choosing "Everyone" in the wizard installed into the user profile with a
  machine-wide registration. The wizard now sets the new-install folder itself
  after the choice (Program Files, Program Files (x86) for 32-bit, or the
  per-user Programs folder). Silent installs are unchanged.

check-install-policy.ps1 now fails if either regresses.
2026-10-27 15:32:22 +00:00
aiongg bdb7e84606 fix(ui): dropdown menus grow back after a search with no matches (#1429)
* feat(ui-egui): font family menu keeps its search field and grows back after a filter clears

Repin craft-libs to the shared dropdown (craft-ui), move the font family menu to
craft_ui::combo::Menu with the search in a fixed header, and use craft_ui::ComboBox
for the credits sort. clippy bans egui::ComboBox.

* chore(deps): craft-libs 60d60d48 (dropdown fixes)

* chore(deps): craft-libs 4d7b82f7 (dropdown fixes)

* chore(deps): craft-libs 92d392cf (#387 merged)

* fix(ui-egui): craft-ui on every target (wasm uses the shared ComboBox too)
2026-10-27 10:28:10 -04:00