Hook up menu-bar actions that lacked a mobile affordance, and implement z-order.
- Stage/object long-press menu (stage.rs): add Paste (on clip, geometry, and
empty stage) and Send to Back / Bring to Front on the selection.
- Timeline long-press menu (timeline.rs): the timeline had no mobile context
menu — add one via manual long-press detection. Clip actions (Split,
Duplicate, Cut, Copy, Paste, Delete) on a clip; animation actions (New/Blank
keyframe, Add keyframe at playhead, Duplicate keyframe, Delete frame, Add
motion/shape tween) on an empty lane. Gate the desktop right-click menu to
!is_mobile so mobile shows only the long-press menu.
- Implement Send to Back / Bring to Front (were // TODO no-ops): new undoable
ReorderClipInstancesAction in lightningbeam-core reorders the selected
instances within their layer's clip_instances Vec (stacking order; last =
on top; geometry stays flattened underneath). Wired via handle_menu_action.
Introduce editable text layers: a resizable text box with editable text,
font size, color, font family, and alignment.
Core:
- New TextLayer/TextContent (text_layer.rs), wired into AnyLayer/LayerType
and all the exhaustive match sites; structured so content can be keyframed
later via content_at().
- fonts.rs: thread-local parley FontContext with three bundled fonts
(Liberation Sans/Serif/Mono, SIL OFL), system-font enumeration consolidated
to base families, document-embedded fonts, glyph/caret/selection geometry,
and a background preloader for the picker fonts.
- Rendering via parley layout + Scene::draw_glyphs (renderer.rs); text
composites through the vector path.
- Actions: CreateTextClipAction (vector-layer branch, undoable),
SetTextContentAction, ResizeTextBoxAction.
- .beam font embedding: MediaKind::Font rows (content-hash dedupe) written on
save and registered on load, with bundled-default fallback.
- VectorClip content bounds include text boxes so text-only clips are
selectable/draggable.
Editor:
- Text tool: click empty/raster/video to create a top-level text layer, or a
vector layer to create+enter a clip containing the text; click an existing
box to edit it.
- Hybrid in-place editing: a hidden egui TextEdit drives input/IME/caret while
the text and caret/selection render in Vello; empty just-created layers are
removed on commit.
- Selection outline + 8 resize handles (re-wrap text) with hover cursors;
factored the corner/edge resize-cursor mapping shared with the Transform tool.
- Info panel: edit text, size, color, alignment, box size, and a font-family
picker that previews each entry in its own font (fonts preloaded in the
background to avoid hitches).
Deps: add parley (git, pinned to match vello's peniko); bundle Liberation
fonts under lightningbeam-core/assets/fonts. Gitignore the local
.cargo/config.toml used to select a machine's ffmpeg.
Resizing the document leaves raster layers as-is (canvas keeps its old pixel size,
anchored top-left). To reconcile, the info panel now shows the active raster
layer's canvas size — driven by the *active* layer, not selection focus, since
painting doesn't focus the layer — and, when it differs from the document, a
Scale / Expand-Crop toggle + a "Layer to document size" button.
- RasterKeyframe::resize_to(w, h, mode): always applies the new declared size;
resamples (Lanczos3) when Scale, top-left pad/trim when Canvas, and only touches
the buffer when pixels are resident (a blank canvas just takes the new size).
Sets texture_dirty so the stage's dirty-scan refreshes the GPU texture.
- ResizeRasterLayerAction resizes every keyframe with undo, holding a (read-only)
RasterStore so paged-out keyframes are loaded one at a time rather than bulk.
Resized keyframes stay resident + dirty and persist on the next full save (no
incremental store write to page them back out). Scale is lossy/compounds;
Expand-Crop is lossless.
`tween_after == Shape` was stored on keyframes but never read. Now the
render path morphs geometry across a shape-tween span:
- VectorGraph::interpolated(other, t): same-topology lerp of vertex
positions, edge curves, stroke widths and stroke/fill colours. Returns
None when topology differs (counts, deleted flags, edge endpoints, fill
boundaries), so the caller holds the source keyframe.
- VectorLayer::tweened_graph_at(time): returns an owned morphed graph for
a shape-tween span whose two keyframes share topology, else borrows the
held keyframe. Editing still uses graph_at_time (the held keyframe).
- Renderer (Vello + CPU paths) renders via tweened_graph_at.
- SetTweenAction + wired the previously-stubbed "Add Shape Tween" menu.
The typical workflow — keyframe, duplicate it (same topology), move
vertices, Add Shape Tween — now morphs between the two. Non-matching
topology falls back to a hold.
Both actions were DCEL-stubs (no-ops). They now extract the selected geometry from a
vector layer's active keyframe into a new VectorClip (group vs movie clip) and place a
ClipInstance in its place (identity transform → renders where the geometry was), which
the existing transform-animation system can motion-tween.
- Shared `clip_from_geometry::extract_geometry_to_clip` (+ undo) does the work; the
actions are thin wrappers. Undo snapshots the source graph + removes the clip/instance.
- `extract_subgraph` now DERIVES shared-fill boundary edges internally (an inside edge
still used by a non-extracted fill must be duplicated, not moved) and unions them with
the caller's `explicit_boundary` — so a plain geometry selection needs no boundary
analysis (the selection already includes fill boundary edges via `select_fill`). The
region-select caller keeps passing its lasso boundary.
- Handlers: Group / Convert to Movie Clip on a geometry selection now build these actions
from `selected_fills`/`selected_edges`.
Next: shape tweens (same-topology lerp).
- SetImageFillAction (core): set/clear `image_fill` on the selected VectorGraph fills,
with per-fill undo (mirrors SetFillPaintAction). Image takes render priority; clearing
reveals the colour/gradient underneath.
- Info Panel Shape section: an "Image:" combo listing the document's image assets (+ None)
for the selected fill(s), showing the current assignment. Assign/clear pushes the action.
This lets an existing shape be given (or cleared of) an image fill, complementing the
import/drop placement. Next: 3.5b — persist image assets in the .beam container.
Make raster layers behave like vector on the timeline.
- Timeline: draw a diamond per `RasterKeyframe` (mirrors the vector keyframe block).
- New Keyframe (K / menu): on a raster layer, insert a BLANK cel at the playhead via
a new undoable `AddRasterKeyframeAction` (+ `RasterLayer::insert_blank_keyframe_at`
/ `remove_keyframe`). Vector path unchanged.
- Stop lazy creation: paint tools now edit the ACTIVE keyframe (at-or-before the
playhead) instead of creating one. The brush captures the active keyframe's exact
time; `RasterStroke`/`RasterFillAction` resolve via `keyframe_at_mut` (error if
none); the tool-site `ensure_keyframe_at` blocks (brush/fill/bucket/wand/quick-
select/floating-lift) are removed — each read already bails when no keyframe exists.
New layers still seed a keyframe at the playhead, so there's normally one to paint
into; painting before the first keyframe is now a no-op (as intended).
Next: onion skinning.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`RasterStrokeAction`/`RasterFillAction` stored the whole before+after RGBA frame
(~16 MB/action at 1080p → up to ~1.6 GB at the 100-action cap). They now store a
`RasterDiff` — only the changed bounding box's pixels before and after — computed
once in `new()` from the full buffers, which are then dropped. A brush dab shrinks
from ~16 MB to tens of KB; a full-canvas fill is unchanged (its bbox is the frame).
Paging interaction: a diff overwrites just the bbox, so the keyframe's pixels must
be resident when undo/redo applies. A clean evicted frame's container bytes equal
its current logical state, so the editor faults the target frame in (synchronously)
before undo/redo via a new `Action::raster_resident_hint` + `peek_undo/redo_raster_hint`.
Dirty frames are never evicted, so they're already resident. If a base is somehow
not resident the apply is skipped (logged), never resized-and-corrupted.
Unit tests cover exact before/after round-trip, blank-first-stroke, no-op, and the
non-resident-base skip.