Commit graph

2 commits

Author SHA1 Message Date
Danila Poyarkov 759f9eb611
feat: show controls in a prebuilt demo and fix what saving it to .fig lost (#938)
* feat: show controls in the demo and tidy the component panels

The demo gains a Controls page: a switch, checkbox, slider, tabs, text
field, and a button with interaction states, written as Reka-named design
JSX, and a settings card of their instances to try in preview.

Go to main component and Detach instance move into the instance's panel
header as icon buttons. The Behaviour section's "Still needed" line is a
status rather than a button that only moved focus, and the rows it names
are marked instead.

The DOM projection writes flex-start for auto layout's start alignment:
CSS stretches children by default, so a hugging button in a column filled
its card in preview and HTML export. The paint page snapshot catches up
with the frame names #930 draws.

* perf: open the demo as a prebuilt .fig instead of generating it

/demo built every page in the browser and laid them all out in one task,
which froze the page for about six seconds. The demo's content moves to
tools/generate/demo, which builds public/demo.fig headlessly, before Vite
serves or bundles the app and only when its inputs changed; /demo opens
that file like any other, off the main thread and behind the loader. The
document is now lint-clean: small captions are 12 px, and the badge and
disabled button text meet contrast.

Opening the demo from .fig showed three bugs in saving and reading files,
fixed here:
- A layer's blend mode was never written, so Multiply and Screen reopened
  as pass-through.
- Underlines drawn from saved glyphs ran to the text box edge; glyphs now
  keep their advance, read from and written to .fig, and the underline
  ends where the text does.
- A frame laid out on its own because of a saved hug size ignored the
  width its parent stretched it to, so centred content moved to the start.

* feat(design-jsx): write Reka elements in TSX

The Reka element names were only part of the JSX string grammar, so code
that builds trees in TypeScript had to assemble strings by hand, unchecked.
@open-pencil/design-jsx now exports each Reka namespace as typed element
functions (Switch.Root, Slider.Thumb, Tabs.Trigger, …), derived from the
same table the string renderer reads, with props typed as the behaviour
props a root binds. The demo's controls are TSX built with them.

* fix(canvas): draw a component set's editing border at the live zoom

A component set without a stroke got its dashed border in the scene, sized
by dividing by the zoom. The scene is cached as retained pictures and
scaled while navigating, so the border was recorded at one zoom and grew
into thick dashes when zooming in, until the page was redrawn. It is
editor chrome, so it now comes from the overlay pass, which draws every
frame at the live zoom, as slot outlines do. The label cache tracks every
component set on the page for it, and image export no longer includes the
border.

* test: fit the P3 paint page to the canvas size it is captured at

The P3 test zoomed to the paint page while the wide-gamut notice still
took space, then dismissed it, so the first capture fit a shorter canvas
than the second and the two only matched by timing. focusPaintEffects
waits for the canvas to take a window resize, the test focuses again after
the notice goes, and the snapshot shows the demo as it now opens.

* refactor(canvas): fill sections and sets through one helper

Without its own fallback border, a component set fills and strokes its rounded bounds the way a section does; fillRoundedBounds fills them for both.

* test: check the demo controls on their own page and type pixel reads

The demo test built and round-tripped the whole four-page document to
check the controls, which took six seconds on CI and timed out; it now
builds the Controls page alone through the same measured-text setup the
build uses. The underline test reads CanvasKit pixels as any number array,
since readPixels may return floats.

* fix(canvas): measure long text extents without spreading glyphs

Spreading every glyph into Math.min/max overflows the argument limit on long texts.

* fix(demo): keep a tab touched while the demo loads and report a failed load

A file opened or a layer drawn during the fetch keeps the tab; a failed fetch shows the open-file error instead of an unhandled rejection.

* test: check a component set border stays one pixel wide when zoomed
2026-10-07 18:37:05 +00:00
Danila Poyarkov e77eeff11c
fix(fig): write text glyphs from the layout the renderer draws (#928)
* fix(fig): write text glyphs from the layout the renderer draws

Text without saved glyphs was written to .fig with outlines from a
character-by-character fallback: one unwrapped line at y = lineHeight,
no alignment, advances in pixels. Figma lays saved text out from that
data, so every wrapped OpenPencil label opened in Figma on one line, and
since saved glyphs now draw before the paragraph, OpenPencil did the same
after a reopen (#914). The Figma clipboard had a second writer with its
own shaping that matched outlines to characters by index.

One builder in @open-pencil/fig now writes derivedTextData for both. It
keeps glyphs a layer already has and otherwise asks the export runtime to
shape the text. Core shapes with the paragraph the renderer draws, so
lines, alignment, and baselines match, and takes each outline from the
glyph ID CanvasKit chose, so ligatures and contextual forms keep their
shapes. CanvasKit does not say which font drew a run, so outlines are
written only when the run's own font covers it; otherwise the layout is
written without outlines and readers lay the text out themselves.
Advances are in em units and the character offset map has one entry per
character, as in Figma's files.

The reader drops glyphs the earlier fallback wrote, recognised by its
offset map one entry longer than the text with every glyph on the one
written baseline, and any glyph set with a missing outline.

* fix(scene-graph): reflow resized text instead of stretching its glyphs

Resizing scaled a text layer's saved glyphs into the new box. That suits
path text, whose glyphs follow its path, but flat text reflows, and with
saved glyphs drawn before the paragraph a resized Figma text layer was
drawn stretched. Scale glyphs only for path text, including baked path
text that kept only rotated glyphs, and let the width change drop the
rest.

* docs(fig): describe how derived text is written

Figma draws saved text from derivedTextData even when it has the font, so the export docs record which glyphs the shared writer keeps, shapes, or leaves without outlines, and the units it writes. The README names the runtime service by what it now does.

* fix(fig): write text without glyphs when shaping fails

Glyphs are derived data, so a shaper error must not fail the .fig save or Figma clipboard copy that writes them. The builder now writes the layer without glyphs and logs a warning; the clipboard used to swallow the error silently, which hid a font-provider mismatch.
2026-10-06 11:37:02 +00:00