Problem: Object Matte had no After Effects comparison, and none can be made
on this Linux machine (no After Effects). The gap notes still listed the
viewer tool, background job and disk cache as missing.
Fix: docs/fidelity/object-matte.md states plainly that nothing is measured
yet and pins down the comparison for when an After Effects 26.2+ reference
is available: four original or openly licensed clips (the synthetic moving
disk from the engine tests, the disk with a jump, two objects with a box
prompt, one CC0/CC BY real clip), the prompt and settings, where reference
mattes stay (gitignored), and the metrics: per-frame IoU against After
Effects (headline), IoU against ground truth, boundary distance, soft-edge
error, temporal stability, drift and time per frame, plus a proposed pass
bar left to the maintainers. Linked from docs/fidelity/README.md.
docs/gaps.md (#14) and docs/target-app-parity.md now list what landed and
leave the comparison as the one remaining item.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: computed Object Matte segmentations lived only in the in-memory
cache, so every restart or memory eviction segmented the shot again (After
Effects 26.5 keeps Object Matte results in its disk cache).
Fix: the persistent disk cache gets a third entry kind, Matte (folder
`mattes`, LZ4-compressed FrameSeg bytes, same checksum, atomic writes, LRU
eviction under the one size limit, purged with the rest). Entries are keyed by
the segmentation's chain key and scale; chain keys already hash the Input Key
(the layer's source, timing, masks and effects above), the prompt, the mode and
the model, so any change simply misses. The effects crate gets a SegStore
trait behind its memory cache: Chain::ensure reads from it on a memory miss
and writes what it computes. The engine installs the session's DiskCache as
the store (held weakly, so switching the disk cache off drops it). Roto Brush
segmentations go through the same chain and are persisted too. Frozen mattes
already persist in the project (the Frozen parameter).
Tests: engine tests_roto::object_matte_mattes_persist_in_the_disk_cache (a
second session on the same folder and project gets all tracked frames back
without being able to read a single input frame, renders identically, and a
moved prompt finds nothing); render disk_cache
segmentations_round_trip_and_reopen (round trip, reopen, damaged file is a
miss, size limit evicts).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Object Matte computed its frames only on demand while rendering, so
playing a shot stalled on every untracked frame, there was no progress or way
to stop, and nothing kept the mattes once computed.
Fix: Object Matte runs through Roto Brush's job machinery (one job slot,
cancellable with roto.cancel, a worker thread on the desktop, a Web Worker in
the browser, never the UI thread). objectMatte.propagate computes the frames
around the reference frame (clipped to the layer) outward, filling the same
content-keyed segmentation cache the renderer reads; the engine's sync now
queues an instance for background tracking when its frames or prompt change.
objectMatte.freeze stores the final mattes in a new hidden Frozen parameter
(Roto Brush's FrozenCache format) under a key of the input, prompt, span,
model and matte settings; renders use them without segmenting while the key
matches; a frozen prompt is refused until objectMatte.unfreeze. Freeze is one
undo step and is dropped if the prompt changed meanwhile.
objectMatte.status reports progress, frozen state and computed frames. Effect
Controls shows Freeze / Unfreeze and Track / Stop with a status line (progress
banner). The frontend now polls Roto Brush / Object Matte jobs on their own:
before, a finished job (and so a Freeze result) was only picked up while a
Warp Stabilizer job was active. Job progress is set before the worker starts.
Tests: engine tests_roto::object_matte_background_job_cancel_and_freeze
(queued by the edit, returns at once with progress, cancel keeps a partial
result, completion computes all 25 frames, frozen renders recompute nothing,
unfreeze recomputes, undo restores); effects object_matte job_frames and
frozen-matte tests (stored pattern used, key mismatch and damaged data
ignored); ui-egui ui_object_matte Effect Controls Freeze end to end.
docs/agents.md: Object Matte section; docs/effects.md regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: Object Matte could only be applied through the objectMatte.apply
command; After Effects 26.2 picks the object directly in the viewer (click it,
or drag a box around it).
Fix: an Object Matte tool (with Roto Brush & Refine Edge in the Alt+W slot,
icon drawn in code). In the Composition viewer a click sets a point prompt and
a drag a box prompt (its centre the point) on the footage, solid or comp layer
under the pointer, converted to layer pixels through the layer's transform.
The layer's Object Matte (the selected one, else its last) is re-prompted;
Shift adds another. Everything goes through objectMatte.apply, which gained
`effect` to re-prompt an instance (point, box or no box, reference frame; the
reach kept unless given), so each prompt is one undo step; the effect is then
selected, so its point controls show in the viewer. Hover draws a crosshair
and the outline of the layer that would get the matte (a live matte preview
would need a segmentation per pointer move). Automation ids:
viewer.objectMatte.crosshair / .target / .box, tools.ObjectMatte.
Tests: ui-egui ui_object_matte (hover, click, drag box re-prompts the same
effect, Shift-click adds one, undo steps); engine
tests_roto::object_matte_reprompt_updates_the_instance. Checked with the
headless snapshot example (hover crosshair, box drag isolating a disk).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: with a trained model installed, Object Matte's optional box reached
MobileSAM only as background points along the box's edges (an approximation),
although the model's prompt encoder takes a box prompt directly.
Fix: a new Roto Brush stroke kind, Box (two opposite corners), that
stroke_prompt turns into the model prompt's bbox (scaled to the matte grid,
corners in any order, degenerate or non-finite boxes ignored). The graph cut
never rasterizes it. Object Matte emits it instead of the edge strokes when a
model segments; the classical fallback keeps the edge strokes. A prior matte's
box no longer replaces a box the user drew on that frame.
Tests: effects object_matte::the_box_reaches_the_model_as_a_box_prompt (a
stand-in model sees the box, at full and half resolution, and no edge points);
track roto::tests::box_stroke_is_the_model_box_prompt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the hand-maintained effect count in app.capabilities still said 306 after Object Matte was registered.
Fix: bump it to 307.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: After Effects 26.2's Object Matte (pick an object with a click,
optionally a box, get a matte that follows it) had no counterpart; only
stroke-driven Roto Brush existed.
Fix: an Object Matte effect (Effect > Matte, ec.matte.objectmatte) turns
the Object Point (and optional box) on a Reference Frame into Roto Brush
strokes and segments them with Roto Brush's engine, content-keyed cache
and the model chosen in Settings > Roto Brush (MobileSAM, classical graph
cut without it). Track Object carries the matte frame by frame up to 600
frames each way; otherwise every frame is segmented on its own. Feather,
Contrast, Shift Edge, Invert and a Matte view. The engine keeps its Input
Key current so upstream edits recompute it. objectMatte.apply creates one
from agents / the CLI. Unusable prompts pass the layer through.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Release prep: bump to 0.8.0 and add a Linux riscv64 release build
- release.yml: new `linux-riscv64` job, ported from GridCraft. It cross-compiles on
ubuntu-24.04 against riscv64 libraries from ports.ubuntu.com (the Linux job's X11/Wayland/GL
-dev packages plus ALSA), ships effectcraft-<v>-linux-riscv64.tar.gz, and smoke-tests
effectcraft-cli under qemu-riscv64. The draft-release job now needs it, so its tarball is
uploaded and covered by SHA256SUMS.txt.
- packaging/linux/package.sh: cross-compiles when CROSS_ARCH/CROSS_TARGET/CROSS_COMPILE are set
(cargo --target, the cross strip, the EMULATOR smoke test), as GridCraft's does.
- Version 0.7.0 -> 0.8.0 (cargo xtask version set), Cargo.lock refreshed.
- README and docs/releasing.md list the riscv64 tarball.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Fix FreeBSD CI: pointer_wrap test harness, wasm32 target, clippy as_chunks
- pointer_wrap tests: the harnesses dropped run_ui's FullOutput with the font atlas upload still
in textures_delta, and epaint 0.36's drop check panics on that in debug builds. Every
pointer_wrap test failed on every platform; FreeBSD is the only CI that runs them. Clear the
delta, as the other UI tests do.
- freebsd.yml: install wasm32-unknown-unknown in the VM. Without it the host plug-in test's build
of examples/plugins/posterize-bands failed (E0463, no std) and the test skipped silently; now
it builds and runs.
- media/convert.rs: chunks_exact(2) -> as_chunks::<2>() (clippy chunks_exact_to_as_chunks,
new in Rust 1.99); same pairs, same dropped remainder.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Animated GIFs came in as one still frame in 0.7.0; bfbdadd3 (after that
release) probes them as movies. This end-to-end test imports a GIF file
through the wired session into a comp and checks the layer shows each of
its frames at its time, the path the viewer and exports share.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
file.import stores a path as given, so footage imported by the CLI or an
agent as `clip.mp4` was saved relative, and opening the project looked it
up from the working directory. The CLI, started in the project's folder,
found it; the desktop app, started from the Finder, Explorer or a launcher
in another folder, showed and rendered neither its picture nor its sound,
while the Project panel still listed the saved duration.
On open, a relative footage path that doesn't exist from the working
directory but does next to the project file is resolved there (After
Effects also looks for footage relative to the project). Paths that exist
as they are, and absolute ones, are kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
Ported from #673: a native error dialog names the auto-saved recovery copy before the window
closes; agent runs through the control channel skip it. The UI calls Hooks::device_lost once.
Co-Authored-By: Dennis M. Francis <44986763+didpublishing@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
A text or shape layer's effect bounds are comp-sized (as in After Effects),
but its buffer surrounds only its content, and each effect grows the buffer
itself when it needs room. CC Smear and CC Tiler never did, so a smear was
cut off where the glyphs end and tiles filled only the text's box.
CC Tiler now covers the whole layer rectangle; CC Smear grows by how far it
moves pixels (Reach x From->To), at most to the layer bounds, so footage
layers, whose buffer is their bounds already, are unchanged. CPU and GPU
alike.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
Every viewer request spawns a job on the frame pool. A pool thread blocked
inside a render (waiting on a parallel loop's join) runs other pool work
meanwhile, rayon's injected jobs included: the spawn of the next request
started a whole frame render on top of the waiting one. Dragging makes a
new revision, and a new viewer frame, every UI frame, so renders nested
without bound on every thread, each holding its buffers (a large image's
full-resolution copies), until all memory was used. Holding the layer still
makes no new revisions, which is why only moving it did this.
A spawn now drains the queue, and one that lands inside a render on its
thread returns at once; the render below takes the queued jobs when it is
done. Each pool thread renders one frame at a time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
The Scrub helper and the real-properties test discarded FullOutput, so the
first pass' font-atlas texture delta panicked on drop in debug builds —
which is how the FreeBSD CI gate failed. Consume the delta the way the
rest of the crate's egui tests do.
Preserve legacy selection repair, modifier routing, viewer lifecycle and workspace persistence through the shared docking integration. Qualify the complete workspace gate by refreshing the 18 stale Audio Spectrum hashes for the existing linear renderer, with an independent synthetic-tone regression.
`effectcraft-cli mcp` had no way to keep an agent's file access to a folder;
the flags were unknown options. Both now take a folder (repeatable, as in
PhotoCraft): with either given, files are read only under a read or write root
and written only under a write root (none: no writes). Defaults are unchanged.
automation::confine::Confinement checks every engine command the backend runs
(execute_command, command_run, open/save_project, each engine.batch step), in
both headless and bridge mode, before it runs: the file parameters of the
commands that read or write files must be absolute paths inside the roots
(symbolic links resolved through the longest existing ancestor, `..` refused),
as must any other absolute path in a command's parameters. Commands that write
where they aren't told are checked against the target they would write: Save /
Increment and Save the open project's file, renderQueue.render every queued
output (renderQueue.list). Scripts, Execute File, Reveal in Finder, Open Recent
by index, `$N` file-path references in a batch, render_frame/screenshot paths
outside a write root and the bridge's UI-driving tools are refused while
confined. docs/mcp.md describes it.
Test: a confined headless server opens and saves inside its roots and is refused
outside them, for a relative path, `..`, a batch step, a `$N` path, Save to a
read-only root's project, the Render Queue's default output there, a symbolic
link out of the write root, and scripts; the unconfined server still writes
anywhere.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
The file dialogs were blocking rfd::FileDialog calls made inside eframe's frame,
that is inside winit's event handler. On macOS the dialog runs a nested modal
event loop there; a file dragged over the window while it was open reached
winit's handler again, which refuses re-entry with a panic across the
Objective-C boundary, aborting the app (Import with Cmd+I, then a drag).
Hooks gains file_dialog_async: a host that sets it shows a command's file dialog
(Import, Open, Save As, Save Frame As, presets, guides, templates, Collect Files,
Relink...) without blocking, and the command runs with the answer on a later
frame (menus::dialog_answered, drained at the start of each frame); a cancelled
dialog runs nothing. The macOS app implements it with rfd::AsyncFileDialog, a
sheet on the window answered when it closes, outside the frame. Other platforms
keep the blocking dialogs (winit queues their re-entrant events), as do the few
panel buttons that pick a file in place (Settings paths, shortcut import/export,
effect file pickers).
Test: with a non-blocking hook, Save As and Save Frame As return at once without
the blocking dialog, the save happens when the answer arrives from another
thread, and a cancelled dialog writes nothing. The macOS sheet itself needs a
manual check (Cmd+I, drag a file over the window, pick or cancel).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
widgets::keep_focus (Project panel rename and comment, Render Queue, Essential
Graphics and Timeline fields) selected the whole text whenever it saw its field
without the keyboard focus, not just on the frame the editor opened. Anything
that took the focus for a frame (an input method, another handler) then
re-selected the name, so the arrow keys seemed to do nothing and Backspace erased
the whole name, as reported on Linux. The editor now remembers the pass it was
last shown: it selects all only when it opens and otherwise just takes the
keyboard back with the caret where it was.
The harness does not reproduce the report's Linux behaviour itself (arrows reach
the field there); the tests cover the path that did: a unit test drops the focus
for a frame and checks the caret survives (and that reopening selects all), and a
kittest renames a Project item and a Timeline layer with Left / Right / Up /
Down / End and Backspace, editing part of each name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
Motion-path hits were only handled for the Selection tool, so the Pen slot's
tools did nothing on a layer's motion path, and every Pen slot tool showed the
same crosshair. As in After Effects, on a motion path:
- Add Vertex (and the Pen) add a Position keyframe at the clicked frame dot,
with the path's value there (prop.addKey), so the path keeps its shape;
- Delete Vertex removes the clicked keyframe (keys.select + keys.delete);
- Convert Vertex switches it between Linear and Auto Bezier spatial
interpolation (keys.interpolation {spatial}).
motion_path now reports its frame dots for the hit test. Over the viewer the Pen
slot tools draw their own pen glyph (Pen, Add, Delete, Convert, Mask Feather) at
the pointer in place of the system cursor, and the Pen shows Add Vertex's pen over
a mask, shape or motion path it would add a point to.
Test: ui_viewer clicks a two-key motion path with each tool and checks the added
key, its spatial interpolation both ways, its removal, and the pointer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
On Adreno (Windows ARM64) a failed vkAllocateMemory surfaces as an internal
ERROR_INITIALIZATION_FAILED, not out-of-memory. Gpu::within_memory only scoped
OutOfMemory, and wgpu's DeviceExt::create_buffer_init maps the new buffer at
creation and panics ("Failed to get mapped range") when the allocation failed,
taking down a frame thread. Buffers with initial contents are now created unmapped
and filled through queue.write_buffer (context::buffer_init), so a failed
allocation is only an invalid buffer and an error for the caller's scope, and
within_memory scopes Internal errors too, discarding the frame for the CPU path
as it does for out-of-memory.
The device was then lost, and egui-wgpu panicked on the main thread staging the
next frame's index buffer on the dead device. Once the host reports the
presentation device lost, the app draws nothing more (no mesh to stage), auto-saves
unsaved changes, keeps the crash-recovery marker so the next launch offers them,
and closes its window.
Tests: a device with a small buffer limit gets an over-limit and an unaligned
buffer_init without a panic and sees the error in its scope; the UI harness
reports a device loss and checks the next frame has no shapes, closes the window
and wrote the auto-save.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
Auto-Bezier end keys now carry handles along the path, so a straight two-key move went through
the arc-length table and landed a rounding error off its keys' midpoint (80.00000000000001).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
The MSI only installed a Start Menu shortcut. The wizard's folder page now has a
"Create a desktop shortcut" box (DESKTOPSHORTCUT, 1 by default; DESKTOPSHORTCUT=0
for silent installs) that adds an EffectCraft shortcut to the Public desktop. The
choice is recorded under HKLM\Software\EffectCraft so an upgrade keeps it, and an
upgrade from a release before this option adds none. test-msi.ps1 checks the
shortcut is created by default, kept by upgrades, removed on uninstall and absent
with DESKTOPSHORTCUT=0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
A tool can hand EffectCraft its own font library (a path list) without
installing the fonts system-wide; the folders are scanned like the
system ones.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Effect Controls painted effect headers with fixed dark fills (#2a2a2a,
#2f3a52 when active) under the theme's text colour, so on Light the effect
names were dark on dark (1.1:1). The Timeline kept the selected layer's
time-graph row, the A/V and switch wells, the time navigator, the work area
bar and the expression field on fixed dark fills too.
Those surfaces are now theme tokens (fx_header, fx_header_active,
tl_graph_selected, switch_well, tl_nav_bar, tl_work_area_bar, expr_bg,
expr_text) with the dark values unchanged and light ones for Light. The
expression editor's syntax colours come from the theme as well: on the
light field a darker set keeps every category, enabled/disabled
expressions, matched brackets and line numbers at 4.5:1 or better
(contrast test, with a negative control showing the dark set fails there).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
A typed value committed by clicking elsewhere could be applied as an
offset, because dragged_id() is set on the press that commits it; a scrub
now needs a decided drag. In the Timeline, clicking a stopwatch or the
keyframe button acts on that layer's property only, as in After Effects
(the Properties panel and keyboard shortcuts act on every selected layer).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
With several layers selected, After Effects applies a property edit to the same property of
every selected layer; we changed only the layer whose row was edited (the Properties panel
showed only the first selected layer), so a stopwatch switched on one layer and later moves
of the others had nothing to keyframe (#355), and scaling or rotating a selection changed
one layer (#491).
- Engine: `prop.set`, `prop.toggleAnimation` and `prop.toggleKey` take `selected: true`:
the change also goes to the same property (by match path, `transform/scale`,
`effects/<effect>#n/<param>`) of every other selected, unlocked layer whose value has the
same kind, in one undo step (a scrub keeps folding into it through its `merge` key). With
`offset: true` (a scrub) each layer changes by the same amount from its own value; a typed
value sets them all. Position with Separate Dimensions is handled per layer.
- The Timeline, Properties and Effect Controls panels pass `selected` (and `offset` while a
value is dragged). The Properties panel heads "(N) Selected Objects" (zh-CN / zh-TW / ja /
uk rows).
- Viewer: a bounding-box handle drag scales every selected layer by the same ratio about its
own anchor point; the Rotation tool turns them all by the same angle (one undo step).
Tests: engine test (stopwatch, scrub offset, typed value, keyframe button, one undo step,
unselected layers untouched); kittests: the Properties panel with three layers selected
(header, stopwatch on all three undone in one step, a scrub moving all three by the same
amount) and a handle drag scaling two selected layers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
The viewer projected a 3D layer's box, its axis gizmo and its Extended Viewer outline
through the camera with a plain perspective divide. A point at or behind the camera
(w ≤ 0) flips through infinity, so a layer turned or moved so that part of it reaches past
the camera drew long lines across the viewer, and its handles and hit area flipped too.
- `layer_quad` (handles, hit tests, marquee) is None for a layer reaching past the near
plane, so nothing is placed or picked on a flipped quad.
- `layer_outline` clips the box to the part in front of the near plane (Sutherland-Hodgman
in homogeneous coordinates); the selection outline and the Extended Viewer use it.
- The 3D axis gizmo's arrows are clipped the same way (`project_segment`).
Tests: a box crossing the near plane keeps only its front part, one wholly behind draws
nothing; gizmo segments are cut at the plane and never flip.
Note: the reporter's exact setup (ellipse, X/Y Rotation, Orientation) wasn't reproduced
in a headless probe with the default camera, where the layer stays in front of it; this
fixes the projection fault that draws such lines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f