Commit Graph
128 Commits
Author SHA1 Message Date
bflatastic ebca682595 Merge main into #2224 2026-11-12 11:12:04 +07:00
bflatastic 8081fe7f6f Merge main into #2218 2026-11-12 10:47:16 +07:00
bflatastic a905f50211 Merge main into #2224 2026-11-12 10:11:23 +07:00
BFlat 4531e99e5c Merge pull request #2070 from dotCooCoo/fix/export-color-docs
Export: color profile text states which formats are tagged and that CMYK PDF is DeviceCMYK
2026-11-12 09:56:27 +07:00
BFlat af600507d7 Merge pull request #2101 from TrevorBrown-dev/fix/loaded-font-wins-2092
Text: a font loaded with text.loadFont wins over the built-in and installed faces of its family and style (#2092)
2026-11-12 09:32:02 +07:00
Trevor BrownandClaude Sonnet 5.5 529134b850 M4: a boolean group's corner radius rounds its result (#2222)
A boolean group takes a corner radius (and corner smoothing): the Design panel shows the field for it, `node.set cornerRadius` writes it, and the renderer rounds every sharp corner of the group's result after the fold, convex and concave alike. Layers inside keep their own radius and do not stack with it, a radius larger than the edges allow shrinks to fit while the field keeps the typed value, Flatten bakes the arcs, and a mixed selection reads Mixed. Measured in Figma's web app on 2026-11-10.

The rounded result is cached apart from the fold (keyed by the folded path, radius and smoothing), so scrubbing the radius never redoes the boolean steps. The .fig and REST importers and the .fig exporter now carry the radius and smoothing of a boolean group, and paste properties and radius variables reach it. Docs: ui-parity, target-app-parity, gaps, file-format-parity, ROADMAP, mcp.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.1 designed; Sonnet 5.5 wrote at medium
2026-11-10 13:36:10 -05:00
Trevor BrownandClaude Sonnet 5.5 4201fed27d M9: .jam import: stickies come in as native board stickies (#1808)
On a FigJam whiteboard the mapper builds a sticky with complete generated records as a native board sticky: the saved colour, size, place, opacity and shadows, the note and the author line as text layers, no background vector. It writes a board field on the REST JSON, which the REST importer reads (junk values are ignored with a warning). Damaged stickies and stickies in a .fig keep the generated layers.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Fable 5.1 briefed; Sonnet 5.5 wrote at medium
2026-11-10 09:17:19 -05:00
skylar taylor-barrick 3839fe4a0c Naming: use Developer Mode instead of Figma's Dev Mode (#2135)
Figma holds a US registration for DEV MODE (US 7574348). Display text only: the view.devMode label, Inspect tab, shortcut row, refusal messages, CLI help, docs and all locale catalogs now say Developer Mode. Command ids, preference keys and saved fields are unchanged; "Dev Mode" stays a search alias in the Actions palette. Adds a naming guard test.
2026-11-09 06:24:44 +01:00
Trevor BrownandClaude Opus 5.5 8c24cbf691 Merge upstream/main into fix/loaded-font-wins-2092
Co-Authored-By: Claude Opus 5.5
2026-11-08 12:32:40 -05:00
FredApps d54439be8b M6: scale imported vector point radii with their instance row (#2097) 2026-11-08 16:10:54 +01:00
FredApps a07a21b697 M6: bound exported instance factors to the supported range (#2097) 2026-11-08 16:08:34 +01:00
FredApps 23ed778562 M6: preserve scaled instance export overrides (#2097) 2026-11-08 15:23:36 +01:00
Trevor BrownandClaude Sonnet 5.5 34c44e6063 Text: a font loaded with text.loadFont wins over the built-in and installed faces of the same family and style (#2092)
FigCraft chose the first face added for a family and style, so the embedded Inter 4.x kept Regular, SemiBold and Bold after Inter 3.19 was loaded and only Medium refitted. A loaded font is the counterpart of a font uploaded to the organization, which Figma's help center ranks first ("Manage conflicting fonts"). The test builds its faces from any embedded or installed font (skipped when there is none) and checks a loaded face beats a built-in one, the latest load beats earlier ones, and other styles resolve as before.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Opus 5.5 briefed; Sonnet 5.5 wrote at medium
2026-11-07 22:15:22 -05:00
Trevor BrownandClaude Sonnet 5.5 62d761a7bc M6: .fig import draws a scaled instance's layers at its scale factor (#2091)
An instance scaled with the Scale tool in Figma stores `symbolData.uniformScaleFactor`; the importer never read it, so the layers of a 128 px instance of a 256 px component came in at 256 px with the main's radii, strokes and spacing. Layers of an instance now take the main's lengths times the factor (composed through nested instances), the same values the Scale tool scales, unless an override or derived row gives the value, which is already at the instance's scale. Eight tests: the reproducer, the round trip, a derived size row not scaled twice, override rows, auto layout gap/padding/text, nested composition, factor 1 or none, and junk factors ignored.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Model-Effort: Opus 5.5 drove; Opus 5.5 briefed; Sonnet 5.5 wrote at high
2026-11-07 22:06:35 -05:00
dotCooCoo 12ade9c78c Docs: the 2026-10-31 export entry keeps the JPG tagging it had on that date 2026-11-06 23:31:02 -08:00
dotCooCoo 297426c595 Export: color profile text states which formats are tagged and that CMYK PDF is DeviceCMYK
The export tool and command descriptions, docs/mcp.md and the file-format
table said PDF is always sRGB, that sRGB JPG is untagged and that every
exported file is tagged with its profile. RGB PDF is sRGB and PDF with cmyk
is DeviceCMYK; JPG embeds an ICC profile for sRGB and Display P3; WebP embeds
one for Display P3 only. The JPG quality (1 to 100, default 90) now has one
row in the format table.
2026-11-06 22:21:51 -08:00
bflatastic 5552d3dac5 Export profile test: an sRGB JPG embeds its profile since main's JPG change 2026-11-07 11:26:26 +07:00
bflatastic 02f5a3520a Merge main into #1759 2026-11-07 11:19:38 +07:00
bflatastic 4bb57d57ac Merge main into #1914 2026-11-07 10:57:26 +07:00
Trevor Brown 4b351d9468 Merge remote-tracking branch 'upstream/main' into fix/jam-connector-control-points
# Conflicts:
#	crates/doc/src/lib.rs
#	docs/boards-parity.md
#	docs/file-format-parity.md
2026-11-06 21:55:49 -05:00
bflatastic 98494e4be8 Merge main into #1914 2026-11-07 09:39:45 +07:00
JonRC a55a728c4c M6: JPG exports take a quality and embed the sRGB profile (#2004)
file.export always encoded JPG at quality 90, and an sRGB document's JPGs
carried no colour profile, so viewers could only assume sRGB. PNG exports
always get an sRGB chunk, and Display P3 JPGs already embedded their ICC
profile.

- ExportSetting gets a quality (1 to 100, default 90, saved only when it
  isn't 90), parsed with the other export options: file.export, export.add,
  export.update, file.exportLayers and MCP export take it.
- encode_export_with takes the quality; encode_export keeps quality 90.
  Every JPG now embeds the document's profile, sRGB included.
2026-11-05 00:09:19 -03:00
Trevor BrownandClaude Fable 5.1 31dcd0e5fe M6: a saved connector path going back into its object is routed instead (#1910)
Review: the first and last runs were checked for lying at right angles to their sides, not for
going out of the object, so a path whose first run went up through the start's box (or whose last
arrived from inside the end's) was kept and drawn through it. Both runs now have to go the way the
side faces; one along the side or back into the object leaves the connector to the router. Two more
cases in the routed-here test.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-04 03:46:49 -05:00
Trevor BrownandClaude Fable 5.1 bda7ca910d M6: a FigJam elbow connector saved with its path keeps it on import (#1910)
The `.jam` importer routed every elbowed connector itself, so one drawn around a layer by hand in
FigJam came in routed through it. FigJam saves the path as `connectorControlPoints`: one per run
between the first and the last, each a point on its run (in the connector's parent's coordinates)
with the run's axis; the first run leaves the start's side and the last arrives at the end's. They
become the `ConnectorPath::Runs` a board connector reshaped by hand has, walked by
`figcraft_doc::route_along`, so the path is kept exactly. Points that don't make such a path (off
both axes, two runs the same way in a row, a first or last run sideways to its side, more runs than
a hand could make) leave the connector to the router as before, and only connectors routed here
count in the "own routing" note. `figcraft_doc::attach_points` and `MAX_RUNS` are exported for it.
Docs: file-format-parity.md, boards-parity.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-04 01:52:02 -05:00
Trevor BrownandClaude Fable 5.1 fe18f6d96d M6: .jam import: a connector end with an AUTO or CENTER magnet faces the other end (#1909)
An end whose magnet is AUTO has no fixed point: FigJam picks the side when it draws, and the file
keeps whatever relativePosition the end last had, so the importer sent every automatic end out of
the stale side (the left one in the reporter's file). AUTO and CENTER ends now attach to the side
facing the other end, as a board connector drawn here does, for straight, elbowed and curved
connectors alike; named magnets and ends without one are unchanged.

Test: connectors_with_an_automatic_side_leave_the_side_facing_the_target (the fixture schema gains
the ConnectorMagnet enum). Docs: file-format-parity .jam row, status line and revision history.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-03 09:48:51 -05:00
dotCooCoo 8b3ded373e Merge main into feat/export-color-profile 2026-11-01 17:05:42 -08:00
bd920fc7de M8: .fig import: a routed connector takes its line style from the generated record (#1807) (#1813)
* M8: .fig import: a routed connector takes its line style from the generated record (#1807)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* M8: .fig import: a routed connector keeps its own empty paints and dash pattern

CodeRabbit review on #1813: an explicit empty strokePaints (no stroke) or
dashPattern (solid) on the connector no longer takes the generated record's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-11-01 14:12:31 -05:00
dotCooCoo afc5e13972 Export: each export setting chooses a color profile
Each export setting has a color profile: Same as file (the default, the
document's profile when it exports), sRGB or Display P3. PNG, JPG and
WebP settings list it in their advanced settings, and export.add,
export.update, file.export, file.exportLayers (for every setting) and
the MCP export tool take it as colorProfile. The rendered pixels are
converted to the chosen profile (out-of-gamut colors clip) and the file
is tagged as before: an sRGB chunk for sRGB PNG, the generated ICC
profile (and cICP in PNG) for Display P3; sRGB JPG and WebP stay
untagged. A setting keeps its choice when its format changes, and
settings saved without one read as Same as file.

SVG, PDF and Android XML show no choice and stay sRGB; PDF has no
output intent yet.
2026-10-31 03:07:17 -07:00
BFlat 6b28ef57dc M4: Dash cap setting for Dash and Custom strokes (#1728) 2026-10-31 09:41:39 +07:00
BFlat 838d368712 M6: .fig connector labels without a saved snapshot are kept, centred on their line (#1659)
* M6: .fig connector labels without a saved snapshot are kept, centred on their line

A connector with its own label text but no saved snapshot imports as a frame holding its line (a vector with the connector's stroke and ends) and the label as text centred on the line's middle by length, in the connector's fill paints or black; its background isn't known and is left out, as the import report says. The connector branch of layer_filtered moves into connector_layer. Gap 1.

* Docs: drop a status item the merge listed twice
2026-10-30 21:10:04 +07:00
BFlat 01b07af55a M6: a pasted image the clipboard doesn't carry shows as missing and is named (#1648)
* M6: a pasted image the clipboard doesn't carry shows as missing and is named (#1646)

The placeholder an import stores for an image it lacks is now striped, not flat grey; the fill keeps the image's name; .fig and paste give a fixed, translated note per image; edit.pasteDesignLayers returns missingImages, the paste's message names those layers, and a paste with notes opens the import report.

* M6: the missing-image placeholder's stripes stay clean when scaled, and a .fig gives one note (#1646)
2026-10-30 19:34:24 +07:00
BFlat 4965620d77 M6: pasted layers keep the colours of their shared paint styles (#1647)
* M6: pasted layers keep the colours of their shared paint styles (#1638)

A clipboard links a style by the guid it has in its library, which it doesn't hold, and by its library asset, whose copy it does hold. Mapper::resource_id now takes a guid only when the file holds a node by it, else the asset, so .fig import and paste agree; a link to a style that really isn't there keeps the layer's stored colours, with a note.

* M6: Swedish catalog: keep both new import notes after the merge
2026-10-30 18:54:24 +07:00
BFlat dd2a9fd013 Docs: one copy of each row and status item, and a check that keeps it so (#1658)
* Docs: one copy of each row and status item, and a check that keeps it so

Merge resolutions that kept both sides had left progress-doc rows (the .fig row four times, co-editing, live cursors, Windows, M0.9) and status-line items (302 in all) twice or more. The rows are merged keeping every distinct fact, the newest wording winning (#1649's connector routing included); repeated status items are removed, the first kept. cargo xtask docs, run by ci, fails on a repeated table row or status item.

* Docs: remove the status items the merge of main brought back

Merging main into #1658 rebuilt the status lines from main's copies, which still listed items twice: ROADMAP 2, docs/roadmap.md 5, docs/gaps.md 43 and docs/target-app-parity.md 190, whose unclosed parenthesis also came back and hid them from cargo xtask docs. They are removed again, the first copy kept, and the parenthesis closed; every distinct item is still present against main.
2026-10-30 18:36:00 +07:00
BFlat 9099f1d5ae M6: .fig elbowed and curved connectors without a saved path are routed, not drawn straight (#1649)
The board connector router moves, unchanged, from the engine into figcraft-doc (route_points, RouteEnd) so the .fig importer can use it: an elbowed or curved connector saved without its path runs in right angles or one curve from the side of each layer it joins (the file's magnet, else the edge its point is on, else the side facing the other end). Straight connectors are unchanged. Gap 1.
2026-10-30 17:11:22 +07:00
bflatastic 68450a6a0e Merge main into #1641 2026-10-30 15:57:31 +07:00
BFlat c1451fecd5 M4: an imported file finds an installed family under the other names its fonts give it (#1292) (#1628)
Fonts name a family by its typographic name and by a legacy name per weight; fonts without typographic names are installed under the legacy ones only. FontDb::resolve finds either from the other, and matches names across case, spaces, hyphens and underscores; every lookup (import, paste, REST import, rendering, missing fonts, the font report) uses it. A weight-number style goes by that weight, and .fig and REST imports wait for the installed fonts to be indexed.
2026-10-30 15:44:09 +07:00
Christopher Linnett bc76665c65 M3: colour variables can be calculated from others, faded or mixed per mode (#1280)
A colour variable's value in a mode can now be {"alpha": {"variable", "opacity"}} - that
variable's colour with its alpha replaced - or {"mix": {"from", "to", "t"}}, a per-channel blend
of two colour variables or literals over the stored values (in the document's colour profile),
alpha included. Calculations resolve lazily like aliases, so bound layers, mode switches,
instances and undo follow their sources; they compose, and a loop, or a calculation that reads
more than 32 variables in its collection's modes (other collections in their default modes), is
refused when it is set. Operands are stored by id and read by exact id, as stored references
are. Files keep them; REST, CSS, Penpot and .fig exports write the colour they show in each mode;
deleting a source leaves its readers on the colour they showed; library import and swap
re-point their operands. The Variables panel shows them with their formula and can swap one for
an alias or detach it to its colour.
2026-10-30 16:25:26 +08:00
bflatastic 027a0bbf92 Merge main into #1636 2026-10-30 15:06:33 +07:00
Christopher Linnett 1a454edd09 M8: .fig import budgets four values per compressed canvas byte instead of one (#574)
The .fig import gave a file one decoded value per byte of its compressed canvas (at least 20M, at
most 128M). Dense legitimate files decode past that, further since every scalar field counts, and
the whole import was refused as "not a valid FigCraft file: .fig file: message: too many values".
The budget is now four values per compressed byte; the floor and both ceilings (128M, 32M on 32-bit
targets) are unchanged, so a small file that unpacks past the floor is still refused and no file
decodes more values than the ceiling allowed before. Generated 21 MB and 15 MB canvases (220,044
and 108,012 layers) and a 49.7 MB one budgeted at the ceiling (530,106 layers) now open whole
without warnings; the 49.7 MB one's test peaks at about 9 GB.
2026-10-30 15:09:50 +08:00
bflatastic d0d79fdee5 Merge main into #1616 2026-10-30 13:57:06 +07:00
BFlat c47177498d M8: every file a command reads by path is a regular file within a cap (#1114) (#1625) 2026-10-30 13:48:26 +07:00
Mert Dede 415e83814f Merge remote-tracking branch 'origin/main' into fix/1076-fig-image-budget
# Conflicts:
#	ROADMAP.md
#	docs/file-format-parity.md
#	docs/roadmap.md
2026-10-30 07:08:02 +03:00
BFlat 4224f4b660 M8: deep files import on the browser's 1 MB stack (#1101) (#1600)
The REST import's layer walk (also the .fig import's second stage) took about 4 KB of stack a level, a megabyte at the 256-level cap, and overflowed a 1 MB thread. It now runs on a work list in the same order, fitting groups once their children are in. The .fig mapping stays recursive at about 1.8 KB a level (455 KB at the cap). Both 256-deep imports now run on a 1 MB stack; the cap stays 256 everywhere.
2026-10-30 11:04:57 +07:00
fcde83a5a6 M8: .fig ZIP image reads share a total byte budget (part of #1076)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
Co-Authored-By: GPT-6.1 Terra <noreply@openai.com>
2026-10-30 06:49:05 +03:00
BFlat 7fdbda9f09 M8: One depth cap for a whole render, and render threads with room for it (#1605)
* M8: one depth cap for a whole render; render threads get 8 MiB stacks (#1082)

A pattern fill drew its source as a fresh render from depth 0, so patterns in patterns could stack three renders of 256 levels. The source now goes on from the depth of the layer it fills. A render level takes about 6.5 KB of stack (measured), about 1.7 MB at the cap: the canvas render, preview and save threads and a band pool of their own now get 8 MiB stacks instead of the default 2 MiB.

* M8: SVG and PDF exports carry the depth into pattern sources and embedded renders (#1082)
2026-10-30 10:43:49 +07:00
BFlat e274279587 M8: one unpack budget per .figcraft open (#1079) (#1599)
Each area (document, images, library, versions, branches) keeps its own cap and now also draws from one total per file open, 6 GiB (2 GiB on 32-bit targets), so a file can no longer make the app unpack about 10 GB. Read in that order, the document and the images up to their own cap fit, so a file whose images pass 4 GiB (#1313) keeps as many as before; images left out by the total stay in the file like those past the image cap. What passes the total is skipped (the document refused) with a note.
2026-10-30 09:44:47 +07:00
BFlatandBrandon Thomas 675777e075 M8: .fig instance expansion without quadratic walks and copies (#1077) (#1594)
Variable and collection nodes are left out of the layer tree (variables are read from the node list), so an instance's expansion no longer walks them again for every instance outside the layer cap. A nested instance takes only the override rows addressed below it, found by range, and shares its outer instances' property values and slot contents instead of copying them. Crafted files: 13.8 s -> 21 ms and 40.5 s -> 98 ms; normal files map as before.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-29 22:01:46 -04:00
BFlat 8ee8ba6ce7 M6: FigJam .jam files open, as boards (#1342) (#1586)
* M6: FigJam .jam files open like .fig files (#1342)

A .jam is the same archive as a .fig (its canvas header reads fig-jam.), which the .fig reader already took; File > Open and its filter, drag and drop, file.import and the command line now take .jam too, and the home screen shows its thumbnail. FigJam layers come in as the .fig reader makes them (stickies and shapes with text as frames with their generated layers, other FigJam layers as frames or vectors with a note, unknown types left out with a note). Tests use a synthetic fig-jam. canvas and a .jam made from our own export.

* M12: a FigJam .jam file opens as a board (#1342)

The .fig reader marks a canvas with FigJam's fig-jam. header as FileKind::Board, so a .jam opens with the board toolbar, the dot grid and the board panels and saves as a board; a .fig stays a design file, and importing a .jam into an open file leaves its kind alone. Its objects stay ordinary layers (frames and vectors with the existing import notes). Builds on the .jam opening branch (#1586).
2026-10-30 08:03:56 +07:00
BFlat d890de92c4 M8: SVG import reads color() and color-mix() colours (#1550) (#1566)
The SVG parser reads neither, so fills came in black and strokes were dropped without a note. They are resolved before parsing: color(srgb|display-p3 ...) converted to the document's profile, color-mix(in srgb|oklab|oklch, ...) evaluated as CSS Color 5 defines it. Other spaces are approximated with a note; unreadable calls become a mid grey with a note. Bounded length, nesting and count.
2026-10-30 04:01:07 +07:00
BFlat 2e3d9b6f07 M8: SVG pattern tiles drawn once per scale, within the layer budget (#1080) (#1551) 2026-10-29 23:57:49 +07:00