Commit Graph
3465 Commits
Author SHA1 Message Date
Trevor BrownandClaude Fable 5.1 b1aa7c2b2a Merge upstream main into fix/control-arrows-pan
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-02 01:23:24 -05:00
Trevor BrownandClaude Fable 5.1 0c18e481eb Merge upstream main into fix/svg-import-stretch-and-dash-offset
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-02 01:23:24 -05:00
Trevor BrownandClaude Fable 5.1 7e6f88c469 Merge upstream main into fix/field-alt-scrub
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-02 01:23:23 -05:00
Trevor BrownandClaude Fable 5.1 3c9d943ff2 Field scrub: the field always senses drags; Alt as the drag begins makes it a scrub until the button is let go
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-02 00:42:22 -05:00
samhyunandClaude Opus 5.5 ed2747e9a4 M8: a stroke's square dash ends and brush shapes paint within its bounds
Three ways a stroke painted past the bounds an export and a redraw go
by, with no effects at all:

- A dash's square end stands out along the path as far as the band
  does across it, so its corners reach √2 times as far from the path.
  Round a curve that is past the box grown by the stroke.
- The ink brush's ragged edges reach 0.54 of the weight from the path,
  not 0.5.
- A scatter brush's copies stray by the weight whatever a width profile
  does, but the profile's scale was applied to the stray as well, so a
  thinning profile left them out.

`outline_reach` now counts square dash ends as √2 at least, and
`Brush::reach_factor` takes the profile's scale for the shapes only and
counts the shapes' reach (`Brush::SHAPE_REACH`), which a test checks
every preset against.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 14:21:44 +09:00
samhyunandClaude Opus 5.5 55821565ac M8: a spread round a stroke with corners keeps its whole shadow
A dashed stroke is cut into dashes with corners wherever they fall round
the outline, and a width profile or a brush can give the stroke corners
of its own. A drop shadow's spread mitres those corners out, but a whole
ellipse or a box still counted as growing by exactly its spread, so the
shadow was cut off at the layer's edge (up to 255 levels).

Such a node now gets the room a star does. Whether the outline's own
mitres end on its box is a check of its own, which the stroke's reach
keeps using, so a node with no spread keeps its bounds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 14:21:33 +09:00
f9b5693621 M2: a text keeps a max height or a number of max lines, not both (#1832)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-11-02 00:02:56 -05:00
8174dd5b74 M4: bulleted lists keep the same bullet at every level of indentation (#1843)
* M4: bulleted lists keep the same bullet at every level of indentation

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

* list_markers: the comment says bullets are the same at every level

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-11-02 00:01:29 -05:00
9c3ec8c1d1 M4: typing 1) and a space starts a numbered list, as 1. does (#1842)
* M4: typing 1) and a space starts a numbered list, as 1. does

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

* Parity: the Text area row names the list creation characters

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

* Parity: the list creation characters without spaces inside the code spans

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-11-02 00:00:19 -05:00
a953a04e33 Shortcuts: Cmd/Ctrl+Shift+Alt+= / - / 0 make the interface larger, smaller and default-sized (#1846)
* Shortcuts: Cmd/Ctrl+Shift+Alt+= / - / 0 make the interface larger, smaller and default-sized

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

* Interface size keys: live while editing text too, and the new size draws on the next frame

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

* A preference set during a frame repaints this window too, so the interface size applies on the next frame

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

* Control channel: a preference changed by a hidden window's command repaints every window

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-11-01 23:59:08 -05:00
f58e022ed7 Shortcuts: Cmd/Ctrl+Alt+arrows rotate a degree at a time, 15 with Shift (#1845)
* Shortcuts: Cmd/Ctrl+Alt+arrows rotate a degree at a time, 15 with Shift

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

* The rotate gesture's label in the English catalog

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

* Rotate keys: about the selection's rotation origin, not while editing points, Dev Mode refusal on the control path too

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-11-01 23:56:06 -05:00
Trevor BrownandClaude Fable 5.1 15bd733886 Number fields: Alt held over the field scrubs it, as the label does
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-01 23:51:07 -05:00
Trevor BrownandClaude Fable 5.1 5050631bcf M5: Hide UI in the presenter: the bars go away and Cmd/Ctrl+\ brings them back (#1820) (#1826)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-01 23:50:03 -05:00
Trevor BrownandClaude Fable 5.1 65bd00f739 M3: Delete on a layer inside an instance hides it instead of being refused (#1834)
* M3: Delete on a layer inside an instance hides it instead of being refused

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

* The components structure test: Delete inside an instance hides

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-01 23:49:58 -05:00
Trevor BrownandClaude Fable 5.1 2abd8c5f52 M2: Remove all auto layout takes it off a frame and every frame nested in it (#1833)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-01 23:49:53 -05:00
Trevor BrownandClaude Fable 5.1 3b8ded69c0 M4: a link on text is underlined by default, and Underline takes it off (#1841)
* M4: a link on text is underlined by default, and Underline takes it off

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

* text.link: only a new link takes the default underline; Underline reads the texts inside a target

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-01 23:49:48 -05:00
Trevor BrownandClaude Fable 5.1 fdd27a0b88 Shortcuts: the Layers panel opens with Ctrl+1 as well as Alt+1 on Windows and Linux (#1844)
* Shortcuts: the Layers panel opens with Ctrl+1 as well as Alt+1 on Windows and Linux

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

* Parity note: in the browser Ctrl+1 is the browser's tab key

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

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-01 23:49:43 -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
NetterSilverandClaude Haiku 5.5 9a1b33bbfd Reject signed major in plugin manifest api version string (#1853)
Found while testing crates/plugin/src/manifest.rs:70 (api_from_number_or_string, the Api::Text arm)

Co-authored-by: Claude Haiku 5.5 <noreply@anthropic.com>
2026-11-01 23:49:26 -05:00
Trevor BrownandClaude Fable 5.1 ecd900ab37 Control channel: ui.key arrows pan the view when nothing is selected, as the window does
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-01 23:47:48 -05:00
dotCooCoo 8b3ded373e Merge main into feat/export-color-profile 2026-11-01 17:05:42 -08:00
Cédric Legrand cda326e24e fix(vectordrawable): M2.1: use inclusive range checks 2026-11-01 20:23:24 +01: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
김삼현andClaude Opus 5.5 264bcb1c03 M8: an arc's spread shadow and mitred stroke get room for their corners (#1802)
* M8: an arc's spread shadow gets room for its mitred corners

Rectangles, frames and ellipses grow by exactly their spread, so their
layers only make room for the spread itself; other shapes make room for
the mitre at sharp corners (up to four times the spread). An ellipse
with arc data (a pie slice, or a ring cut short) counted as an ellipse,
but its ends meet its sides in sharp corners: a spread of 25 on a slice
of 0.3 rad cut its shadow off at the layer's edge (up to 255 levels).

Only an ellipse that goes all the way round now grows by exactly its
spread. `figcraft_geom::full_sweep` is what `ellipse` already used to
tell a whole ellipse or ring from an arc, so the two agree.

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

* M8: an arc's mitred stroke gets room for its corners too

Review: `outline_reach` counted every ellipse as an outline whose stroke
mitres end on the grown box, the same mistake as the spread. A thin
slice with a 20-unit mitred stroke and a spread of 10 still had its
shadow cut off (255 levels). It now asks `grows_by_exactly_its_spread`,
which picks the same shapes and leaves arcs out.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 14:12:27 -05:00
김삼현andClaude Opus 5.5 6b2e464361 M8: a scaled node's spread shadow keeps its room in SVG (#1803)
The SVG export blurs a spread drop shadow in a filter whose region is
the silhouette's page bounds grown by the spread's mitre and the blur.
The spread grows the outline in the node's own units, though, and the
transform then scales it: on a star scaled three times, a spread of 10
mitred out to some 97 units while the region only made room for 48, so
SVG viewers cut the shadow's points off. The spread now grows the region
as far across and down as the transform takes a local step of that
length, as the paint bounds already do.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 14:12:23 -05:00
Trevor BrownandClaude Fable 5.1 83c28cf3ae M6: SVG import: a stretched image stays stretched (#1811)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-01 14:12:18 -05:00
Cédric Legrand c41471d20b build(pathops): M2.1: inherit workspace Rust version 2026-11-01 19:54:54 +01:00
Cédric Legrand e7a599baea style(text): M2.1: clarify font filter precedence 2026-11-01 19:42:52 +01:00
Trevor BrownandClaude Fable 5.1 8bc405edbc SVG import: the dash offset fix alone, with open-path, mixed-subpath and text tests
The stretched-image fix moved to its own PR.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-01 12:16:33 -05:00
Trevor BrownandClaude Fable 5.1 4e22873b44 Page opacity test: a scrub on the field is live and one undo step
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-01 12:01:33 -05:00
Trevor BrownandClaude Fable 5.1 d72f096186 M8: color-mix(): a none in color() is a missing component too
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-01 11:54:35 -05:00
mortezanaeemi-ops ef52759ebc Add files via upload 2026-11-01 16:17:46 +03:30
mortezanaeemi-ops 7f4b295983 Add files via upload 2026-11-01 16:17:10 +03:30
mortezanaeemi-ops c411152b87 Add files via upload 2026-11-01 15:34:41 +03:30
mortezanaeemi-ops 5d4a5082ec Add files via upload 2026-11-01 14:54:48 +03:30
mortezanaeemi-ops 6969232326 Add files via upload 2026-11-01 13:53:11 +03:30
samhyunandClaude Opus 5.5 8c93d0559f M8: dots quantize their position apart from the view's origin
Review of #1796: quantizing the mark's distance from the view's corner
took in the error of the view's origin converted to pixels, so a mark
one 1/65536 px step short of a half could still cross it. At a step of
24 and a zoom of (75.5 - 2^-17) / 24, a pan by 51 pixels moved the
first mark. The mark's position and the origin are now each quantized
before the one is taken from the other.

The origin rounds its halves down: some 4 x 10^10 pixels out, its error
is exactly half a step, and rounded up it moved the marks again. Past
2^37 pixels the step is finer than f64 and values are used as they
are, so huge ones no longer overflow to infinity and come out as a
mark at the view's corner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 19:22:29 +09:00
mortezanaeemi-ops a5ebc01d2c Add files via upload 2026-11-01 13:50:48 +03:30
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
samhyunandClaude Opus 5.5 12b0bcddcb M8: a test for texture's whole-pixel moves in wide layers
Review of #1795: the tests checked the rounding of grain cells but not
the move texture now adds in whole pixels. Rounded cells with the old
`f32` move still passed them. A pan across a 2,400-pixel-wide striped
layer catches it: past 1,024 pixels, the old move came out a pixel off
for some pixels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 18:47:09 +09:00
samhyunandClaude Opus 5.5 c8f34acc59 M8: dots on the 1/65536 px rounding step stay put too
Review of #1796: rounding a mark's position to 1/65536 px with `round`
sent the halves of that step away from zero, the asymmetry the PR set
out to remove, one step further down. At a step of 25.5 - 2^-17 px, a
pan by 26 pixels put the first mark a pixel off a full render. Both
roundings now go half up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 18:40:05 +09: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
Max MittelstädtandGPT-5.6-Luna a46e2369bc Preserve finite values at large rounding precision
Found while testing crates/geom/src/transform.rs:59

Co-Authored-By: GPT-5.6-Luna <noreply@openai.com>
Co-Authored-By: GPT-6-Luna <noreply@openai.com>
2026-11-01 09:31:20 +00:00
samhyunandClaude Opus 5.5 e39e563408 M8: dots halfway between pixels stay put across pans, bands and patches
The board's dot grid puts each mark on the device pixel nearest its
page position, measured from the view's top-left. At a zoom of 1.0625 a
step of 24 is 25.5 pixels, so every second mark falls halfway between
two pixels, and a view a whole number of pixels on could put it on the
other one: `round` sends halves away from zero, so a mark left of the
view's origin went left and one right of it went right, and the origin
`(px + offset) / scale` reached the half with another rounding error.
A pan moved those marks by a pixel (up to 123 levels off).

Positions are now rounded to 1/65536 px and then half up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 18:21:00 +09:00
samhyunandClaude Opus 5.5 058cc24bef M8: grains stay in their cells whatever origin a render starts from
Noise and texture lay their grain cells out in the layer's own
coordinates, so a patch, a band or a pan should draw the grains a full
render draws. At a zoom of 1.7, a 1-unit cell is 1.7 pixels across and
every tenth edge falls on a row of pixel centres; a render whose origin
is a whole number of pixels off reached that edge with another rounding
error and moved the row into the next cell. An edit over a textured
layer patched up to 197 levels off a full render, and a noisy one 224.

Cell positions are now rounded to 1/2^20 of a cell before they're cut
into cells, and texture adds its scatter to the pixel index in whole
pixels instead of flooring it in f32 together with the index.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 18:06:56 +09:00
Trevor BrownandClaude Fable 5.1 8b6c92796e SVG color-mix(): every missing component is carried over, through nested mixes too
Review follow-up on #1773: the carry-forward now covers red, green and blue in an sRGB mix (converted first in another space, so zero there, per CSS Color 5 section 4.1 on analogous components) as well as the alpha in every mix, and a component missing in both colours stays missing in the result, so an enclosing mix carries it from its other colour.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-01 00:39:38 -04:00
Trevor BrownandClaude Fable 5.1 16d97cf8e9 M6: SVG import: a stretched image stays stretched, and a dash offset on a closed outline is kept
Found while testing crates/svg import (no existing issue), each with a test that fails before and passes after. An <image preserveAspectRatio="none"> took ImageScaleMode::Fill, which keeps the picture aspect and crops; a box of a different aspect now takes a whole-picture Crop, which the renderer draws as a stretch (SVG 2 section 8.6). A stroke-dashoffset equal to half the first dash was skipped on every shape because an open line starts half a dash in by itself; a closed outline runs its pattern from its start, so the exemption now applies to open outlines only (SVG 2 section 13.5.6).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-01 00:33:58 -04:00
Trevor BrownandClaude Fable 5.1 16b13919aa Page hex field: an 8-digit hex asking for opaque sets 100%
Review follow-up on #1770: the typed colour keeps the page opacity only when the text carries no alpha (not 4 or 8 hex digits); FF0000FF on a translucent page now sets 100%. Test extended.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-01 00:20:29 -04:00
Trevor BrownandClaude Fable 5.1 12c9acbf0c events test: the stamp is within a minute of a second clock read, not ordered against it
Review follow-up on #1766: a backward wall-clock step between the push and the test read made the ordering assertion flaky.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-01 00:17:54 -04:00