883 Commits

Author SHA1 Message Date
Elena Torró
b48d163f5f
⚡ Fix slow drop shadow spread in CPU exports (#12718)
* ⚡ Fix slow drop shadow spread in CPU exports

* 🐛 Fix text opacity in board drop shadows on CPU exports
2026-10-29 18:27:19 +01:00
Alejandro Alonso
6508449fa0
🐛 Rebuild text layout when image fills finish loading (#12564)
Text image shaders are baked into Skia paragraphs at layout time.
touch_shapes_with_image only scanned shape.fills/strokes, so text
spans were never invalidated after store_image. Detect text content
image fills and force a layout rebuild before touching tiles.

Closes #12563
2026-10-23 09:55:23 +02:00
Elena Torró
a1606c42ff
🐛 Export bitmaps over the GPU surface limit through the exporter (#12455) 2026-10-20 15:37:07 +02:00
Belén Albeza
ef68304cb8
🐛 Draw masked group inner shadows in PDF and multi-element export (#12468)
* 🐛 Fix render_to_pdf call in the ToUnicode test

The test passed a scale argument that render_to_pdf does not take, so
the render-wasm test build failed to compile.


* 🐛 Draw masked group inner shadows in PDF and multi-element export

The CPU renderer behind PDF and multi-element raster export drew a
masked group's drop shadow but never its inner shadow, so the masked
content came out flat. Wrap the masked content in a layer that draws
the inner shadows over it, as the GPU single export does.


* 🐛 Export every visible inner shadow of a masked group

The workspace skips shadows too small to see at the current zoom, to
render faster. Export has no such need, so draw every visible inner
shadow instead of filtering by scale.
2026-10-20 15:04:54 +02:00
Alejandro Alonso
c0450a77c3
🐛 Limit board guide hit testing to the board frame (#12460)
WASM find_guide_at only checked distance on the guide axis, so board-bound
guides stayed interactive outside their board. Respect frame_range in
GuidePool hit testing so interaction matches the drawn solid segment and
the SVG hit area.

Closes #12322
2026-10-20 09:49:05 +02:00
Belén Albeza
60a94c65cc
🐛 Fix missing letters when copying text from exported PDFs (#12441)
Skia maps ligatures and other substituted glyphs to U+0000 in the PDF
ToUnicode maps. Rebuild those maps from the laid-out paragraphs and
append them as an incremental update.

Closes #12393
2026-10-20 09:32:53 +02:00
Elena Torró
0801674af2
🐛 Ignore export scale in render-wasm PDF export (#12447) 2026-10-19 18:21:02 +02:00
Alejandro Alonso
29cd50621d
🐛 Exclude inner shadows from WebGL export bounds (#12427)
Inner shadows were joined into extrect via the drop-shadow filter, so a
Y offset grew the PNG/SVG export surface. Only drop
shadows expand bounds, matching classic export. Closes #12386.
2026-10-19 17:23:26 +02:00
Belén Albeza
fd03ce2a63
🐛 Show text typed on a new line during the v3 layout preview (#12384)
A modified shape is a cached copy, and edits through get_mut did not
reset it. After Enter the preview stops sending modifiers while the size
holds, so the canvas kept painting the old text.

AI-assisted-by: claude-opus-5-5
2026-10-19 17:17:35 +02:00
Alejandro Alonso
655300e4d1
🐛 Include board drop shadow in multi-element raster export (#12440)
Headless raster/PDF used the board selrect so overflowing children stayed
cropped, but that also clipped the board's own drop shadow. Use
export_root_bounds: own strokes/shadows/blur without joining children.

Closes #12388
2026-10-19 15:29:40 +02:00
Elena Torró
c401151541
🐛 Fix mask group drop shadow in PDF export (#12362) 2026-10-19 12:07:51 +02:00
Elena Torró
01aacde0a8
🐛 Refresh board bounds after toggling clip content in WASM SVG export (#12363) 2026-10-16 14:01:48 +02:00
Elena Torró
ccdfaffbef
🐛 Fix stroke to path for strokes with different widths per side (#12356) 2026-10-16 12:36:23 +02:00
Alejandro Alonso
11bf8d1f67
🐛 Collapse text selection to edge on Left/Right (#12226)
With a range selected, Left and Right were moving one character from
the focus instead of collapsing to the selection start or end. Match
standard text-field behavior and keep Shift+arrows extending.

Closes #12207
2026-10-08 16:50:10 +02:00
Elena Torró
91b30d387f
🐛 Fix Ctrl+Home/End moving only to the paragraph edge (#12225) 2026-10-08 16:37:05 +02:00
Alejandro Alonso
78d26243c7
🐛 Sync Design tab styles on text-editor click caret (#12205)
Click caret placement only moved the selection and never refreshed
current styles, so the Design tab kept showing the previous fill and
typography. Place the caret through place_caret_from_position so
StylesChanged is emitted like keyboard navigation.

Closes #12195
2026-10-08 15:42:21 +02:00
Elena Torró
29ea6e49b6
🐛 Step over whole emojis in the text editor (#12202)
Caret moves, deletes and caret rects use grapheme boundaries.
Closes #12188
2026-10-08 13:42:25 +02:00
Elena Torró
b3a62eeb4b
🐛 Paint layout children in canvas z-order in vector exports (#12171) 2026-10-08 09:07:33 +02:00
Elena Torró
70dfe90571
🐛 Clear doc atlas when canvas background color changes (#12112) 2026-10-06 10:34:47 +02:00
Belén Albeza
60679bbbcd
🐛 Revert RTL auto-width text growing from its right edge (#12051)
Revert #11775 (commit 142f3d9de8). We are putting this fix on hold
until we settle the UX for RTL auto-width text.

Relates to #11523
2026-10-01 18:29:07 +02:00
Alejandro Alonso
cba51cfb7e
🐛 Keep adjacent tile compose origins abutted (#11968)
Round the view offset once and place tiles at
origin + k * TILE_SIZE so half-pixel pans do not
open a 1px background seam between neighbors.
2026-09-29 12:38:04 +02:00
Elena Torró
c44484a1e5
🐛 Fix SVG filter shadows and inherited group fills in render-wasm: (#11925)
* 🐛 Fix shadows derived from imported SVG filters

* 🐛 Fix inherited group fills in exports and group drop shadows

* 🔧 Run exporter tests on render-wasm changes
2026-09-28 07:29:25 +02:00
Elena Torró
4943285e63
🐛 Fix inner and outer rect strokes ignoring corner radius (#11926) 2026-09-25 14:12:01 +02:00
Elena Torró
63baf86152
🐛 Fix groups, masks and booleans drop shadow cases (#11911)
* 🐛 Fix blocky and black drop shadows on masked groups

* 🐛 Fix drop shadow spread on paths, bools and circles
2026-09-25 11:50:19 +02:00
Alejandro Alonso
e3e75725d4
✨ Export WASM SVG background blur via foreignObject (#11765)
Emit an XHTML foreignObject with CSS backdrop-filter for visible
background blur. The inner div uses clip-path:path(...) for the GPU
fill+stroke silhouette (glyph outlines for text). The FO sits outside
opacity and layer-blur filters. Stacked on the masks SVG export work.
2026-09-24 15:29:25 +02:00
Belén Albeza
a34b4011cf
🎉 Render different stroke styles per side (#11883)
* 🎉 Render different stroke styles per side

* 🔧 Stabilise per-side stroke snapshots against clip id churn

SVG clip ids come from a counter that lives for the whole test
process, so a snapshot holding one depends on how many clips other
tests emitted first. The masked-group export tests that landed on
develop shifted that count and broke two per-side snapshots, whose
only diff was the id.

Route the remaining per-side snapshots through with_stable_clip_ids,
which renumbers ids in order of appearance. No snapshot in the suite
now carries a generated id.

AI-assisted-by: claude-opus-5
2026-09-24 13:20:33 +02:00
Alejandro Alonso
ad7b93beb7
✨ Export masked groups to WASM SVG as alpha masks (#11652)
Paint the mask subtree into a native <mask mask-type="alpha"> and wrap
content under mask="url(#…)", matching GPU DstIn (soft/group masks).
Drop silhouettes use the same alpha mask; empty-fill SVG paths inherit
group fills.

Closes #11378
2026-09-24 11:25:34 +02:00
Belén Albeza
142f3d9de8
🐛 Fix RTL auto-width text growing away from its right edge (#11775)
This makes RTL texts in auto-width to grow towards their left side in
the text editor v3.

AI-assisted-by: claude-opus-5
2026-09-23 13:02:17 +02:00
Belén Albeza
34427c8a28
🐛 Fix borders not fully rendered next to masked groups (#11807)
A masked group now closes its focus scope on the mask pass, pops the
nested stacks on the content pass only, and draws its exit strokes on
the content pass. The focus depth also resets when each tile's walk
starts.

Closes #11805

AI-assisted-by: claude-opus-5
2026-09-23 12:45:02 +02:00
Miguel de Benito Delgado
f0880cf3f9
🐛 Crop cached drop shadows to their filter bounds (#11801)
Snapshot only the scaled shadow bounds instead of the full reusable
filter surface. This avoids retaining viewport-sized textures for each
small shadow while keeping cached pixels valid when the surface is reused.

Use the bounded snapshot for both shape and frame shadow caches. Add
regression tests for rounding, scaling, surface limits, and snapshot reuse.

AI-assisted-by: gpt-6
2026-09-22 11:09:45 +02:00
Belén Albeza
0581c35452
🐛 Fix drop shadows in masks (#11754)
* 🐛 Fix shadows on a masked group in the WASM renderer

A masked group renders in two passes: its content, then the mask shape
composited with DstIn so everything outside the mask silhouette is
erased. Both happen inside one save_layer, and the group's drop shadow
was composited into that same layer before the mask pass — so the mask
erased it. A drop shadow lives mostly outside the silhouette, so it
disappeared entirely.

Inner shadows never drew at all: render_fill_inner_shadows needs fill
geometry to paint into, and a group has none.

Both now ride on an image filter set on the masked-group layer, which
Skia evaluates after the mask is composited, so the shadow comes from
the real masked pixels rather than the group's own, empty geometry.
The effects compose in the order the SVG renderer uses for a group:
drop shadows, then the source, then inner shadows, with the layer blur
over all of it.

That layer is opened on a canvas carrying no transform, so the filter
is built in device units. Shadow::scale_to_device does that rather
than scale_content, because radius_to_sigma is affine: scaling the
radius applies its constant term once at device scale, while a filter
built in document space has the term scaled by the canvas matrix. The
two would blur differently by 0.5 · (scale - 1) sigma, visible as a
masked group's own shadow being narrower than the same shadow on its
parent. The masked-group layer blur had the same flaw.

Three paths are suppressed for masked groups so nothing is drawn twice:
the silhouette composite, the nested_shadows inheritance that would
reach text descendants, and the fill inner-shadow pass.

Every save and restore around that layer is keyed on the shape alone.
Enter and exit run on different walker passes, and a pan or zoom in
between changes fast mode, so deriving them from render state could
leave the canvas clip stack unbalanced.

Refs #11697

AI-assisted-by: claude-opus-5

* 🐛 Fix a container's drop shadow over a masked group in WASM

A container builds its drop shadow by drawing each descendant as a
black silhouette and blurring the result. The walk descends only
through children that can be flattened, and a masked group never can,
so it stopped there and asked the group to draw its own geometry. A
group has none, so nothing was drawn and the shadow layer stayed
blank: no shadow at all for a group, board or frame holding a masked
group. This is what the file attached to the issue reproduces.

render_drop_black_shadow now draws the masked silhouette for such a
group — content children flat black, DstIn the mask, and only then the
offset, blur and spread. Masking after the blur would trim the shadow
along the wrong edge.

The walk recurses, so it narrows the clip the way the main walker
does: content a clipping container hides must not widen the shadow.
The clip rule now lives in one place, shared with the walker, and a
test pins the two against each other. The shadow layer is sized to the
silhouette plus the shadow's reach rather than falling back to the
clip, so a wide blur is not cut at the tile edge.

Spread keeps the renderer's existing behaviour: the silhouette goes
through the same get_drop_shadow_filter every other shadow uses, so a
masked group gains no ordering of its own.

Closes #11697

AI-assisted-by: claude-opus-5
2026-09-18 13:04:11 +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
Elena Torró
4c940296f1
🐛 Fix fixed-scroll on the viewer (#11752) 2026-09-17 09:43:13 +02:00
Elena Torró
cc9c60507d
🐛 Do not apply stroke style to caps (#11744) 2026-09-17 09:39:39 +02:00
Alejandro Alonso
20547f658e
🐛 Preserve tile coverage when packing atlas slots (#11749)
Packed atlas compose inset Linear samples after blitting the full
drawable into the slot, which dropped edge texels and opened
multi-pixel gaps on text that crossed tile seams (#11696), especially
under HiDPI packing. Write the drawable into the inset content rect
and clamp-pad the 1px frame so Linear compose keeps coverage without
bleeding into the next cell.

Closes #11696
2026-09-16 16:40:33 +02:00
Alejandro Alonso
fcae641c7b
🐛 Skip content clip on WASM SVG drop silhouettes (#11655)
Clipped frames (show-content=false) were applying clipPath to the drop
silhouette. Spread/offset fills were outset correctly but then truncated
to the true selrect, so spread rings vanished on export. Match the GPU:
paint drop silhouettes outside the content clip; keep clipping on the
real content pass only.

Closes #11653
2026-09-16 16:08:17 +02:00
Alejandro Alonso
b8af89c1b8
✨ Export text strokes to WASM SVG (#11578)
SkSVGDevice drops save_layer composition for text strokes, so emit
center strokes (with <g opacity> when alpha), inner strokes via glyph
clipPath, and outer strokes via inverse-glyph luminance mask.

Closes #11386
2026-09-16 15:54:07 +02:00
Shreyash
76e9289fcf
🐛 Fix stroke to path dropping caps and markers (#11614)
* 🐛 Fix stroke to path dropping caps and markers

Caps and markers (round ends, arrows, square/diamond/circle markers) are
painted separately by `handle_stroke_caps` after the stroke itself, so
`stroke_to_path` — which only outlines the shape path — dropped them.
Converting a stroke to a path lost every end decoration except the
Round/Round and Square/Square pairs Skia draws natively, and even those
were missing because the outline paint kept the default butt cap.

Cap geometry now lives in `shapes/stroke_paths.rs` as plain path
builders, shared by the canvas renderer and by `stroke_to_path`, which
unions the caps into the outline for open paths and honors
`to_skia_linecap`. The SVG export no longer overlays the caps a second
time on top of the expanded outline, which would double the alpha of
translucent strokes; the clip silhouette for image strokes gets them
from the outline too.

Co-authored-by: Shreyash Agare <agareshreyash26@gmail.com>
2026-09-16 12:46:00 +02:00
Alejandro Alonso
3adf9ade14
✨ Export drop and inner shadows to WASM SVG (#11593)
SkSVGDevice drops save_layer image-filters, so re-emit visible drop and
inner shadows (plus optional layer blur) as one native SVG filter chain
matching classic order: flood, drops, SourceGraphic, inners, blur.

Closes #11379
2026-09-11 13:52:57 +02:00
Alejandro Alonso
ffad71bdc5
🐛 Hard-clip tile atlas blit to avoid seam hairlines (#11639)
AA clip on the Current→atlas blit softens shared tile edges so
the canvas background shows through as 1px lines at the 512px
grid when tiles are composed with SrcOver.

Closes #11638
2026-09-11 09:22:36 +02:00
Elena Torró
32ed9b5a08
🐛 Fix board clip on drag (#11620) 2026-09-11 08:27:11 +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
Alejandro Alonso
aa78ca0be8
🐛 Keep text image fills aligned during drag (#11610)
Cached Skia paragraphs bake absolute image/gradient shaders at layout
time. On move, clones reuse those paragraphs while painting at the new
selrect, so glyphs move and the fill stays put. Record the paint origin
when layout is built and translate the canvas when painting from cache
so shaders track the text. Also sync bounds before update_layout so
fills bake against the current container.
2026-09-10 16:55:45 +02:00
Alejandro Alonso
dbe5941a23
✨ Export layer blur to WASM SVG (#11580)
SkSVGDevice drops paint image-filters, so re-emit visible layer blur as a
native feGaussianBlur filter on the composite <g>. Match canvas sigma via
radius_to_sigma(value * scale), and skip Skia blur filters on the SVG
VectorRenderer path so shapes do not vanish.

Closes #11380
2026-09-10 14:25:47 +02:00
Alejandro Alonso
dc12f1db91
✨ Export image-filled strokes to WASM SVG as linked images (#11559)
Closes #11384

Skia's SVG backend drops save_layer+SrcIn, so image strokes are re-emitted
as a linked <image> clipped to an opaque stroke silhouette (filled outline,
clip-rule evenodd). Open-path caps join the silhouette and grow the image
dest by cap_bounds_margin so markers stay textured.
2026-09-10 13:29:14 +02:00
Elena Torró
9b24907992
🐛 Fix render text-decoration on spans and update regression tests (#11583) 2026-09-10 09:42:09 +02:00
Elena Torró
61caefbcf3
🔧 Support independent image bounds on wasm export (#11590) 2026-09-09 18:27:22 +02:00
Elena Torró
6586631293
🐛 Fix wasm boolean paths with opposite winding operand (#11551) 2026-09-09 18:19:29 +02:00
Elena Torró
3d4a5ca2aa
🔧 Use mutex so only one test owns global at a time (#11585) 2026-09-09 17:10:04 +02:00
Danny Shirely
d45c6710b7
🎉 Implement independent image bounds resizing (#11430)
* 🎉 Implement independent image bounds resizing

Add canvas resize interaction mode that allows users to resize an
image object's bounding box independently from the underlying bitmap
content without scaling or distortion while holding the Mod key.

AI-assisted-by: gemini-2.5-pro

* ♻️ Address reviewer feedback from elenatorro

- Remove legacy cfh/image-shape? check in shape-has-image-fill?
- Guard bounds-resize with positive dimensions instead of clamping scalev to preserve flipping
- Remove :metadata from transform-attrs in modifiers.cljs
- Restore preserveAspectRatio logic based on keep-ar? in fills.cljs
- Compute source rect against destination rect for raster and SVG fills in WASM renderer

* 🔧 Fix clippy needless borrow warnings in wasm image fills

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-09-09 15:16:56 +02:00