524 Commits

Author SHA1 Message Date
Elena Torró
7ecf48c52c
⚡ Speed up uploading page shapes to wasm (#12854)
* ⚡ Compute text fonts once per shape when uploading to wasm

* ⚡ Write the shapes batch in place and coerce fills once

* 🐛 Free the upload buffer when a batch record write fails
2026-11-06 15:45:34 +01:00
Elena Torró
afc40a3309
🐛 Refresh wasm heap views after allocations and wasm calls (#12833)
Growing the heap detaches typed views taken before an alloc or a call.
Take them after, so reads and writes never hit a detached buffer.
2026-11-05 18:09:59 +01:00
Elena Torró
8d7e275b6b
🐛 Count id and type in the wasm shape proxy (#12793)
The proxy counted one entry fewer than it yields, so it was never equal to a
plain map with the same entries. Count both fields and test it.
2026-11-04 13:01:05 +01:00
Elena Torró
51cbae6ea5
⚡ Improve memory consumption on shape decoding (#12790)
* ⚡ Avoid decoding the opened page twice for its frame index

The frame index was built over a with-meta copy, so pages got decoded again.
Read it from the page objects themselves and fix the with-meta identity check.

* ⚡ Intern keywords when decoding transit on the frontend

Each shape is decoded on its own, so every shape had its own key keywords.
Cache them in the read handler so all decoded data shares one instance.

* ⚡ Use an array map as the wasm shape proxy delegate

A hash map with ~35 keys costs ~3 KB of nodes per shape on large pages.
An array map holds the same entries in one array, at a small lookup cost.
2026-11-03 17:41:36 +01:00
Alejandro Alonso
deb5771248
✨ Use WASM for SVG markup in code inspect and copy (#12567) (#12608)
When render-wasm is active, generate SVG for the Code inspector and
Copy as SVG from the WASM export path instead of classic SVG codegen.
Add multi-root render_shapes_svg so multi-select stays one document.

Closes #12567
2026-10-27 12:05:30 +01:00
Belén Albeza
2045fc4900
🐛 Keep text image fills in place when pasting (#12552)
A text image fill was always laid over the whole text box, so a pasted
image span showed another part of the picture in a box of another size.

Text image fills now honour the image transform, as shape fills do: the
image covers the transform rect instead of the whole box. Paste gives
each pasted image span a transform that keeps the image at the same
pixel offset from the span, like the gradients.

The SVG export emits one linked image per span transform, rather than
one per image id, and the style panel keeps the transform it reads back.

AI-assisted-by: claude-opus-5-5
2026-10-23 13:00:53 +02:00
Alejandro Alonso
e27c65c77a
✨ Run WASM asset exports locally in the browser (#12515)
When render-wasm is active, shape exports (single file, ZIP, and Export
boards multi-page PDF) run on the shared worker WASM module so the UI
stays responsive. A progressive PDF session (begin/add-page/finish)
updates progress per page; cancel settles on the client. The server
exporter path remains for the SVG renderer.
2026-10-23 11:02:23 +02:00
Belén Albeza
0505eb3b1d
🐛 Fix pasted gradient fills in text editor v3 (#12516)
* 🐛 Keep text gradients in place when pasting at the caret

Text gradients are stored as fractions of the whole text box. Paste
copied those fractions unchanged, so in a box of another size the
handles and colours moved away from the pasted text.

Copy from the v3 editor now records where each span with a gradient or
image fill starts in its text box, and the box size, in the clipboard
payload. A new WASM export, text_editor_get_range_origin, gives the
start of any text range.

Paste at the caret finds where those spans start in the target and
rewrites their gradients so the handles keep the same pixel offset from
the span. Auto-sized texts use the box size they get when the editor
closes.

AI-assisted-by: claude-opus-5-5

* 🐛 Rebuild text gradient shaders when the text box resizes

Text gradient and image shaders are built into the cached Skia
paragraphs against the text box size. A resize with no content change
reused those paragraphs, so the colours stayed matched to the old box
until a reload.

The text now remembers the box size its shaders were built for and lays
itself out again when that size changes and a span has a gradient or
image fill. Text with only solid fills keeps its cache.

AI-assisted-by: claude-opus-5-5

* 🐛 Keep text gradients in place when pasting on the canvas

A text selection pasted on the canvas becomes a new text whose box
differs from the source, so its gradients moved like they did for caret
paste.

After adding the new text, lay it out, find where its gradient spans
start and move their gradients to keep the same pixel offset from the
span, in the same undo step. The range origin export now takes a shape
id, so it works on texts that are not being edited.

AI-assisted-by: claude-opus-5-5

* 🐛 Keep text gradients in place when pasting copied texts at the caret

Text shapes copied whole, rather than as an editor selection, reached the
caret paste without any geometry, so their gradients still moved away from
the pasted text.

A shape copy now records where the gradient and image spans of each
selected text start, and the box size, by shape id. Paste joins them in
the order of the joined paragraphs, with no origin for texts copied
without one.

Copied texts each have their own box, so every span origin now carries
its own box size instead of one box for the whole payload.

AI-assisted-by: claude-opus-5-5
2026-10-22 13:39:26 +02:00
Andrey Antukh
fcf9c27635 Merge remote-tracking branch 'origin/staging' into develop 2026-10-21 10:14:55 +02:00
Elena Torró
a1606c42ff
🐛 Export bitmaps over the GPU surface limit through the exporter (#12455) 2026-10-20 15:37:07 +02:00
Andrey Antukh
fffc3ee222 Merge remote-tracking branch 'origin/staging' into develop 2026-10-19 22:18:27 +02:00
Elena Torró
0801674af2
🐛 Ignore export scale in render-wasm PDF export (#12447) 2026-10-19 18:21:02 +02:00
Andrey Antukh
6f33d8d9d8 Merge remote-tracking branch 'origin/staging' into develop 2026-10-09 19:16:03 +02:00
Elena Torró
4faac8f746
🐛 Fix text decoration selection on v3 editor (#12255) 2026-10-09 15:48:03 +02:00
Miguel de Benito Delgado
5afdeffa5f
♻️ Move frame type constants to webgl.cljs (#11949)
* ♻️ Move some constants out of app.render-wasm.api

- Moved to aapp.render-wasm.wasm where they can be imported
  without pulling half the editor with them
- Renamed one for consistency

* 🔥 Delete unused function, update docstring
2026-10-09 11:22:19 +02:00
Belén Albeza
cb1118f904
🎉 Preserve Penpot's internal formatting when pasting (v3) (#12137)
* 🎉 Copy v3 editor text with its styles in a payload

Copy and cut in the v3 text editor put the styled selection in a
hidden span of text/html. Add its codec and the clean-up for pasting
into another file or team.

Refs #12047

AI-assisted-by: claude-opus-5-5

* 🎉 Paste text copied in Penpot at the v3 editor caret

Text copied in Penpot keeps its styles when pasted at the caret. The
first paragraph keeps the target's attrs; later ones bring their own.
The context menu paste now shares the keyboard paste path.

Refs #12047

AI-assisted-by: claude-opus-5-5

* 🎉 Paste text copied in Penpot on the canvas with its styles

Refs #12047

AI-assisted-by: claude-opus-5-5

* 🎉 Paste copied text shapes at the v3 editor caret with styles

Copied shapes now record their team. Shapes that are not texts paste
nothing into the editor.

Refs #12047

AI-assisted-by: claude-opus-5-5

* ✨ Replace custom fonts in shapes pasted across teams

Refs #12047

AI-assisted-by: claude-opus-5-5
2026-10-07 18:20:03 +02:00
Miguel de Benito Delgado
f252439d84
♻️ Factor GL context initialization out of canvas init (#11931)
* ♻️ Factor GL context initialization out of canvas init

- fixes setting of gl-context-handle to be after _init so an exception
  does not leave invalid state behind.
- api.init-canvas-context uses a new webgl.init-context!
- this decouples config from init, so we can reuse it in benchmarks
- some helpers move to the webgl ns

AI-assisted-by: muse-spark-1.3, deepseek-flash, glm-5.3

* 🐛 Release acquired GL context when renderer init fails

- Properly release resources if init fails
- Rethrown exceptions with the original cause
- Fix vacuous check in clear-canvas

AI-assisted-by: muse-spark-1.3, deepseek-flash, glm-5.3

* ✨ Test webgl context init and release

AI-assisted-by: muse-spark, glm-5.3

* 💄 Address review comments

* 💄 Drop `!` from some function names
2026-10-07 18:19:34 +02:00
Belén Albeza
1d7c775f72
🐛 Fix not setting mimetype in image fills (#12136) 2026-10-07 12:26:45 +02:00
Belén Albeza
7c039231f7
🎉 Implement paste of basic HTML formatting (v3) (#12050)
* ✨ Add rich HTML paste to the v3 text editor

Pasting HTML into the v3 text editor keeps bold, italic, decoration and
text-transform, and drops fonts, colors and links. Gated behind
text-editor-wasm/v1-html-paste.

Refs #10465

AI-assisted-by: claude-opus-5-5

* ✨ Paste HTML on the canvas as a styled text shape

With v3 HTML paste on, pasting HTML on the canvas creates a text shape
with the same emphasis. Clipboard options are now read at paste time.

Refs #10465

AI-assisted-by: claude-opus-5-5
2026-10-02 15:07:12 +02:00
Elena Torró
13a9da3bc3
⚡ Reduce commit cost when dropping large selections (#12032)
Collect children ids into one transient vector and track WASM shape
changes in a JS Map instead of a volatile persistent map.

AI-assisted-by: claude-opus-5-5
2026-10-02 11:22:19 +02:00
Andrey Antukh
c561a0baff Merge remote-tracking branch 'origin/staging' into develop 2026-10-01 10:59:50 +02:00
Alonso Torres
d851f82678
🐛 Fix crashing happening on hot reloads (#12005) 2026-10-01 09:21:05 +02:00
Miguel de Benito Delgado
efbb554af1
♻️ Move use-shape into app.common.render_wasm (#11917)
- Introduces a new ns since there wasn't a suitable one
- Simplifies serialize-shapes-batch! and allows usage outside the frontend
2026-09-28 18:19:55 +02:00
Miguel de Benito Delgado
fa81a3f648
🐛 Refactor batch serialization and fix derived svg-attrs in exporter (#11909)
* ♻️ Share structural batch upload through common helper

- Merge svg-filters and svg-fills to app.common.render-wasm.svg-derived
- Add serialize-shapes-batch! in common, shared by the sync and chunked
  workspace paths
- Add a routing test for the helper and wires the svg-filters test.

AI-assisted-by: muse-spark, GLM 5.3

* 🐛 Derive SVG effects inside single-shape serializer

- Single and batch paths: one svg effect derivation step
  owned by shared serializers.
- set-object forwards the derived shape to its host attrs,
  and the exporter reads the derived fills, so SVG-attr
  fills, blur and shadow render as in the frontend.
- Adds regression test to the exporter.

AI-assisted-by: muse-spark, GLM 5.3, deepseek-flash
2026-09-25 11:48:21 +02:00
Elena Torró
8124ecf148
⚡ Improve canvas snapshot and nested drop shadows calculation (#11766)
* ⚡ Capture canvas snapshot only when the renderer is idle

* ⚡ Skip descendant silhouettes for frames with nested drop shadows

* 🔧 Add a profiling build mode for render-wasm
2026-09-18 09:21:52 +02:00
Alejandro Alonso
87e5ffdf94
🐛 Restart progressive render on canvas background change (#11643)
Cancel Partial/ViewportReady before invalidating tiles so a mid-flight
pass does not leave background-only holes. Closes #11642.
2026-09-11 14:12:57 +02:00
Elena Torró
0913545b41
🐛 Fix letter spacing and position when flattening text to path (#11555)
* 🐛 Fix letter spacing and position when flattening text to path

* ♻️ Simplify text to path conversion and add flatten tests
2026-09-11 08:20:22 +02:00
Andrey Antukh
37dab75e1a Merge remote-tracking branch 'origin/staging' into develop 2026-09-10 20:29:44 +02:00
Alejandro Alonso
ac84557740
🐛 Re-upload WASM text after WebGL context restore (#11589)
During reload-renderer!, reloading? keeps initialized?/ready? false
while set-objects runs (especially the sync path for small files).
Text content used that guard and was skipped; geometry already used
live?. Gate use-shape, has-shape, and set-shape-text-content on
wasm/live? so text is restored with the rest of the shapes.
2026-09-09 17:25:18 +02:00
Alonso Torres
bf2fcba3be
🐛 Fix problems with pixel precission in WASM renderer (#11560) 2026-09-09 15:11:58 +02:00
Alejandro Alonso
2e5157e6aa
✨ Export image fills to WASM SVG as linked images (#11529)
Emit clipped `<image href>` for fills with a registered media URL instead
of Skia base64 embeds. Register URLs from the frontend and exporter via
store_image_url. Cover rect, path, frame, and text (glyph clip) cases in
SVG export tests.

Closes #11383
2026-09-08 17:30:53 +02:00
Elena Torró
d82038a570
🐛 Fix paragraph span restyle on line change (#11503)
* 🐛 Fix paragraph span restyle on line change

* 🐛 Fix text shape boundaries on resize
2026-09-04 13:43:23 +02:00
Elena Torró
0533be100d
🐛 Fix text selection render overlay and double/triple click flash (#11512) 2026-09-04 12:38:31 +02:00
Alejandro Alonso
86aebd3429
⚡ Reduce WASM text selrect churn after batch page load (#11471)
* 🐛 Align WASM auto-width text size with HTML measurement

Include the HTML paragraph-set 1px right margin in Skia auto-width
so WASM and DOM measurements match.

* ⚡ Scope and defer post-font WASM text relayout

Track pending font faces per shape, force-relayout only affected
texts, defer stale-selrect sync until the first tiles paint, and
batch request-render after attr updates. Tolerate missing font-face
keys so callers that omit them do not blow up.
2026-09-04 12:24:37 +02:00
Elena Torró
eb8d3a2fef
🐛 Fix paragraph selection (#11493) 2026-09-03 16:17:38 +02:00
Elena Torró
03e6f119e5
♻️ Clean unnecessary methods (#11472)
* ♻️ Remove duplicated/unused set-children code

* ♻️ Remove unused methods
2026-09-02 12:58:52 +02:00
Alejandro Alonso
b9ddfc1596
⚡ Batch WASM shape upload to speed up page switches (#11443)
Upload structural shape attrs (base, children, blur, shadows,
flex, layout-item) via multi-shape `_set_shapes_batch` FFI in
chunks of 512, then apply host attrs with use-shape selection.
2026-09-02 10:32:52 +02:00
Alejandro Alonso
d1a54134bb
✨ Add text and @font-face to WASM SVG export (#11442)
* ✨ Add text and @font-face to WASM SVG export

Emit selectable <text> elements and inject @font-face rules that
reference the font URLs registered at load time. Text fill bypasses
save_layer wrappers that SkSVGDevice drops. Text strokes are deferred
to a follow-up PR.

* 🐛 Strip trailing commas from Skia SVG text positions

Skia's SVG backend appends a trailing comma to per-glyph x/y lists.
Firefox rejects the malformed attribute and drops glyph positioning,
making exported text vanish or mis-render.
2026-09-01 17:06:50 +02:00
Alejandro Alonso
89a17df403
✨ Add basic WASM SVG export path (#10891)
Introduce a minimal vector SVG exporter via render-wasm: solid fills,
native opacity/blend wrappers, frame clip-content, FFI/frontend wiring,
insta snapshots, and a visual preview gallery. Defer save_layer
workarounds (shadows, blur, masks, special strokes) to follow-up PRs.
2026-08-31 16:06:38 +02:00
Alejandro Alonso
57c0e81616
⚡ Present viewport before interest and clamp paint to atlas (#11313)
Present visible tiles via ViewportReady so zoom settle turns sharp
without waiting on the interest ring, and paint at atlas slot size so
DPR 2 does not rasterize 1024 only to downscale into 512 slots.
2026-08-25 12:56:07 +02:00
Andrey Antukh
81c3b3cd56
📎 Update copyright name on file header (#11346) 2026-08-25 11:55:10 +02:00
Alejandro Alonso
8aefa2ddfd
🐛 Freeze viewport gestures during WASM page transition (#11301)
Pan/zoom via render_from_cache while the tile atlas is still empty
left a blank workspace under the page-transition blur. Ignore
set-view-box / view-interaction-start until tiles-complete, block
pointer events on the viewport SVG, and flush any deferred local
viewport sync when the overlay ends.
2026-08-21 14:19:50 +02:00
Alejandro Alonso
5dab689a6e 🐛 Pack tile atlas and clamp HiDPI surfaces under GPU limits
HQ tiles are 512px and the atlas stays at 4096² (64 full-size
slots). Browser zoom plus a forced ?dpr= can need more visible
tiles than that, and a framebuffer larger than the GPU allows.

Pack interest tiles into smaller atlas cells, blit at 512 then
scale, and inset Linear samples so seams do not bleed. Clamp the
canvas backing store and DPR together, wrap Skia at the real
drawingBuffer size, and wait one frame after DPR changes so CSS
client size and overlays stay aligned.
2026-08-21 11:17:16 +02:00
Alejandro Alonso
2dcf1a8a0a Merge remote-tracking branch 'origin/staging' into develop 2026-08-20 09:15:03 +02:00
Alonso Torres
4ac14cfd08
✨ Add component synchronization to waitForLayoutUpdate (#10964)
* ✨ Add component synchronization to waitForLayoutUpdate

* 🐛 Fix async mock leak in workspace-reflow-test

Use mock/with-mocks instead of with-redefs for http/send! mock in
failed-google-font-css-does-not-abort-shared-consumers test.

with-redefs restores bindings when the block exits synchronously,
but the RxJS subscription fires asynchronously. This caused the mock
to leak into subsequent tests (workspace-media-test), producing 3
spurious failures.

AI-assisted-by: mimo-v2.5-pro

* ♻️ Replace async with-redefs with mock/with-mocks in frontend tests

with-redefs restores bindings when the block exits synchronously,
which is too early for async code (t/async, rx/subs!, promises).
mock/with-mocks uses set! and restores in the done callback, keeping
mocks alive across async boundaries.

Converted 15 with-redefs usages across 4 test files:
- workspace_reflow_test.cljs: 2 genuinely async tests (P1)
- routes_test.cljs: 3 SSO caching tests (P2)
- main_errors_test.cljs: 8 expired-org SSO tests (P2)
- comments_test.cljs: 2 comment thread tests (P2)

36 purely sync with-redefs usages left unchanged — with-redefs
is correct for synchronous code.

AI-assisted-by: mimo-v2.5-pro

* 📎 Fix fmt issues

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-08-18 17:45:42 +02:00
Belén Albeza
be83656d55
🎉 Add caret style changes (text editor v3) (#11171)
* 🐛 Fix editor v3 quitting when changing typography options

* 🎉 Apply text styles to collapsed caret

* 🐛 Fix not persisting the new selrect

* 🐛 Fix selrect not being recomputed on caret style changes

* 🐛 Fix quitting the editor when changing typography on empty texts
2026-08-13 07:23:15 +02:00
Alejandro Alonso
201b51e8c5 Merge remote-tracking branch 'origin/staging' into develop 2026-08-12 15:11:31 +02:00
Belén Albeza
fee416d275
🐛 Fix crash after changing typography options (v2 and v3) (#11221) 2026-08-12 15:10:23 +02:00
Alejandro Alonso
290b14167a
🔧 Allow forcing render-wasm DPR via ?dpr= query param (#11211)
Makes HiDPI repro possible without hardcoding get-dpr or relying on the
real devicePixelRatio (e.g. ?dpr=2).
2026-08-11 17:11:06 +02:00
Elena Torró
38b990ef90
🔧 Add exporter headless backend (#10875)
* ✨ Add headless wasm render backend to the exporter

* ♻️ Move render-wasm bridge to common and split wasm builds

* 🔧 Upload builtin font variants in the wasm exporter

* ♻️ Move shared font and resources utils out of render_wasm

* ⚡ Fetch only the exported roots in the wasm exporter

* ⚡ Bound save_layer rects in the vector export path
2026-08-06 16:13:06 +02:00