* Layer: Un-precompose a selected pre-comp layer back into its parent composition (#2918)
layer.unprecompose copies the nested composition's layers into the parent at the pre-comp
layer's place with timing and position carried over, and refuses what it can't keep exactly.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Un-precompose: refuse a pre-comp layer with Solo or Guide on
A soloed pre-comp layer hides the comp's other layers and a guide layer is
left out of renders; the expanded copies carry neither switch, so the picture
would change. Listed with the other blockers, in the command description and
the agents.md Cookbook entry; the refusal test covers both switches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Un-precompose: warn instead of refusing, only a locked layer stops it
A locked pre-comp layer is still refused, with or without `force`. Every
other former refusal reason is a warning: without `force`,
`layer.unprecompose` returns an error that lists the reasons and changes
nothing. `dryRun` returns `{dryRun, warnings}`. With `force` the layer is
unpacked in one undo step and the result carries `warnings`. Whatever the
pre-comp layer itself carried is dropped, and other layers' parent, track
matte and layer-property links to it are removed.
Layer > Un-precompose in the UI lists the reasons in a prompt with
Unpack Anyway / Cancel (`dialog.unprecompose.force` /
`dialog.unprecompose.cancel`). A layer without reasons unpacks at once.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Un-precompose: warn about expressions that refer to the pre-comp layer
An expression on another layer of the comp that names the pre-comp layer
would no longer find it once the layer is unpacked. The expression can
name it in quotes or call `layer(<index>)` with its index, bare or on
thisComp; another comp's comp("…").layer(…) is not counted. Each such
property is now listed once as a warning. Like the other warnings,
`force` unpacks and `dryRun` lists it. The expressions are left as
written.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Un-precompose: warn about lost frame sampling and Position expressions, keep markers in order
Review follow-up (CodeRabbit on #3081):
- A nested comp that preserves a frame rate other than the parent's
samples its layers on its own frames, and the expanded copies would
lose that. Motion-blurred nested layers under a different shutter
would also blur with the parent's shutter. Both are now warnings.
- When the pre-comp layer's Position differs from its Anchor Point, the
move is added to the stored Position of the unparented nested layers.
An unparented layer whose Position, or a separated axis, has an
expression may ignore that move, so it is now a warning.
- Copied comp markers are sorted into time order with the comp's own.
- New tests: a 10 fps nested comp with Preserve Frame Rate in a 30 fps
comp, a different shutter, a Position expression with and without a
move, and nested matte and layer-value remapping with shifted markers
merged in order.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Un-precompose: warn when an expression's layer numbers shift; the prompt keeps its target
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Un-precompose: a shorter renumbering warning
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* Image sequences: a gap in the numbering holds the previous frame, as After Effects
Split out of #1876 (originally 7850cbf4), because it reverses the colour bars
documented in #297 and needs its own discussion.
After Effects 2026 shows the last existing file across a gap in a sequence's
numbering (files 1, 2, 4, 5 show frame 2 at frame 3; read back from After
Effects). EffectCraft drew colour bars there. The media pool now holds the
previous file (Footage::sequence_file_held); colour bars remain only where no
file precedes the frame. Missing-frame reports on import are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Image sequences: hold the first file of the preceding number across a gap; fix the import warning
A gap now holds the file sequence_file picks for the preceding number, so
files sharing a number do not switch at the gap. The Report Missing Frames
warning says the previous frame is held instead of "shown as placeholders".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Image sequences: Interpret Footage says a gap holds the previous frame
The Interpret Footage note for a numbered sequence with gaps still said the
missing frames are shown as colour bars; the media pool holds the previous
frame there now. The note, its comment and two test doc comments say so,
and a forms test checks the note's wording.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* Masks: set the corner roundness of a Rounded Rectangle mask (#3067)
layer.addMask takes a roundness (corner radius in px) for shape rounded, and the new
mask.setRoundness changes it afterwards on a rectangular mask.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Mask roundness: refuse ellipses, keep feather points and key correspondence
mask.setRoundness accepted any closed path whose vertices lie on its bounding
box, so an ellipse was replaced by a rectangle. It now accepts only a plain
rectangle (four corner vertices, straight sides) or a rounded one (eight
vertices, straight sides alternating with circular corners).
The rebuilt path keeps the old start vertex and direction, and its feather
points, so an animated path's other keys and the feather points still line
up. A change between square and rounded corners changes the vertex count; it
is refused when the path is keyed or has feather points instead of leaving
keys with different counts or dropping the points.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* Layer: Quick Set Anchor, nine anchor points with Position compensation (#2967)
layer.quickSetAnchor moves the Anchor Point of 2D layers to a corner, edge midpoint or the
centre of their content and shifts Position and its keys so nothing moves. Layer > Transform >
Quick Set Anchor lists the nine points.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* Quick Set Anchor: skip layers with an animated Anchor Point, Scale or Rotation
The anchor is set at the current time and the Position shift is worked out
from the current Anchor Point, Scale and Rotation, so on a layer where any of
them is keyed or has an expression the layer would move at other frames.
Such layers are skipped like 3D and locked ones (an error when none is left).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Quick Set Anchor: skip layers with a Position expression or Auto-Orient along the path
An expression on Position (or the separated X/Y) overrides the shifted value,
and Auto-Orient along the path adds a rotation the shift does not account for;
both would make the layer jump, so they are skipped like 3D layers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Quick Set Anchor: error when no selected layer has content bounds
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Quick Set Anchor: test separated, keyed Position keeps the layer in place
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* G8: plug-in library
Community Plug-in Library for WebAssembly effect plug-ins (#376): one
reviewed catalog (index.json), install / update / remove from Settings >
Plug-in Library (also Effect > Plug-in Library...), library.* commands,
start-up loading of installed packages with SHA-256 checks. Downloads go
through the system curl (https, GitHub hosts), as trained models do.
Preview before installing (library.preview): the release is downloaded,
checked like an install, and rendered in the sandbox on a built-in sample
frame at default settings (2 s at 12 fps) with a Before/After split, plus
the effect's parameters, "Open on GitHub" and an optional author video
link (catalog field preview.video). Authors need to do nothing for it.
Hardening: a module that fails to load leaves the installed copy alone,
Update All updates every package in one job, minApp also applies to an
explicit version, curl ignores ~/.curlrc and URL globbing, unique temp
files, percent-decoded file URLs. Plug-in effect slots grow from 64 to
256 with a clearer overflow error.
Docs: plugins.md "Plug-in Library", publishing-plugins.md (author guide
from zero to listed), gaps.md; examples/plugin-catalog is the starting
content of the catalog repository (schema, validation workflow).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* contributors: credit iseemhlyfta
* G8: plug-in library: doc comments on the library's functions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* G8: plug-in catalog check: require one object with schema 1 and a packages array
An empty file or several JSON values passed jq empty, and with
SKIP_SCHEMA=1 nothing else checked the catalog's shape.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* G8: plug-in library: one preview texture set; catalog schema requires effects
The preview kept a texture set per package version previewed (about
6 MB each) for the whole session; it now keeps one, replaced when
another package is previewed. Releases in the catalog schema must list
at least one effect id, as the author guide says.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* G8: plug-in catalog schema: effect ids follow the manifest's id rules
Letters, digits, '.', '_' and '-', and not the reserved 'ec.' prefix,
so library.missing can match them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* G8: refuse Plug-in Library jobs while automation file access is confined
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* G8: split the Plug-in Library page into toolbar, card, links and actions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* G8: show Plug-in Library toasts without a Roto job; cache preview frame checks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* G8: log Plug-in Library packages that fail to load at start-up
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* G8: check an update before replacing a loaded package; Update All past failures
- An update to a package that is already loaded is now compiled and its manifest checked
(through the previewer, without registering it) before the new file is written and the old
version's folder deleted. Before, a release with the right SHA-256 that didn't load replaced
the working version and failed at the next start.
- Package ids that differ only in case, or end in a dot, would share one folder on Windows and
macOS: the catalog keeps the first of them, and ids can't end in a dot (schema too).
- Update All: a package that can't be picked or installed no longer stops the others; the
result names what was updated and what wasn't.
- Downloads: at most 5 redirects, and one that ends off GitHub's hosts is refused, as the module
docs say.
- A job that panics no longer leaves the library busy; poisoned status locks are used, not
dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* G8: rustfmt the update test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Add After Effects project import with bounded parsing, explicit loss reports, and regression coverage.
Fetch the cleaned external test corpus at commit 97262276f625afdeeb61787e4cbb74b3f17af510 and verify each fixture against the pinned SHA-256 manifest.
B44 and B44A pack only half floats, so at 32-bit float the menu and the
render log say the samples stay uncompressed. Half-float frames hold samples
above 65 504 at that limit instead of writing infinity. The EXR parameters
move into their own helper; exr_out is public for other writers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OpenEXR sequence output modules get two settings, exrCompression (none,
rle, zips, zip, piz, pxr24, b44, b44a, dwaa, dwab; scan lines in each
method's own block height) and exrBitDepth (half or float). The defaults
are ZIP and 32-bit float, so existing projects render the same bytes as
before. They go through the project file, Output Module templates, the
renderQueue commands and renderQueue.formats, the CLI (--exr-compression,
--exr-depth), the Output Module menu (lossy methods are marked) and the
render log. Half rounds each sample to the nearest half float.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A run reads the project, edits it and writes the whole file back, so two
at once silently dropped the edits of whichever saved first. A run that
saves over the file it opened (--save, or --save-as the same file) now
holds an OS lock on `.<name>.lock` next to the project from before its
read until it exits. The next run says so on stderr, waits, then edits
what the first one saved. Ported from filmcraft-cli
(storytold/filmcraft#3926).
The OS drops the lock however the process ends, so a crash leaves nothing
stale. If the lock can't be taken the run warns and saves as it did
before. A missing project stops the run before it opens anything, with
the same error opening gives, so a project created in between is never
saved without the lock. The desktop app and `mcp` don't take this lock.
camera.solveStatus said `solved: true` for any stored solve, but the solver
keeps the frames it cannot resect as unsolved and interpolates the camera
there. `solved` now needs every frame, the same check camera.exportBlender
makes, and `partial` marks a solve with gaps, next to `solvedFrames`.
The tracked frames lived only in memory, so a saved project lost them: a render in another
process (effectcraft-cli render-frame) segmented the frames again with the classic engine.
A propagate with a tracker now saves the frames it followed with the instance, in a hidden
parameter beside Freeze's (TrackCache: each frame's matte, Refine Edge band, colour models and
confidence, keyed by the inputs it was tracked from).
- They apply while the strokes and span they came from are unchanged, whichever model Settings
choose, so a project renders the same when it reopens, without the model loaded or chosen.
- A stroke or span change makes them stale until the next Propagate. A Propagate replaces them;
with the classic engine it leaves none.
- roto.status reports tracker.saved (the model they came from) and a notice when Settings
choose another model.
- The save at job completion is not an undo step (the frames are a cache of the strokes) and is
skipped if the strokes changed while tracking. Decoding checks the saved size, so a corrupt
project is ignored rather than overflowing.
Object Matte saves nothing. Projects saved before this change open unchanged (the parameter is
filled in on open).
Tests: a saved project reopens with the model unloaded and renders the saved frames through
Chain::ensure; choosing the classic engine keeps them until a classic Propagate replaces them; a
stroke change makes them stale. The SAM 2 tests and the failing-tracker test take the Roto test
lock.
Claude-Session: https://claude.ai/code/session_0136kZtXrYo5VUWEFxWduwUD
With SAM 2 (or MobileSAM) chosen, a propagate that started before the model had loaded ran
the whole job on the classic engine and reported nothing. The chosen model is now in use
before a job that segments with it starts (Roto Brush 2.0 / 3.0 and Object Matte):
- roto.propagate and the other Roto jobs wait for a load or download under way when called with
wait: true (bounded to 10 minutes). Without wait they refuse while the model is still loading,
and start the load in the background. A model that is not installed, or failed to load, is
refused either way.
- roto.status reports model {selected, installed, active, loading, error} and a notice while the
chosen model is not the one in use. source is "model" for a frame the model segmented on its own,
instead of "classical".
- Auto-propagate keeps edited instances pending while the model loads, and shows errors as toasts
instead of dropping them.
- Docs: what propagate does now; tracked frames live in the session that ran propagate and are not
saved in the project file, so a separate render process does not show them; roto.model.select
lists sam2.1-hiera-tiny.
Tests: a selected model that is not installed refuses to propagate (no weights needed); SAM 2
selected without wait is loaded before propagate (EFFECTCRAFT_SAM2). The SAM 2 tests and the
failing-tracker test take the Roto test lock, since they share the process-wide model slot.
Claude-Session: https://claude.ai/code/session_0136kZtXrYo5VUWEFxWduwUD
Apache-2.0 entry pinned to a Hugging Face commit with its SHA-256 and size,
listed in Settings > Roto Brush and installable with the existing verified
install flow. No weights are bundled.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Session::write_output (and refuse_export) is the one place that checks; Save Frame As
(PNG/PSD/EXR), Export Lottie, Save a Copy (+XML), Collect Files, Export Timeline (and its
pre-rendered movies), Export Template, Save Animation Preset, Export Guides and Export
Shortcuts write through it. The control channel's render.frame/ui.screenshot, MCP
render_frame and CLI render-frame --out check first (new query file.checkOutput for the
bridged cases). Save/Save As/auto-save are untouched.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Two queue items that resolve to one output (the same comp added twice, a duplicated item) replaced
each other's file inside one job. Nothing is on disk when the job starts, so no check said anything.
- A default output name (renderQueue.add, render.preRender, comp.saveFrameAs {queue},
file.createProxy) that another item of the queue writes, in any output module or segment, is
numbered like a footage collision: Main.mp4, Main_1.mp4, Main_2.mp4. Sequences are compared by
the frames they write, so two stills of one comp at different times keep their names. Names a
caller gave and names already stored in items are never changed.
- Outputs of one job that still name the same file (explicit names, renderQueue.duplicate, output
modules, segments) refuse the whole render before anything is written, naming both items.
Sequences clash only on frames both write; a later item with Skip Existing Files leaves the
earlier item's frames alone; items refused for footage take no part.
Refusing the whole render, not just the later item: which of two items should keep the file is the
user's to say, and failing only the later one would leave a half-rendered queue found after they
left. renderQueue.duplicate stays a verbatim copy so the clash is reported, not hidden.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
Since #2604 a picked numbered still imports as itself unless the import
asks for a sequence, as in After Effects. The compositing and tracking QA
scenarios import a PNG run, so they pass `sequence: true`; docs/agents.md
describes the new default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of the first commit found three places that still asserted the old cleanup: the UI integration
test (which would have failed CI), docs/agents.md, and the comments on TextEdit::created, layer.newText
and the engine test itself.
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>