Commit graph

85 commits

Author SHA1 Message Date
Danila Poyarkov e532f616ba
refactor!: move shared primitives below dom-css and core (#771)
* refactor!: move shared primitives below dom-css and core

dom-css depended on core for color conversion, base64 helpers, text
direction, and web-font assets, so core could not use dom-css and every
caller special-cased HTML and Tailwind output.

Color conversion and management, base64 helpers, and text/layout
direction now live in scene-graph under `color`, `bytes`, and
`text-direction`. dom-css takes web-font resolution as an injected
`fonts` option and owns the font face types, so it depends only on
scene-graph and core can depend on it.

BREAKING CHANGE: `@open-pencil/core/color` and `@open-pencil/core/bytes`
are removed, and the direction helpers are no longer exported from
`@open-pencil/core/text`; import them from `@open-pencil/scene-graph`
subpaths. `exportHTMLBundle` takes a font resolver in `fonts` instead of
`'assets'`.

* fix(tools): import color parsing from scene-graph in visual bisect

* fix(mcp): declare the scene-graph dependency

MCP imports `@open-pencil/scene-graph/bytes` since base64 helpers moved
there, but only reached scene-graph through core, so isolated installs
and package checks depended on transitive resolution.

* refactor!: use js-base64 directly instead of a base64 wrapper

Base64 helpers had moved into scene-graph only to sit below dom-css,
but they are a thin wrapper over js-base64 and unrelated to the graph;
fig already called js-base64 directly.

Callers use js-base64 and check `isValid` where input comes from outside
(clipboard, imported HTML, tool arguments, the plugin API). A new
`open-pencil/no-hand-rolled-base64` lint rule rejects atob, btoa, and
Buffer Base64 conversions, and AGENTS.md records the convention.

BREAKING CHANGE: `@open-pencil/core/bytes` is removed; use `js-base64`.

* fix(dom-css): keep images with invalid Base64 inline in HTML export

`exportHTMLBundle` accepts documents parsed from outside HTML, and
js-base64 drops characters it cannot decode, so extracting an invalid
image data URL wrote different bytes. Such images now stay inline.
2026-09-26 11:39:11 +04:00
Danila Poyarkov 2d9e83d58a
refactor(export): derive export format lists from the IO registry (#755)
The Export panel, SDK helpers, app menus, and CLI each kept their own
hand-written format lists, so new formats such as PPTX reached some
surfaces and not others.

Scene Graph now owns the persisted export-setting format ids, Core IO
adapters carry literal ids so that list is checked against real adapters,
and the panel labels, scale handling, app format types, and CLI format
validation/help are derived from the registry. PPTX joins the Export
panel as a result.
2026-09-25 15:42:42 +04:00
Danila Poyarkov 899fecf845
fix: convert document colour profiles, keep text edits live, and fix .fig exports (#716)
* fix(vue): update instance text properties while typing

Instance text property edits only reached the canvas on Enter or blur, so
the canvas and layer tree lagged behind the field. Emit model updates as
the text changes, commit on blur or Enter, and route bursts through the
existing interactive-edit lease and undo batch so rapid edits collapse
into one transaction.

* fix(vue): present the canvas in sRGB to keep P3 blends correct

CanvasKit 0.41 wraps sRGB on-screen surfaces as RGBA8 but every other
color space as RGBA16F, while browser drawing buffers stay RGBA8 when
their color space changes. Requesting DISPLAY_P3 therefore produced
invalid destination copies and broken blends: black rectangles and brown
Overlay fills over Display-P3 documents. Keep presentation in sRGB and
read the buffer back rather than trusting the setter, leaving the
document color space and its stored colors untouched.

* perf(fig): encode glyph path commands without per-coordinate allocation

Glyph outline encoding allocated an ArrayBuffer, DataView, and Uint8Array
for every coordinate and spread each byte into a number array, so recovery
snapshots and text-heavy exports blocked the main thread for 159-167ms.
Size the output once and write through a single DataView; encoded bytes are
unchanged.

* refactor(vue): separate component property edit resolution from batching

The live text path had grown a boolean flag through a single applyValue that
resolved the edit, chose the batch, and mutated instances, which made the
two entry points differ only by that flag.

Resolve an edit once, keep a named batch key, and let setValue and
setTextValue state their own batching policy. Watch the page and selection
sources directly now that selection is replaced by identity, drop the
redundant scene dependencies the useSceneComputed wrapper already tracks,
move the variant option projection next to the swap projection, and resolve
the swap candidate list once per controls pass instead of once per control.

* feat(vue): restore wide-gamut P3 presentation where the renderer supports it

CanvasKit wraps sRGB on-screen surfaces as RGBA_8888 and every other color
space as RGBA_F16, with the pixel format deliberately not exposed, so a
Display-P3 surface only matches the browser buffer when that buffer is
floating point. Chromium 122+ provides drawingBufferStorage for that; this
negotiates the pairing, keeps the sRGB fallback everywhere else, and warns
with the existing dismissible banner when a Display-P3 document cannot be
presented in wide gamut.

Software rasterizers advertise the float extensions but fail an offscreen
framebuffer attach on the first content frame, so they stay on sRGB, as do
WebKit and Firefox, which have no drawingBufferStorage. A new
document:color-space-changed event recreates the surface when a P3 document
arrives after mount, which previously kept whatever surface the first
document created.

* refactor(web): report the canvas presentation instead of re-deriving it

The wide-gamut notice decided availability from a capability probe, which can
disagree with the surface: configurePresentation also falls back when the
float storage install is rejected or the color space setter is ignored. Pass
the surface's actual result through a new onPresentation option, mirror the
document color space into app state, and let the notice read both, so it
appears exactly when a Display-P3 document is really presented in sRGB. That
also removes two editor-event subscriptions and a tab watcher.

Rename SafariBanner to FileApiBanner, since the condition is the File System
Access API rather than Safari, and move the availability check and picker call
into one capability module instead of repeating them at each save site.

* refactor(web): point capability notices at one neutral support reference

The file API notice linked "Use Chrome" to a Chrome download page while
naming Edge as inert text, and the wide-gamut notice offered no browser
guidance at all. Both now link to the caniuse support table for the API that
decides the capability, so the advice is vendor-neutral and stays correct as
versions move.

External link behavior moves into one primitive: SettingsLink and both
notices share it, gaining rel="noopener noreferrer" and the desktop opener
path, which the notices need because the wide-gamut notice also renders in
Tauri where a raw anchor cannot open an external page.

* fix(fig): stop failing .fig export on unencodable OpenType feature tags

The Kiwi schema types toggledOn/OffOTFeatures as its OpenTypeFeature enum,
which has no PNUM, TNUM, LNUM, ONUM, FRAC, SMCP, C2SC, SUPS, or SUBS member.
Features that map to a typed axis were only written when enabled, so a
disabled one fell through to a raw tag, and encoding then rejected it:
`Invalid value "PNUM" for enum "OpenTypeFeature"`. Because save and recovery
snapshots share that export path, any text using those features could not be
written to a `.fig` file at all — the demo's own typography comparison hit it
in every run.

Disabled mapped tags now clear their axis to the schema's neutral NORMAL value,
an enabled tag on the same axis wins over a disabled sibling so "TNUM on, PNUM
off" still means tabular figures, and tags with no Kiwi representation are
dropped instead of poisoning the whole export.

* perf(core): recompute layout only for the pages a component edit affects

Editing a component recomputed layout for the entire graph, which cost tens
of milliseconds per edit in documents with several populated pages. Layout now
runs once per affected page: the pages of the edited subtrees, their
components, and every instance of those components, which may live on another
page.

The layout function is injected so the scoping contract is testable, and the
existing behaviour is kept when no page can be resolved.

* feat(core): follow the document colour profile when painting

Numbers in a document are coordinates in the profile that document declares,
so painting into a surface with a different profile has to convert them.
Nothing did: stored values were handed to the GPU as-is, which is why a
Display-P3 document looked more saturated on a wide-gamut display than on an
sRGB one, and why export labels and stored values disagreed.

Rendering now resolves each colour from the document's profile into the
surface's profile, reporting clipping when a wider profile does not fit, and
OKHCL colours resolve into the requested target instead of being baked to
sRGB. New documents also default to sRGB, matching Figma, so Display P3 is
reserved for documents that declare it rather than being assumed for
everything OpenPencil creates.

* test(canvas): exercise the P3 spec on the paint page

The P3 rendering spec used the demo's reference page and the shared
`selectDemoReferencePage` helper. The paint page covers the same ground —
gradients, shadows, blurs, multiply and screen blends, an alpha mask — and
the helper is going away with the reference page, so this keeps the spec
independent of that demo content.

The card specs now follow whichever page owns the card instead of switching
by page name, which works for either demo layout.
2026-09-18 00:43:10 +03:00
Danila Poyarkov ca17b2ac55 chore: update instance linkage with master 2026-09-13 15:03:02 +03:00
Danila Poyarkov fbacd1a34a fix(clipboard): embed images with Figma-compatible hashes 2026-09-13 13:22:17 +03:00
Danila Poyarkov 449d09542f chore: update clipboard fidelity with master 2026-09-13 13:00:21 +03:00
Danila Poyarkov f8f5f0fc4e fix(instances): preserve ambiguous imported extras 2026-09-11 10:57:18 +03:00
Danila Poyarkov ff1c157a86 refactor(fig): clarify imported instance reconciliation 2026-09-11 10:30:03 +03:00
Danila Poyarkov b436ee4d2b refactor(fig): own Figma clipboard encoding and conversion 2026-09-11 00:36:03 +03:00
Danila Poyarkov 9d4fe4e421
fix(fig): preserve marker-based instance text edits on export (#648) 2026-09-10 19:16:03 +03:00
Danila Poyarkov ec0aacd4f6
build: upgrade oxfmt and format Vue imports (#669)
* build: upgrade oxfmt for Vue import sorting

* style: apply Vue-aware import sorting
2026-09-10 02:45:13 +03:00
Danila Poyarkov e971f735ef fix(fig): preserve explicit text alignment metadata 2026-09-02 22:54:13 +03:00
Danila Poyarkov f2ce4ac9a0 fix(fig): preserve explicit blend modes 2026-09-02 19:38:42 +03:00
Danila Poyarkov 1cc4c252dc fix(fig): preserve implicit fixed text sizing 2026-09-02 00:46:09 +03:00
Danila Poyarkov 9bb9478833
refactor: replace complex conditional object spreads
- Add a typed, modular custom Oxlint rule package with direct regression coverage
- Replace complex conditional object spreads with explicit construction across the repository
- Preserve all existing custom rule registrations and diagnostic behavior
2026-09-01 19:49:57 +03:00
Danila Poyarkov 1e1c38182d refactor(scene-graph): model instance overrides structurally 2026-09-01 11:44:46 +03:00
Danila Poyarkov 4eb9694d58 fix(fig): clean up consolidated instance override fix 2026-09-01 02:01:05 +03:00
Danila Poyarkov fcbd938b80 fix(fig): consolidate instance override propagation 2026-09-01 01:07:19 +03:00
Danila Poyarkov 29aba9e953 fix(fig): restore instance constraint propagation 2026-09-01 01:04:31 +03:00
Danila Poyarkov 7e325e4072 fix(scene-graph): share instance sync fields 2026-09-01 01:04:22 +03:00
Danila Poyarkov 07e4cb2438 Merge remote-tracking branch 'origin/master' into consolidate-pr551-next 2026-09-01 01:02:35 +03:00
Ed-Key 9b48e47329 feat(canvas): render arrow stroke caps on lines and open vectors
ARROW_LINES and ARROW_EQUILATERAL stroke caps were accepted by the schema
and round-tripped from .fig files, but the renderer mapped every cap to
Butt, so imported arrows lost their heads and arrows could not be drawn
locally.

Add a pure arrow-cap geometry module to scene-graph (open-endpoint
collection with per-vertex cap overrides, weight-scaled head geometry)
and draw heads in the stroke color for LINE nodes and open VECTOR
centerline strokes, including the dashed-centerline path. Fold the head
reach into strokeOverflow and the node picture margin so heads are not
cropped by visual bounds, caches, or culling. Round-trip per-vertex
stroke caps through the .fig vector style override table so one-ended
Figma arrows import correctly. Expose both caps in the stroke inspector
cap picker with localized labels, and cover the renderer with an
arrow-stroke-caps Playwright canvas snapshot.

Claude-Session: https://claude.ai/code/session_012zjEbyLSBYyPpJP4fu1XNB
2026-08-31 21:25:38 +03:00
Danila Poyarkov 40b56dab5e fix(fig): guard unresolved component property swaps 2026-08-31 16:50:45 +03:00
Danila Poyarkov b0e9995e4c fix(fig): tolerate empty component property assignments 2026-08-31 16:31:42 +03:00
PAVEL TKACHEV 992e09a5cd fix(fig): open legacy raw fig-kiwi files
- Detect unwrapped fig-kiwi containers before ZIP extraction
- Preserve trailing PNG thumbnail chunks and cover the format with a regression test
2026-08-31 09:01:54 +03:00
Danila Poyarkov 376447a5dd Merge remote-tracking branch 'origin/master' into pr-550-push-integration
# Conflicts:
#	CHANGELOG.md
2026-08-20 17:20:25 +03:00
Danila Poyarkov 9f7ce64bf6
Merge pull request #562 from open-pencil/guide-authoring
feat(editor): author canvas and frame guides from rulers
2026-08-20 09:39:19 +03:00
Victor Wads 51f8464621
Merge remote-tracking branch 'source/master' into victorwads/fast-pages-open-recent
# Conflicts:
#	CHANGELOG.md
#	desktop/src/lib.rs
#	desktop/src/menu.rs
2026-08-19 14:47:08 -03:00
Danila Poyarkov c141663291 fix(editor): address guide authoring review
- Dispose CanvasKit dash effects and render nested frame guides
- Validate imported guide GUIDs and invalidate stale raw guide metadata
- Resolve frame owners through nested hit ancestry and require primary-button ruler drags
- Share guide preview types through a neutral editor module
2026-08-19 20:13:57 +03:00
Danila Poyarkov f892cb583c feat(editor): add ruler guide authoring foundation
- Preserve page and frame guide GUIDs through .fig conversion
- Add undoable guide add, move, transfer, and remove actions
- Start page/frame guide previews from CanvasKit rulers
- Render warm guide previews with frame-scoped solid lines and dotted continuation
2026-08-19 18:08:48 +03:00
Danila Poyarkov 33962b764c fix(fig): harden component property round trips
- Allocate collision-safe property GUIDs across every export path
- Preserve instance-swap component keys and unresolved GUID targets
- Prefer edited assignments over imported raw metadata
- Cover recursive serialization, assignments, and key preservation
2026-08-19 18:05:25 +03:00
Danila Poyarkov 8aeda81f3a Merge remote-tracking branch 'origin/master' into review-pr-548-head 2026-08-19 17:27:47 +03:00
Danila Poyarkov 91e759d7fd feat(editor): add configurable snapping and guides
- Snap vector points, moved layers, and resized edges to geometry, objects, guides, and pixels
- Add persistent snapping preferences with browser and native menu controls
- Normalize canvas and layout guides across Scene Graph and .fig conversion
- Clear transient snapping feedback across interrupted interactions
2026-08-18 19:11:48 +03:00
Victor Wads 6da2f2654b
fix(fig): use Cover pages for thumbnails 2026-08-18 03:43:38 -03:00
xemc ff54a222d8 fix(fig): live edits to instance-descendant fields discarded on save
Editing a field on a node nested inside an INSTANCE (e.g. recoloring an
icon instance's fill) was silently reverted on the next save/reload.
Nothing recorded the edit as an override, and the lazy-population resync
that runs on every export (applySymbolOverrides, propagateResolvedFills)
unconditionally reapplied the stale, originally-imported override data
over the live graph.

Fix records instance-descendant edits via recordInstanceOverride (wired
into the shared FigmaNodeProxy updateNode helper) and has both resync
passes skip fields with a live override via a new hasLiveOverride check.
Also adds serializeFillOverrides so fill overrides are actually written
to the exported symbolOverrides payload (previously only :text overrides
were ever serialized).
2026-08-18 11:22:29 +10:00
xemc 783cdf9dbc fix(fig): component property definitions/references/assignments never survive a save/reload round trip
parseGuidOrNull requires the strict Figma GUID shape (sessionID:localID),
but every OpenPencil code path that creates a component property definition
uses `prop:<hex>` IDs, which never match. applyComponentMetadata silently
dropped componentPropDefs/componentPropRefs/componentPropAssignments/
variantPropSpecs whenever any entry failed that check, so any component
property created by the app (as opposed to imported straight from a real
Figma file) vanished on the very next save.

Mint a stable synthetic GUID for non-Figma-shaped property IDs instead of
dropping them, memoized per export so defs/refs/assignments/variantPropSpecs
pointing at the same property stay consistent. Also fix INSTANCE_SWAP
default/preferred/assignment values, which reference target node IDs and
had the same GUID-shape assumption, and were never being resolved back to
real node IDs on import in the first place (remapComponentIds only handled
instance.componentId, not these fields).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-18 08:10:11 +10:00
Victor Wads 2544b3e4db
perf(fig): show pages before full document decode 2026-08-17 19:05:39 -03:00
rcoenen be272dc4dc
feat(fig): support text on path 2026-08-17 12:00:01 +03:00
Danila Poyarkov b93ffea073
feat: add Figma-style variants and component libraries (#512)
Closes #239
2026-08-14 19:31:41 +03:00
Danila Poyarkov 7d0f9746ee fix(fig): preserve nested text overrides
- Propagate effective text through structurally protected clone chains

- Preserve explicit text overrides and add focused regression coverage
2026-08-12 19:36:37 +03:00
Danila Poyarkov b21509cc8d fix(storage): isolate workspace refresh state
- Discard stale provider refreshes and queued previews

- Guard malformed deflate data and listener failures

- Start periodic refresh without an immediate callback
2026-08-10 22:30:54 +03:00
Danila Poyarkov 4fe27dd1e6 fix(storage): harden workspace previews
- Validate ranged thumbnail payloads and S3 bounds

- Invalidate stale previews and expose loading errors

- Document the public document workspace composable
2026-08-10 20:02:58 +03:00
Danila Poyarkov dde7376bd3 feat(storage): show workspace document previews
- Load embedded Figma thumbnails through bounded S3 byte-range requests

- Add a headless Vue workspace composable with lazy previews and refresh lifecycle

- Cache local previews and refresh the workspace after saves and synchronization
2026-08-10 17:21:36 +03:00
Danila Poyarkov 55b368dc4a fix: complete acronym casing follow-ups
- Cover UI identifiers in the acronym guardrail
- Preserve FIG thumbnail and metadata values through archive parsing
- Use Vue-compatible acronym prop attributes
2026-08-10 16:38:17 +03:00
Danila Poyarkov 7f91594d0c refactor: standardize acronym casing
- Rename first-party API, RPC, JSON, CORS, SVG, JSX, and related identifiers to preserve acronym casing
- Keep upstream and serialized boundary names unchanged
- Add a lint guardrail and migration notes for exported APIs
2026-08-10 15:48:16 +03:00
Danila Poyarkov c493bd0872 fix(fig): preserve vector network origin when resizing
- Scale imported vector vertices around their own geometry origin\n- Cover offset geometry and tangent scaling
2026-08-10 12:52:43 +03:00
Victor Wads 54abfca334
fix(fig): address instance rendering review 2026-08-10 05:09:03 -03:00
Victor Wads bc79d16ac4
fix(fig): preserve instance text edits on save 2026-08-10 04:30:28 -03:00
Victor Wads e159893ec4
fix(fig): preserve component instance rendering 2026-08-10 04:30:06 -03:00
Danila Poyarkov 9829669f85 refactor(fig): name divider position result 2026-08-07 09:14:48 +03:00