Commit Graph
4418 Commits
Author SHA1 Message Date
Valentin KleinandClaude Opus 5.5 93cce09f3c .aep import: name the After Effects version in the report; 25.x and 24.x copies import like 2026 (#3136)
* AEP import: the report names the After Effects version of the file (#3115)

svap holds the version of the After Effects that wrote a project, head
bytes 4 to 7 the version the file is written for; Save a Copy As 25.x /
24.x of After Effects 2026 keeps svap and writes the older version into
head. AeVersion decodes the packed word (26.2.1x2), AepFile gains
file_version, saved_by_version and head_revision, and ImportReport gains
aeVersion (fileVersion, savedBy, tested; "unknown" when the file does not
say). A version outside 24 to 26 is imported as usual and gets a
limitation line, not a loss.

The 12 corpus families saved as 2026, 25.x and 24.x copies import to the
same project and report; docs/aep-format.md lists what the copies change
in the container. The fixture tests are ignored until the corpus holds
the files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* AEP import: doc comments on the version tests and helpers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* AEP import docs: tested is a major-version classification from copies saved by After Effects 2026

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* AEP import docs: the importer reports detected losses, not every loss

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* aep tests: compare the imported settings across version copies too

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:36:41 +01:00
Valentin KleinandClaude Opus 5.5 ab3674cc2b .aep import: map HDR Highlight Compression, Echo, Linear Wipe, Offset and Color Stabilizer (#3119)
* AEP import: map HDR Highlight Compression, Echo, Linear Wipe, Offset and Color Stabilizer

The importer reported these five effects as lost. They now become
EffectCraft's effects of the same name with every parameter After Effects
stores (ranges, defaults and popup items from our own After Effects 2026
project, documented in docs/aep-format.md 3.7):

- ADBE HDR ToneMap -> ec.utility.hdrcompression (Amount)
- ADBE Echo -> ec.time.echo (Echo Time, Number Of Echoes, Starting
  Intensity, Decay, Echo Operator by item name)
- ADBE Linear Wipe -> ec.transition.linearwipe
- ADBE Offset -> ec.distort.offset (Blend With Original is a stored 0..1
  fraction, a percentage here)
- ADBE Deflicker -> ec.color.colorstabilizer (fresh Stabilize default
  Levels, points, Sample Size)

A table row can name the range EffectCraft's effect works in
(ParamMap::limit): Echo values or keys beyond 255 echoes or a Decay of 1
are imported as stored and reported.

Tests build each effect into the committed synthetic.aep (fx_inject.rs,
which also checks its pard layout reads back). On the sample project the
effect losses go from 75 to 3 (the out-of-range Echo values).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* AEP import: doc comments on the quick-win test helpers and tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* AEP import: Echo keyframe regression for the range loss

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:36:34 +01:00
Valentin KleinandClaude Opus 5.5 fee50dbfd1 Scripting: After Effects match names for 14 effects and Casts Shadows (#3122)
* #3113: use After Effects' effect match names in scripting

Checked every row of crates/script/src/matchnames.rs against After Effects
26.2 (app.effects, canAddProperty and the layer property trees). Fourteen
effect names were wrong: twelve did not exist (Pixel Motion Blur is
ADBE OFMotionBlur, Shadow/Highlight is ADBE ShadowHighlight, Ellipse is
ADBE ELLIPSE, ...), and Noise and Photo Filter pointed at After Effects'
hidden obsolete versions (now ADBE Noise2 and ADBE PhotoFilterPS). A
light's Casts Shadows is ADBE Casts Shadows.

The old names stay accepted for lookup (addProperty, property(), and
parameters such as "ADBE Noise-0001"), so existing scripts keep working;
.matchName emits only the After Effects name. A names-only fixture of
After Effects' first-party match names backs the new test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Scripting: match names that changed only in case still find effects and parameters

property("ADBE Ellipse") and "ADBE Grow Bounds-0001" found nothing after the names became
ADBE ELLIPSE / ADBE GROW BOUNDS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:36:28 +01:00
Valentin KleinandClaude Sonnet 5.5 9408d70da0 Disk cache: stamp footage files for the salt off the UI thread (#3106)
* Disk cache: stamp footage files for the salt off the UI thread

Importing an After Effects project whose footage includes long image
sequences on a network share froze the desktop app for 15-27 s. The
import itself takes about 0.25 s; the time went into the edit's bump(),
where refresh_disk_salt() stats every footage file (13,466 sequence
frames in the measured project, ~15 s) on the UI thread whenever a disk
cache is attached. File > Open and footage reload hit the same path.

With Session::disk_salt_background (set by the desktop app) the salt is
taken on a worker thread and picked up by poll_disk_salt() from the UI
tick. Until it lands the layer cache runs memory-only, so keys under the
old salt (other files, or another project's items) are never read. A
newer footage change supersedes a salt still being taken. Headless
sessions keep taking it synchronously.

A settings change that keeps the disk cache folder no longer re-stamps
every footage file either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Disk cache: opening a project drops the footage salt being taken

A salt job still running for the old project was kept when the new project named the
same footage, so the salt stamped before the files changed stayed in use.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:19:27 +01:00
Valentin KleinandClaude Opus 5.5 9fc73122ea Home screen: leave it when a menu command opens a composition (#3092)
* Home screen: leave it when a menu command opens a composition

File > Import > After Effects Project... from the Home screen imported the
project (the toast said so), but the Home screen stayed on top, so it
looked as if the import had done nothing. Only the Home screen's own
buttons left it.

run_engine now leaves the Home screen when a command succeeds and changes
the active composition to one that is open, the rule scheduled script
tasks already follow. Frontend events run afterwards, so Help > In-App
Tutorials and New Project from Template still show it. A command that
opens no composition (or fails) keeps it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Home screen: also leave it for a project that replaces one with the same composition id, a picked file and the New Composition dialog

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:19:21 +01:00
Valentin KleinandClaude Sonnet 5.5 549a4d0540 Import: EDL clips without a SOURCE FILE link the file beside the list (#2836) (#3019)
* interchange: probe EDL media the reader flags offline (#2836)

An EDL line without a SOURCE FILE comment only names its clip. The reader
guesses the path beside the list and flags the media offline, and the
importer never probed offline media, so the file next to the EDL stayed
missing. Offline media is probed like the rest; a path that is not there
still becomes a placeholder.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* interchange: probe guessed paths of EDL media only (#2836)

An OTIO missing reference also gets a path built from the clip name and
is flagged offline, so a same-named file beside the document would have
replaced the intended clip silently. Only EDL imports probe offline media.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* interchange: confine guessed EDL paths to the list's folder before probing (review point)

CodeRabbit: an EDL clip without SOURCE FILE gets a path from its clip name; a
name like ../x.mov or an absolute one could link a file outside the EDL folder,
and rebase_to_file_timecode probed it too. Such guessed paths are now not
probed and stay placeholders; SOURCE FILE paths are kept as before.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 17:19:14 +01:00
Valentin KleinandClaude Sonnet 5.5 ae068ff931 EXR export: write the output space's chromaticities attribute (#3038)
* EXR export: write the output space's chromaticities attribute (#3037)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* EXR chromaticities: tests for unmanaged and PQ ProEXR, every space once

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* EXR export: note why the comp background is added before decoding (review point)

CodeRabbit asked to convert Comp.background to the output space before
compositing. Every output path (Image::to_rgba8_over) adds the background to
the output-encoded frame as it is, so the EXR now matches the other outputs;
converting would make the EXR colour differ from the PNG of the same comp.
Comment only.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Format the merged code with rustfmt

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 17:19:05 +01:00
Valentin KleinandClaude Sonnet 5.5 ee7c51740d effects: Write-on and Stroke Brush Size goes up to 200 px (#2980) (#3021)
After Effects 2026 accepts 0 to 200 for Brush Size of both effects (a
script setting 201 is refused with "out of range 0 to 200"); the range
here ended at 50. The slider keeps its 0 to 50 travel; larger values are
typed.

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 17:18:58 +01:00
Valentin KleinandClaude Opus 5.5 0f94a1f983 Skip the startup update check when started for an agent (#3097)
* Skip the startup update check when started for an agent

An app started with --control or EFFECTCRAFT_CONTROL_PORT still checked
for updates at start, and the modal "newer version available" prompt
blocked agent runs until a person dismissed it. Agent launches already
skip crash recovery and the Home screen; now they skip the startup update
check too. Help > Check for Updates is unaffected (it goes through
updates_ui::check / check_requested, independent of the startup flag).

Documents what a control-port launch skips in docs/control-protocol.md and
the main.rs module doc, with a unit test for the startup decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Control protocol docs: a window-start failure still shows a blocking message box

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 17:18:52 +01:00
Valentin KleinandClaude Sonnet 5.5 779df34e13 contributors: set names for vangel76 (#3041)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 17:18:46 +01:00
FredApps 8be83ab559 Merge pull request #3162 from storytold/house-rules
Copy the shared house rules into AGENTS.md
2026-11-06 14:59:41 +01:00
FredApps 357c72ea33 Copy the shared house rules into AGENTS.md.
The blocks come from craft-contrib at dde09ea31e2e457f5b4653a6e76f7cdf55944bdf. CLAUDE.md imports AGENTS.md. The house-rules workflow checks the copy.
2026-11-06 11:50:01 +01:00
ash 7fc1be0bf2 Report offline footage and recover automatically when media returns (#3161)
* 3118: report and recover offline footage consistently

* 3118: preserve manual reload and refresh data watch paths

* 3118: document availability and reload contracts
2026-11-06 11:38:57 +01:00
vangel76 924c74f436 Merge remote-tracking branch 'upstream/main' into aep-import-lumetri
# Conflicts:
#	docs/file-format-parity.md
2026-11-06 10:59:50 +01:00
Valentin Klein ce9eaec650 Add After Effects project export (#2594)
Add After Effects project export with chunks built in code, explicit loss reporting, engine and UI integration, and regression coverage.
2026-11-06 09:23:16 +01:00
ash bc4b96a5fc Reveal newly created mask controls in the Timeline (#3149)
* 3145: Reveal newly created mask controls in the Timeline

* 3145: Document mask reveal contracts and regression cases

* 3145: Make stale-event regression cases exhaustive
2026-11-06 08:53:04 +01:00
FredApps 3102b6e632 Merge pull request #2915 from vangel76/perf/exr-output-options
Render Queue OpenEXR: choose compression and half or 32-bit float
2026-11-06 02:26:23 +01:00
FredApps e2010893d2 Merge pull request #3130 from vangel76/cryptomatte-matte-only
Cryptomatte: Matte Only puts the matte in RGB with alpha 1, as After Effects does
2026-11-06 02:26:11 +01:00
vangel76andClaude Opus 5.5 19fa551459 Merge upstream/main into aep-import-lumetri
Take recorded_value from #3134 for ordinary effects in import_mapped; the ValueRecord switch is gone (stored_value now reads default-state records too).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 00:58:18 +01:00
vangel76andClaude Opus 5.5 4934de892d Merge upstream/main into cryptomatte-matte-only
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 00:53:40 +01:00
vangel76andClaude Opus 5.5 23e4778118 Merge aep-import-quickwins (#3119) into aep-import-thirdparty
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 00:53:04 +01:00
vangel76andClaude Opus 5.5 13b97e4e65 Merge upstream/main into aep-import-quickwins
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 00:49:49 +01:00
vangel76andClaude Sonnet 5.5 f9b860838c Merge upstream/main into perf/exr-output-options
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 00:49:29 +01:00
FredApps 3768ff40a3 Merge pull request #3134 from vangel76/aep-tdsb3-values
.aep import: read effect values stored as default-state records
2026-11-06 00:20:44 +01:00
FredApps d1e6393bce Merge pull request #3133 from vangel76/fx-hdr-compression
HDR Highlight Compression: match After Effects' compression curve
2026-11-06 00:17:21 +01:00
vangel76andClaude Sonnet 5.5 692cc52289 AEP import: Lumetri value records without their arbitrary data are reported as damaged
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 00:15:17 +01:00
FredApps 8b63d4308c Merge pull request #2916 from vangel76/fix/exr-scene-linear-output
EXR export: no compand / tone mapping, and the output space's own curve (#2913)
2026-11-06 00:12:32 +01:00
FredApps 39af86e9f4 Merge pull request #2929 from vangel76/perf/aux-footage-hint-474
perf(media): tell the media pool which EXR footage effects read (#474)
2026-11-06 00:12:06 +01:00
FredApps 35ce9b2694 Merge pull request #2920 from vangel76/perf/exr-decode-cache-474
Multi-layer EXR: one decode per frame shared by the picture and EXtractoR / Cryptomatte, picture-only decode for footage alone (#474)
2026-11-06 00:10:05 +01:00
vangel76andClaude Sonnet 5.5 0f88236884 AEP import: negative cases for EXtractoR and IDentifier channel names and a missing Channel Info payload
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 00:05:54 +01:00
vangel76andClaude Sonnet 5.5 c5c7dc7f3b Merge #3119 branch into aep-import-thirdparty
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-06 00:04:55 +01:00
vangel76andClaude Sonnet 5.5 c32aa7cfbc AEP import: Echo keyframe regression for the range loss
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 23:58:35 +01:00
4a89e68f9f Content-Aware Fill: spill the range and stream the output (#2570 phases 2-4) (#3079)
* Content-Aware Fill: bit-pack holes, spill frames to disk, stream outputs (#2570 phases 2-4)

* Content-Aware Fill phases 2-4: bit-pack holes, spill frames, stream outputs (clippy clean)

* Address CodeRabbit review: spill safety, streaming correctness, perf, memory guard (#2570)

* Address the remaining CodeRabbit review findings on Content-Aware Fill (#2570)

A non-streaming fill returned empty frames, progress could jump backwards between
the sink and the fill, the memory guard ignored the membrane frames still resident
for the flow pass (and wasm's in-RAM copies), and the GPU fill stages ran outside
an error scope, so an OOM retired the shared context instead of falling back to
the CPU for that frame.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* Content-Aware Fill: sample spilled frames without decoding them, check spill errors (#2570)

Tracing sampled spilled frames by decoding whole frames under the shared cache lock, so
parallel traces across neighbouring frames evicted each other and mostly paid a full-frame
read while every other thread waited. Sample only the 2x2 bilinear support with positioned
reads on an open handle instead, and report a failed spill read in the no-hole branch
rather than streaming black frames as a successful fill.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>

* Content-Aware Fill: spill membrane/propagated frames, unquantized input, surface scratch errors (#2570)

Flow and propagation now write their intermediate frames to scratch spills beside
the output instead of holding the whole range as float RGBA, and the input spill
uses 32-bit floats whatever the project depth so the fill sees the same values as
the old in-memory path. Spill reads/writes and scratch creation report through a
shared error slot that run_plan surfaces instead of masking disk failures as a
cancellation. Sampling uses positioned reads; the guard models the packed masks
plus per-thread scratch.

* Content-Aware Fill: report a membrane-spill write failure instead of cancelling (#2570)

* Content-Aware Fill: include the flow fields in the memory guard (#2570)

* Content-Aware Fill: scale the memory guard with workers and check scratch space (#2570)

The guard's scratch term was a fixed 8 frames, which under-counted a wide
machine with Lighting Correction by several times. It now models the sequential
synthesis stage plus a few frames per worker (from available parallelism) and
takes Lighting Correction into account.

Native fills write three f32 copies of the range to scratch beside the output,
so a range that cannot fit on that volume is now refused up front with the
free space in the message, before any frame is rendered.

Tests cover the worker/lighting budget, the scratch formula, a shared error slot
between the input and secondary spills, an uncreatable spill folder, and the
engine surfacing a spill failure as an error instead of a cancellation.

* Content-Aware Fill: resolve a relative output folder for the scratch check (#2570)

A bare relative output name has an empty parent, so the walk-up found no
existing ancestor and skipped the free-space check. Resolve it against the
process directory first, and cover that in the test.

* Do not reformat the GPU fill tests in the spill branch (#2570)

The file was already not rustfmt-clean upstream, so the formatter only added
noise to a file this PR no longer needs to touch: phase 1's call-site changes
are already on the base branch.

* Content-Aware Fill: measure peak RSS and correct the per-worker scratch model (#2570)

The ignored measure_peak_rss_of_a_fill test fills a spilled 640x360 range and
reports VmHWM. On this 18-worker machine 90 frames peak at 340-470 MB across
runs (35 MB with RAYON_NUM_THREADS=1) and 600 frames at 582 MB, so the scratch
slope is ~7 frames a worker, not the 2 the first guard revision assumed.
Scaling the summary down showed a clean linear slope at 8 and 4 workers, and
Lighting Correction stayed below the flow stage's peak.

Budget 8 frames a worker (9 with Lighting Correction), just above the measured
top of the range, and adjust the guard test to the real numbers. At 600 frames
the measured peak is 582 MB against ~11.1 GB for the old five-copies model, a
19x reduction.

* Contributors: credit Madhav Agarwal in the About window

Adds the self-chosen real and display name for @gmrmad1. The generated
contributors.json picks it up once the PR is merged.

* Address CodeRabbit: size the guard from the rayon pool; migrate GPU tests (#2570)

fill_workers used available_parallelism, but the parallel fill stages run on
the rayon pool, which RAYON_NUM_THREADS can size larger. Use
rayon::current_num_threads so the scratch budget follows the threads that
actually run; wasm keeps rayon's single-thread fallback.

The GPU inpaint tests still used the pre-spill FillInput API. The earlier
'do not reformat' revert dropped their API migration along with the
formatting, leaving cargo test -p effectcraft-gpu uncompilable. Restore the
Frames::Memory / HoleMask / sink migration and verify the 15 inpaint tests.

* Fix gate: flatten the wasm prop lookup and keep the budget wrapper test-only (#2570)

* Address CodeRabbit review: spill error check, global-pool preflight, docs (#2570)

* Include RAM-backed spill in the preflight and guard streaming reads (#2570)

* Resolve relative spill paths, prefer stacked mounts, guard the Edge Blend sink (#2570)

* Make the fill spill anonymous and test the streaming claims (#2570)

The spill was a named file in the output folder that only Drop removed, so
an out-of-memory kill or a force-quit left a multi-gigabyte
effectcraft-fill-* file in the user's project folder, and a failed set_len
after keep() leaked it too. The spill is now an anonymous temporary file
(tempfile_in), which the system reclaims when its last handle closes; every
read and write is positioned on that one handle (pwrite/seek_write join the
existing pread/seek_read), and the folder is kept only so the propagated
frames spill beside the input.

Also:
- Edge Blend checks for a failed read before handing a frame to the sink,
  so a black fallback frame never streams.
- The propagation workers stop as soon as the spill records an error.
- HoleMask::get/set use get()/get_mut(), so a mask whose public words are
  shorter than len cannot panic.

New tests cover the PR's headline claims: a spilled range fills
bit-identically to the same frames in RAM (returned and streamed), the sink
sees frames strictly in order, a truncated spill fails with a recorded
error and streams nothing, a failure mid-stream stops before the next frame,
propagation stops after a failure, the spill has no name in its folder, and
HoleMask round-trips at word boundaries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Content-Aware Fill: clean up a stopped fill and count two scratch copies (#2570)

- A fill that fails or is cancelled after frames started streaming deletes
  the PNGs it wrote, and the output folder when the fill made it and it is
  left empty (OutputFiles, dropped after the spill so Windows can remove
  the folder).
- After a successful fill, run_plan checks that every frame has a file, and
  apply_plan/add_fill_layer return an error on an empty range or file list
  (a job worker's reply) instead of indexing [0].
- scratch_need charges two f32 copies (32 B/px): flow_pairs drops the
  membrane spill before the propagated one is created, so the third copy
  never coexists.
- The spill-creation error carries a stable "spill: cannot make the scratch
  file" marker; the panel test asserted on the OS wording, which on Windows
  is "Cannot create a file when that file already exists".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Content-Aware Fill: a stopped fill leaves files it didn't create (#2570)

A fill into a given outputDir that already holds an earlier sequence
overwrites its frames as it goes (as before). The cleanup of a failed or
stopped fill deleted every path it wrote, including those earlier frames, so
a layer still using that sequence lost them. It now deletes only the files
the fill created (CodeRabbit on #3079).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 22:51:11 +01:00
skylar taylor-barrick a408d990b2 Export: use conformant ProRes alpha run termination (#2225)
* #293: use conformant ProRes alpha run termination

Pin the direct ProRes encoder to FilmCraft e4be1577 while preserving all other codec pins. Add a full MOV export regression for final alpha runs across both 4444 profiles, alpha modes and blur settings.

* #293: document ProRes alpha export regression

* export: repin FilmCraft to the merged ProRes alpha fix and move the round-trip regression into roundtrip.rs

All FilmCraft deps pinned to d5ead4ba (squash merge of filmcraft #2991). H.264 pinned movie bytes move 104341 -> 104342: only the encoder version string in the SEI (0.8.0 -> 0.12.0) and five size fields change.
2026-11-05 22:45:23 +01:00
MarekSpodBiedryandskylar 7ce61c68fc Grow the buffer to the pinned quad so Corner Pin draws outside the layer rectangle (#1938) (#1967)
* Grow the buffer to the pinned quad so Corner Pin draws outside the layer rectangle (#1938)

* Corner Pin: grow through pad_sides, skip adjustment layers, clamp to the pixel budget (#1938)

grow_sides returns the four sides for Buf::pad_sides on both paths, replacing grow_to/regrow and the inline GPU copy. Growth is skipped on adjustment layers, cut proportionally to the shared pixel budget (and 16384 per side) instead of giving up, and covered by tests for a far pin, an adjustment layer and a degenerate quad.

* Corner Pin: give the grow_sides test its own doc comment (#1938)

* effects: Corner Pin keeps a finite buffer for non-finite pins and grows without popping as a pin moves far away

* effects: rustfmt the Corner Pin non-finite pin test

---------

Co-authored-by: skylar <hi@skylar.cc>
2026-11-05 21:52:10 +01:00
vangel76andClaude Opus 5.5 0a103c055c HDR Highlight Compression test: 20 PNG codes up to 1, 0.1 % above
The reference test now checks the bound the PR states in the code space of the renders
(the sRGB curve x 6553.5) instead of a linear 2e-3, which allowed about 28 codes at the dark end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 19:31:17 +01:00
vangel76andClaude Opus 5.5 e25025545a AEP import: an effect record is read only in the written or the default state
A record in another tdsb state is reported as damaged data again, as before the
default-state change. The synthetic test checks Levels' recorded popup directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 19:28:55 +01:00
vangel76andClaude Opus 5.5 495221aac6 AEP import: doc comments on the quick-win test helpers and tests
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 19:28:55 +01:00
vangel76andClaude Sonnet 5.5 ab6e76973a ProEXR test: a non-sRGB output curve (Rec. 709) decodes with its own curve
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 18:47:54 +01:00
vangel76andClaude Opus 5.5 3a9188e0fb HDR Highlight Compression on the GPU: a NaN Amount keeps the layer, as on the CPU
Doc comments on the touched helpers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 18:11:29 +01:00
vangel76andClaude Opus 5.5 337299ddf6 AEP import docs: Lumetri wheel render figure measured at the same pixel
The EffectCraft value for the Shadows wheel's blue lift at black was read at input 0.05; at input 0 it is 0.17 (After Effects 0.13).

Part of #3111

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:50:28 +01:00
vangel76andClaude Opus 5.5 6a67b4dac1 AEP import: Lumetri Color's master RGB curve, Hue vs Sat and colour wheels
After Effects 2026 presets made by hand (1200 lumetri-58 ... -62) decode
the data the scripted samples could not set.

- RGB curve (-0039): the first of the four curves is the master curve
  and arrives as curves/rgbCurves/master. A moved red, green or blue
  curve is reported: no sample tells which slot is which channel.
- Hue vs Sat (-0042): y is the offset from neutral, 1 doubling the
  saturation (in linear light), so EffectCraft's y is 0.5 + y / 2. The
  other four hue / saturation curves stay reported when moved.
- Color Wheels (-0047): four triples of hue (degrees), brightness slider
  (0.5 neutral) and distance. Shadows, Midtones and Highlights become
  EffectCraft's wheel colours (HSL of that hue, saturation = distance,
  lightness 0.5); the brightness sliders and the fourth wheel are
  reported.
- Damaged curve or wheel data is reported as damaged.
- Legacy Tone And Saturation is reported when a record says on: its pard
  default turns from off to on when the project is saved again (the
  25.x / 24.x / 26 copies of 1200), so comparing with it missed it.
- The Input LUT and Look presets confirm -0095 / -0096 hold the names;
  the LUT files are After Effects' own and are not embedded.

Evidence: the 62 read-backs still equal the imported values in the new
63-composition 1200 file. The version copies store the same Lumetri
definitions, ranges and values and give the same report. Renders: master
curve within 0.024 (After Effects uses a natural spline, EffectCraft's
Lumetri a monotone one), Hue vs Sat mean 0.008, wheels mean 0.042.

Part of #3111

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:48:29 +01:00
vangel76andClaude Opus 5.5 9c25944c70 AEP import: EXtractoR's and IDentifier's chosen channels by name
Presets of our own (blocks 1300 and 1310) show what the Channel Info stores for a chosen
channel.

- EXtractoR: each record is a kind in byte 0 and the channel name. Named channels are
  imported by name (Z, Z, Z renders equal to After Effects) and `(copy)` as the input's
  own channel. The kind is byte 0 only: one definition default has `02 00 00 32`.
- IDentifier: byte 19 holds the chosen channel's name, `Object ID` when none is chosen.
  After Effects lists only UINT channels and leaves the layer unchanged without one, which
  the loss now says. A chosen `ObjectID` / `MaterialID` channel (any case) is the one
  EffectCraft reads itself, so it imports without a loss; other names are reported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:45:19 +01:00
vangel76andClaude Opus 5.5 af7044d7db HDR Highlight Compression: check the curve at Amount 25, 50 and 75 (#3123)
New After Effects 2026 renders of the same EXR at Amount 25, 50 and 75
(our own 1110-extra sample, 16-bit PNGs) confirm the transfer curve that
was fitted at 84, 90 and 100. The worst channel error over all pixels is
4.0, 5.5 and 4.3 of 65535, against 997, 2415 and 3867 for the old soft
knee. The code is unchanged.

The measured-reference test now covers all six Amounts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:39:00 +01:00
vangel76andClaude Opus 5.5 b9182972b2 HDR Highlight Compression: match After Effects' measured transfer curve (#3123)
The effect used a soft knee on the brightest channel and left everything
below the knee unchanged. After Effects compresses the whole range. Fitting
our own 16-bit renders of an HDR ramp and coloured patches (Amount 0, 84,
90, 100) gives:

  Y    = max(0.30 R + 0.59 G + 0.11 B, 0)
  comp = min(c / (1 + Y), 1)                    per channel
  out  = ((1 - t) c^0.4 + t comp^0.4)^(1/0.4)   t = Amount / 100

A grey value x maps to x / (1 + x) at Amount 100. The worst error against AE
drops from 985 to about 2-4 of 65535 at Amounts 100, 90 and 84. Amount 0
stays identical.

CPU and GPU change together. Tests: a reference table of measured values,
GPU parity at Amount 90, and over-range input that exercises the clip at 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 17:02:26 +01:00
vangel76andClaude Opus 5.5 e730da1435 AEP import: read effect values recorded in the default state (tdsb 3)
After Effects records some effect parameters as value records in the
default state (tdsb 3) that still hold the value it uses. A survey of 839
corpus and sample files finds 25 such effect records, all in Lumetri
Color (its hidden controls) and Cryptomatte (Selection Mode on); every one
differs from its pard default and reads back in After Effects as recorded.
No effect record in the default state holds a value that does not count.

The general reader now takes them: EffectParam::recorded_value (the same
name, signature and body as in the Lumetri import) reads a record whatever
its state, value() and is_default() use it, and the importer's mapped
parameters read it, so such a record imports like a written one instead of
being reported as an unreadable value with the default used.
stored_value keeps to written records.

Layer, camera, light, shape and text properties in the default state all
hold After Effects' defaults or values with no effect on that layer
(the point light cone, a polygon's inner radius, a camera's opacity), so
their readers are unchanged. docs/aep-format.md records both findings.

Closes #3126

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 16:51:53 +01:00
vangel76andClaude Opus 5.5 50cbbfe310 Cryptomatte: Matte Only puts the matte in RGB over alpha 1 (#3127)
With Matte Only on, Cryptomatte wrote the matte into all four channels:
a premultiplied white only as opaque as the matte. After Effects'
Cryptomatte plug-in shows Matte Only as an opaque grey picture, the
matte in RGB with alpha 1 (its Matte Only render of 2026-11-05 is opaque
black where nothing is selected). The CPU effect and the GPU shader now
both give (m, m, m, 1).

Projects saved before keep their render: a hidden Matte Only In Alpha
switch (off for new instances, presets and AEP imports) is turned on by
migrate::legacy_default when an older instance is upgraded on open, and
then keeps the matte in every channel on CPU and GPU.

Tests: the CPU path for a partly covered pixel, an unselected one and
outside the aux area, in both forms; the upgrade of a saved instance;
the GPU parity test over every display with Matte Only off, on and on
with the legacy switch; the host pick tests read whole pixels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:38:36 +01:00
vangel76andClaude Opus 5.5 ce3a5b3673 AEP import: map Lumetri Color (ADBE Lumetri) to EffectCraft's Lumetri Color
The importer now converts After Effects' Lumetri Color. Its parameters are
found by match-name suffix, not by parT index: After Effects inserted
-0127 ... -0130 mid-list and five tone sliders share one valid range.

- Values are read from every value record, also those in the default
  state (tdsb 3), where Lumetri keeps High Dynamic Range, HDR Specular and
  the second Look popup (EffectParam::recorded_value; stored_value is
  unchanged).
- A switched-off section, and HSL Secondary without a picked key colour,
  draws nothing in After Effects: its set values are reported in one line
  and not imported.
- The HSL key data is decoded into hue / saturation / lightness ranges.
- LUTs and Looks (by name), controls without a counterpart, eyedropper
  colours and undecoded curves, wheels and Split Toning are reported.
- EffectCraft's Lumetri sliders take After Effects' valid ranges.
- import_param is split into lookup plus import_mapped for reuse.

Evidence: 1200 samples (After Effects 2026). The 62 read-backs of mapped
controls in sections that act equal the imported values. The other 61
lie in sections that do nothing and are reported. Renders of identity
grades are within 1/255.

Part of #3111

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 15:25:42 +01:00
1786c24523 expr: an expression cannot rebind a prelude name for the next one (#2133) (#2176)
* expr: an expression cannot rebind a prelude name for the next one (#2133)

One runtime serves every expression on a thread and user code runs as a script
at global scope, so `linear = function () {}` rebound the name for every later
expression, and for every later project in the session. #1387 closed the `__`
globals, the model constructors and the prelude's own prototypes; the rest of
the global surface is here: the prelude's own 104 helpers, the built-ins that an
expression and the prelude both call, and the prototypes those hang off.

Freezing a prototype costs an expression the right to shadow an inherited name
on its own object: once the inherited data property is non-writable, `o.x = …`
is dropped in sloppy mode rather than creating an own property, which is the
override mistake. Every name that was shadowable before is therefore replaced
by an accessor whose setter writes an own property on the receiver, emulating
OrdinarySetWithOwnDescriptor so the substitution does not show through.

The repair covers every own writable, configurable data property of every
prototype the block reaches, not a list of commonly shadowed names: a list
leaves `a.map = …` throwing and `a.push = …` and `a.join = …` silently running
the native method, which is worse, because the expression keeps working and
quietly computes something else.

`name` and `message` on an error's prototype are the one exception, and they
are frozen rather than repaired. The host renders a thrown error by reading
both off the object with no context to call a getter with, and nowhere safe to
call one: the time budget is checked between polls of the evaluation and
formatting runs after that returns, so a getter of the expression's own would
run outside it, bounded only by the loop and recursion limits at some hundreds
of millions of iterations. `e.name = 'MyError'` is dropped in silence as the
price of keeping user code out of the failure path.

The setup code reads every intrinsic it needs before the first prototype is
rewritten, and indexes arrays rather than calling their methods. Repairing
`Array.prototype` turns `concat` and `forEach` into accessors, and an earlier
attempt at this used both while walking the remaining globals, which left
every prototype after `Array.prototype` unfrozen and the prelude failing to
load at all.

Measured on macOS arm64, release, min of 3 x N, against this base: an
inherited-name read costs ~68 ns where it is now an accessor, which is +18%
on a loop doing a hundred of them and +2% to +7% on ordinary expressions.
Extending the repair past `Object.prototype` is what the array-method read
pays for; nothing else moves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* expr: the prelude's lock answers delete and defineProperty too (#2133)

Tests for the two routes to a global binding the existing ones did not cover
against a prelude function, asked for in review. `writable: false` answers an
assignment and a `var`; it is `configurable: false` that answers these, and
they fail differently: a `delete` of a non-configurable property evaluates to
false in sloppy mode, while a `defineProperty` over one throws, so that is the
one route whose refusal the user sees.

Also records which mode decides that, because it was asked and the answer is
not the obvious one: an expression cannot reach strict mode at all.
`Runtime::script` wraps the text in `try{…}catch(__e){throw __e}`, so a
`"use strict"` the user writes is an expression statement inside a block
rather than a directive prologue and has no effect. A refused write is
therefore always the silent kind, never a TypeError.

No change to the prelude; these pass as it stands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* expr: freeze Math and JSON plainly instead of repairing them (#2176)

Math and JSON are namespaces, not prototypes: nothing inherits sin or
parse, so the accessor repair keeps no shadowing there and only puts a
getter call on every Math.* read of a per-frame expression. Freeze them
directly and lock the two bindings so Math = {} cannot replace them for
the next expression either. Prototypes keep the repair.

---------

Co-authored-by: Anubhav-Rai <adobearakap@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
2026-11-05 15:21:59 +01:00
FredApps b2eb3f7877 Merge pull request #3091 from ZIDAN44/fix/1826-marker-bin-double-click
Timeline: a double-click on the composition marker bin adds a marker at the current time (#1826)
2026-11-05 15:13:25 +01:00