Commit Graph
326 Commits
Author SHA1 Message Date
BFlat 00e7b3c9d9 Merge pull request #1800 from TheGalitube/fix/h7861-reject-overflowing-32-bit-varint
Reject overflowing u32 varints
2026-11-07 12:00:12 +08:00
bflatastic dbc9bf7236 Merge main into #1987 2026-11-07 10:34:16 +07:00
BFlat 7dbb439ed2 Merge pull request #1937 from storytold/fix/sec-format-charge-failed-reads
fix(format): charge an archive member's unpacking even when the read fails
2026-11-07 11:29:14 +08:00
Trevor BrownandClaude Fable 5.1 0257f53bb9 Test: a connector saved with its path starts 4 pt outside its sides, as main's connectors do
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-06 21:55:50 -05: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
BFlat b20860829c Merge pull request #1983 from storytold/fix/sec-rest-variable-fill
fix(format): cap the bytes copied to fill a variable's missing modes
2026-11-07 10:43:14 +08:00
BFlat 2e7cafe662 Merge pull request #1876 from TrevorBrown-dev/feat/text-style-paragraph
M3: text styles carry paragraph spacing and indent, the list style and the OpenType features (#1829)
2026-11-07 10:29:27 +08:00
BFlat e9fc975aa6 Merge pull request #1756 from dotCooCoo/feat/vector-network-corner-smoothing
M4: closed vectors take corner smoothing
2026-11-07 10:27:00 +08:00
BFlat fa0698d6cb Merge pull request #1984 from storytold/fix/sec-sketch-run-work
fix(format): bound the characters Sketch text runs write in all
2026-11-07 10:23:58 +08:00
BFlat 9e83091a14 Merge pull request #1798 from TheGalitube/fix/h7855-duplicate-kiwi-field-ids-silentl
Reject duplicate Kiwi field IDs
2026-11-07 10:11:22 +08:00
bflatastic 3ebd14ad2d Merge main into #1876 2026-11-07 09:10:47 +07:00
BFlat 47b5979ef0 Merge pull request #2012 from TrevorBrown-dev/feat/text-direction-1830
M4: a paragraph's base direction can be forced: Text ▸ Text direction ▸ Left to right / Right to left (#1830)
2026-11-07 09:58:59 +08:00
bflatastic acbc988225 Schema test: its field ids no longer repeat, now that repeats are refused 2026-11-07 08:48:40 +07:00
bflatastic dfa6b858c8 Merge remote-tracking branch 'origin/main' into landb/1798 2026-11-07 08:45:50 +07:00
BFlat cbe6c414b8 Merge pull request #2064 from storytold/carry/1932
perf(format): index the lookups that made style-heavy files quadratic
2026-11-07 09:43:25 +08:00
Trevor BrownandClaude Fable 5.1 1ba0e1d62a Merge upstream/main into feat/text-style-paragraph
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-06 20:40:40 -05:00
Trevor BrownandClaude Fable 5.1 5a63c08a78 Merge upstream/main into feat/text-direction-1830
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-06 20:40:21 -05:00
BFlat 83054f771d Merge pull request #1971 from TrevorBrown-dev/fix/connector-end-gap
Boards: connectors attached to a side start 4 pt outside the border, as FigJam's do (#1916)
2026-11-07 09:37:24 +08:00
bflatastic 7eac937acc Style-heavy files: a repeated name keeps its first place, and one more lookup uses the id set 2026-11-07 08:24:02 +07:00
bflatastic 03aa619fba Merge main into #1932 2026-11-07 08:24:02 +07:00
bflatastic 638ae9bd29 Merge main into #1895 2026-11-07 08:20:40 +07:00
Trevor BrownandClaude Fable 5.1 2e331ecd70 Merge upstream/main into feat/text-style-paragraph
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 16:45:00 -05:00
Trevor BrownandClaude Fable 5.1 7c6bc49e9f Merge upstream/main into feat/text-direction-1830
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 16:41:34 -05:00
8da11395e9 M3: glass effects take variables, and their frost stays within 0–100 (#1886)
* M3: glass effects take variables, and their frost stays within 0–100 (#1885)

Glass's light angle, intensity, refraction, depth, dispersion, frost and
splay bind to number variables, on a layer (`variable.bind`,
`effects/<i>/<field>`, `radius` being the frost) and in an effect style
(`style.bind`), and the Design panel's glass fields take a variable like
the shadow and blur fields. A variable reads as the field shows it:
intensity, refraction, dispersion and splay in percent.

A glass's frost stays within 0–100, the reference tool's range: a larger
frost from an older file draws at 100, and the field, a bound variable and
scaling stop there.

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

* M3: a frost from an older file scales and shows as it draws (#1885)

Review follow-up: a frost saved past 100 draws at 100, so scaling a layer
starts from that (150 halved draws at 50, not 75) and the Design panel's
Frost field shows it. The revision row's binding path escapes its pipes, so
the table keeps three columns.

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

* M3: Dev Mode and .fig export show a frost from an older file as it draws (#1885)

A frost saved before the 0–100 range draws at 100, and the canvas, CSS and
scaling already read it that way, but Dev Mode's Glass row and the .fig
export still read the stored number, so they said 150 where the canvas
shows 100. Both now read the sanitized frost.

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

---------

Co-authored-by: Oleksii Sukholita <oleksii.sukholita@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-05 22:08:15 +01:00
Trevor BrownandClaude Fable 5.1 c16c3676c2 M4: text direction: CSS keeps Left beside a forced rtl, paragraph arrays read to the paragraph count, the empty last paragraph keeps its direction (#1830)
From review: `text-align: left` beside `direction: rtl` (Left is absolute on the canvas); the REST import reads the four parallel paragraph arrays up to the paragraph count, not the 256-entry list limit; `rtl_para` gets an entry for the position after the last char so an empty last paragraph keeps its forced direction; the localization status line.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 03:46:28 -05:00
Trevor BrownandClaude Fable 5.1 cd5c9aeb09 M4: a paragraph's base direction can be forced: Text ▸ Text direction (#1830)
Direction came from the letters only; Add right-to-left text says controls override it per paragraph (Text > Text direction, Left to right or Right to left). `TextDirection` on `ParaList`, `Bidi::of` with a base level per paragraph, `text.direction` and the three menu commands (the edited paragraphs while editing), `node.set textDirection`, the Type settings control once a right-to-left script is in the text, inspect `textDirection` / `lineDirections`, the CSS `direction`, and the `.fig` lines' `sourceDirectionality` in and out.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-05 03:23:07 -05: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
Dylan M. Taylor deee3d8acf fix(format): bound the characters Sketch text runs write in all 2026-11-04 04:50:11 +00:00
Dylan M. Taylor ddb8bfa665 fix(format): cap the bytes copied to fill a variable's missing modes 2026-11-04 04:44:24 +00:00
Dylan M. Taylor 519ffd4987 fix(format): import a Sketch page once however often the file lists it (#1973) 2026-11-03 23:01:19 -05:00
Dylan M. Taylor 88851996a0 fix(format): a saved file replaces its predecessor with the same permissions (#1957) 2026-11-03 22:22:03 -05:00
Trevor BrownandClaude Fable 5.1 2dc7bed2a0 M12: connectors attached to a side start 4 pt outside the border (#1916)
A connector end attached to a side was routed from the middle of that side, so the stroke's
centre line started on the border and half its width plus the round cap lay on the fill, and
several connectors leaving one side piled up into a knot sticking into the card. FigJam starts
the line a few pixels outside the border and connectors leaving one side run together along the
side's normal before they fan out (the issue's screenshots). `board_route::END_GAP` (4 pt): an end
attached to a side (`RouteEnd::Box`) starts that far outside it along the side's normal; an end
placed by hand on the object (`RouteEnd::On`) keeps its exact point; a free end is unchanged.
The pinned route coordinates in the engine, format and UI tests move with it; the overlay's
attach points stay on the border. Docs: the Connectors rows, status line and history of
boards-parity.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-03 21:08:19 -05:00
Dylan M. Taylor 1206c0156e fix(format): charge an archive member's unpacking even when the read fails
The shared unpacking total (and Sketch's) was debited only when a member
read succeeded, so a snapshot or page member damaged near its end could be
named again and again by an index and unpacked each time without ever
spending the budget. A failed read is now charged its recorded size; one
refused for that size before any unpacking still costs nothing.
2026-11-03 14:41:56 +00:00
Dylan M. Taylor 37803cb1c8 perf(format): index the lookups that made style-heavy files quadratic
REST import searched every style imported so far for each layer's style
reference; .fig export compared each text span's style with every distinct
style before it; Penpot token-set ordering searched the whole order array
for every comparison of the sort. Each now looks up a set, a map or a
position table built once.
2026-11-03 14:30:05 +00:00
Dylan M. Taylor 35684eaa02 fix(format): index kiwi field lookups and bound schema and empty-array growth (#1896)
A .fig message looked up each tag's field with a linear scan of its
definition, so a wide schema and many tags cost their product. Schema
decoding capped fields per definition but not in total, and inflated the
schema chunk under the 512 MiB message cap. Empty non-byte arrays spent
nothing from the decoded-value budget while still growing the output.

Field ids (and enum values) are now found through a per-definition map
built on first use, keeping first-match semantics. A schema holds at most
200 000 fields together and its chunk may inflate to 16 MiB. Every array
field spends one value for itself before its elements.
2026-11-03 06:09:08 -05:00
c69eb55f31 M8: an ellipse whose start has gone round many turns keeps its sweep in .fig (#1891)
* M8: an ellipse whose start has gone round many turns keeps its sweep in .fig

The importer clamped the starting and ending angles to ±1000 rad each
before taking the sweep, so an arc starting near or past 1000 rad lost
its sweep: a whole ellipse starting at 999 came back sweeping 1 rad, one
starting at 1500 swept nothing. The sweep is now the difference of the
angles as written, and a start past a turn is brought into [0, 2π) the
way the renderer's sine and cosine see it. The .fig export writes the
start within a turn too, where a 32-bit angle still holds it and the
sweep.

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

* M8: an arc far from zero keeps its sweep rather than closing into a whole ellipse

Review follow-up on #1891. The whole-turn tolerance grows with the
angles, so once a start can lie far from zero it swallowed real arcs: an
arc of 6.1 rad written at 1e6 rad read as a whole ellipse, and at 3e7 rad
even a sweep of nothing did. The tolerance now only applies while it is
at most 1e-3 rad (about 0.06°), which holds up to a start of about 4,000
rad; further out the sweep is kept as written.

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

* M8: an arc with no end stays whole however far out it starts

Review follow-up on #1891. With no endingAngle the importer took the end
as start + 2π and the sweep as the difference, which far from zero
rounds away from a turn: from a start of 1e15 it came out 6.25, drawn as
an arc with its radius edges now that the snap to a whole turn is
bounded. No end now means a sweep of exactly 2π.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-11-03 00:13:17 -05:00
Dylan M. TaylorandBrandon Thomas 0eec582805 perf(format): drop dangling Penpot token aliases with a worklist (#1897)
drop_dangling_aliases rebuilt the id set and rescanned every token each
round, and an alias chain lost one link per round: quadratic in the
chain. Dependencies are now indexed once and invalidation propagates
through a queue, counting how many tokens make each variable id, so each
token is visited a bounded number of times with the same result.

Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-11-03 00:13:07 -05:00
Dylan M. Taylor 19afdd21f9 fix(format): import a .fig blob once however many hashes name it
Distinct image hashes pointing at one embedded blob shared its copy, but
the import resolved each hash separately: the blob was copied out and
hashed again for every alias. Aliases without an images/ member of their
own are now renamed to the first hash naming the blob before layers are
built, so the blob is copied and hashed once.

Image paints also spelled out their whole hash as hex for every layer,
whatever its length; hashes longer than any image can be named by (64
bytes) now resolve to nothing, as a missing hash does.
2026-11-02 20:44:37 -05:00
Trevor BrownandClaude Fable 5.1 695ca07528 M3: text styles carry paragraph spacing and indent, the list style and the OpenType features (#1829)
ParagraphStyle beside TextStyle in StyleKind::Text: style.create takes it from the layer or a value, style.apply sets it (the list on the paragraphs touched for a range), style.update moves it, editing it unlinks, .fig files keep it with the style; tests in styles_behaviour_tests and fig_export_assets_tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-02 06:43:02 -05:00
김삼현andClaude Opus 5.5 702b0d7894 M8: a whole ellipse drawn backwards keeps its simple stroke in SVG (#1848)
* M8: a whole ellipse drawn backwards keeps its simple stroke in SVG

An ellipse with a sweep of -2π goes all the way round, as the renderer
and the .fig export already read it. The SVG export's simple stroke
checked `sweep >= 2π` only, so such an ellipse's inside or outside
stroke was written as a filled even-odd ring instead of a stroke.

All three places now ask `figcraft_geom::full_sweep`, as the geometry
and the bounds do.

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

* M8: a backwards whole ellipse's stroke ring in the renderer and .fig export, tested

Review follow-up (#1848): the renderer's stroke ring and the .fig
export's stroke geometry read a sweep of -2π as whole too, now through
`full_sweep`. A test for each checks a backwards ellipse strokes the
same ring as a forward one, and fails if either goes back to checking
positive sweeps only.

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

* M8: the .fig backwards-ellipse test compares the stroke ring itself

Review follow-up (#1848): the test checked only that one stroke path was
written. It now compares the path's commands and winding rule for the
forward and backwards ellipse, and that the ring is even-odd.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 23:49:38 -05:00
김삼현andClaude Opus 5.5 fa8faf216d M8: a whole ellipse starting anywhere stays whole through a .fig file (#1851)
A .fig file (and the REST JSON) holds an ellipse's start and end angles
as 32-bit floats, and FigCraft writes them that way too. For a whole
turn that starts anywhere but zero, end - start comes out a hair short
of 2π as often as not (start 0.3: 6.283185184), which reads as an arc:
the ellipse came back as a pie slice whose centred stroke draws a line
from the centre to the edge.

On import, a sweep within the angles' precision of a turn is now a turn.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 23:49:34 -05: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
Max MittelstädtandGPT-5.6-Luna 3f7f5f38ce Reject overflowing u32 varints
Found while testing crates/format/src/fig/kiwi.rs:190

Co-Authored-By: GPT-5.6-Luna <noreply@openai.com>
Co-Authored-By: GPT-6-Luna <noreply@openai.com>
2026-11-01 10:09:26 +00:00
Max MittelstädtandGPT-5.6-Luna f797cbe029 Reject duplicate Kiwi field IDs
Found while testing crates/format/src/fig/kiwi.rs:615

Co-Authored-By: GPT-5.6-Luna <noreply@openai.com>
Co-Authored-By: GPT-6-Luna <noreply@openai.com>
2026-11-01 09:39:49 +00:00
dotCooCoo 099cc03754 Vector: simplify, offset and Penpot export keep a closed vector's smoothed corners
vector.simplify and vector.offset rebuild a closed vector from its rounded
outline with the layer's corner smoothing, as the canvas draws it. A vector
whose regions have paints of their own was rebuilt from its sharp points
before; it now keeps its rounded corners. The Penpot export writes a stroked
vector's path with its smoothed corners.
2026-10-31 03:21:43 -07:00
dotCooCoo 000d39b042 M4: closed vectors take corner smoothing
A vector's corner smoothing was stored, imported and exported, but its
rounded points were drawn as plain circular arcs, and the Design panel
hid the Corner smoothing control for vectors.

A vector with a closed region now draws its rounded points with the
layer's corner smoothing, on the canvas, in hit testing, in PNG, SVG and
PDF exports, and in the fill and stroke geometry of .fig exports. Each
point keeps its own radius; a point without one stays sharp. The corner
uses the profile of smoothed polygon corners, and smoothing is reduced
before the radius when a corner does not fit in half of each segment.

FigCraft's own rules for cases the help page does not describe: only a
point between two straight segments that both bound a closed region is
smoothed; points on open branches and points next to a curved segment
keep the plain arc; concave corners are smoothed like convex ones.

The Design panel shows Corner smoothing and its iOS (60%) button for
vectors with a closed region, and not for open paths.
2026-10-31 02:33:38 -07:00
BFlat 12e9b409ed M12: a connector end on any part of an object with Cmd/Ctrl (#1733) 2026-10-31 10:27:56 +07:00
BFlat 760aece238 M5: an Open link action can open in place of the prototype, with Open in new tab off (#1685) 2026-10-31 02:37:54 +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