Commit Graph
1271 Commits
Author SHA1 Message Date
Claude 3c063b5075 GPU: a failed allocation reported as an internal error, or a lost device, no longer crashes the app (#591)
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 22:54:56 +00:00
Claude 1c7fcccef2 Packaging: the Windows installer offers a desktop shortcut, on by default (#343)
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 22:30:58 +00:00
BFlat d024a16cf0 Merge pull request #667 from storytold/claude/community-pr-review-lwtrlr
Fix community-reported issues: Light theme contrast, macOS Sync with System, Timeline eyedropper, DX12 kernel fallback
2026-10-11 05:21:54 +07:00
Claude e615b06256 Merge branch 'fix/f6': community issue fixes
# Conflicts:
#	crates/gpu/src/tests.rs
2026-10-10 21:59:26 +00:00
BFlat 62ec101a41 Merge pull request #655 from storytold/claude/community-pr-review-lwtrlr
Fix community-reported issues: multi-layer edits, stacked-panel tabs, 32 bpc blend modes, and more
2026-10-11 04:49:01 +07:00
Claude 9495810ff5 Tests: the near-plane check tests for the flip, and loopOut compares within float tolerance (#572, #456 follow-up)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 21:48:52 +00:00
Claude ef96419bec Appearance: the Light theme repaints Effect Controls headers, Timeline wells and the expression field (#644)
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 21:29:46 +00:00
Claude 82788b7a68 Multi-layer edits: only a real drag offsets the other layers; the Timeline stopwatch stays per property (#355, #491 follow-up)
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 21:28:25 +00:00
Claude 3f03e025ac Merge branch 'fix/f7': community issue fixes 2026-10-10 21:27:44 +00:00
Claude 657f34d40b Layers: with several layers selected, value edits, stopwatches and viewer Scale / Rotate change them all (#355, #491)
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 21:23:36 +00:00
Claude 03598229ba Viewer: 3D layer outlines and the axis gizmo clip at the camera instead of drawing long lines (#572)
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 21:23:23 +00:00
Claude 56fd5d52b1 Dock: a panel dropped on a stacked panel becomes a tab in its frame, as in After Effects (#302)
A stack (the Default workspace's right column) held one panel per frame. Dropping a panel
onto the middle or the tab strip of a stacked panel inserted a new stacked frame below it,
and dropping on its top or bottom edge split the whole column.

A stacked frame now holds tabs (`StackEntry { panels, active, open, height }`):
- Center / tab strip drops add the panel as a tab of the anchor's frame and show it.
- Top / Bottom drops add a frame of its own above / below the anchor's frame only.
  (Left / Right still split the column: a frame can't hold a side-by-side split.)
- Clicking a frame's shown tab (or its header bar) expands / collapses it; another tab
  shows that panel. Closing a tab keeps the frame; its last tab removes it.
- Saved workspaces from before (`{"panel": …}` per frame) still load.

Tests: dock unit tests (Center / Top / Bottom on a stack, insert_tab, close, old saved
entries); a kittest dragging Effect Controls onto Properties makes a two-tab frame whose
tabs switch the shown panel.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 21:22:38 +00:00
Claude e2a47495eb GPU: an effect kernel the shader compiler rejects no longer turns the GPU compositor off (#613)
GpuContext::new built every compute pipeline up front and failed as a
whole when one didn't build. On DirectX 12, FXC (wgpu's default D3D shader
compiler) aborts on the `pointwise` effect kernel, so the whole app fell
back to CPU compositing.

The compositor's own kernels are still required, but the effect kernels
(`pointwise`, the blur/glow/shadow passes and every fx_* kernel) are now
optional: one that fails to build is logged and listed
(GpuContext::unavailable_kernels), and a GPU effect chain that dispatches a
missing kernel reports it (Enc::take_missing) so those effects render on
the CPU while everything else stays on the GPU.

This does not make FXC accept `pointwise` (that needs a Windows/FXC box to
bisect); it limits the damage to the effects using it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 21:17:18 +00:00
Claude 1bcfe6912d Timeline: colour properties get an eyedropper after their swatch, as in After Effects (#633)
A shape's Fill and Stroke Color (and every other colour property) in the
Timeline only had a swatch that opens the colour picker; After Effects puts
an eyedropper next to it to sample a colour from the Composition panel.
The Timeline now shows that eyedropper (`timeline.prop.<uid>.eyedropper`),
reusing Effect Controls' viewer pick: click it, then click in the
Composition panel, and the property is set to the sampled colour.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 21:11:53 +00:00
BFlat 3b4036f536 Merge pull request #648 from storytold/claude/community-pr-review-lwtrlr
Land #641: GPU precomp layers, with follow-ups
2026-10-11 04:03:47 +07:00
Claude d3b26f2a65 Appearance: Sync with System keeps following macOS appearance changes after startup (#642)
apply_visuals pinned egui's theme preference to Light or Dark on every
apply. egui syncs that to the native window (ViewportCommand::SetTheme),
winit then gives the NSWindow an explicit Aqua/DarkAqua appearance, and a
window with its own appearance no longer receives the system's
ThemeChanged events, so Sync with System froze on the first frame's
appearance.

Our visuals now go into both of egui's styles (light and dark), and the
preference is System while the appearance mode is Sync with System, so the
window keeps following the OS; fixed Light / Dark modes still pin it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 21:00:37 +00:00
Claude ecb2cf1d32 Blend modes: classic modes fade with opacity and HDR modes keep over-range values, as in After Effects (#494)
Measured against After Effects at 32 bpc:

- Classic Color Dodge, Classic Color Burn and Classic Difference at partial opacity: AE
  fades the source toward the mode's neutral colour (0 for Dodge and Difference, 1 for
  Burn) by the opacity and applies the classic formula at full strength (b / (1 − k·o),
  |b − k·o|, 1 − (1 − b) / (1 − k·(1 − o))). We mixed the full-strength result by opacity:
  up to 45/255 off on ordinary 0–1 footage.
- Overlay, Exclusion, Luminosity, Color Burn and Classic Color Burn keep their plain formula
  for values above 1 (HDR passes). They were missing from `supports_hdr`, so their inputs
  were clamped to 0–1 (Exclusion was up to 208/255 off); Classic Color Burn also clamped
  its result at 0, where AE keeps negative values.

The CPU blend (color::blend_pixel) and the GPU kernel (common.wgsl) change together.

Tests: the reporter's formulas for each case (blend.rs); the CPU/GPU parity test of every
blend mode at every bit depth (over-range values, 70 % opacity) passes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 20:51:24 +00:00
Claude 1c9b5f1828 Graph Editor: the speed graph of a curved motion path is smooth, not jagged (#456)
Spatial properties move along their path by arc length: a table of 96 chords per segment,
read back by linear interpolation of the curve parameter. Within each cell that mapping is
linear, so the speed (the Graph Editor's speed graph, a central difference of the
position) jumped at every cell boundary and was off by up to ~2 % on a curved path: a
sawtooth over a constant-speed move, and a slight wobble in the motion itself.

The table now holds Gauss-Legendre arc lengths, and `at_length` refines the cell's linear
guess with Newton steps on the true arc length (|B'(u)| as the derivative), so position and
speed follow the path exactly.

Test: linear keys along a strongly curved path give a constant speed (within 0.1 %) at
every sample; the old mapping was off by 2 %.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 20:42:34 +00:00
Claude faffad3a6e Media: a multi-layer EXR's picture and its EXtractoR / Cryptomatte channels share one decode (#474)
A multi-layer OpenEXR file without an unnamed RGB layer (Blender's compositor File Output)
was decoded twice per frame: `layered_image` decoded every channel to find the picture,
then `MediaPool::aux` decoded the same bytes again into the aux-channel cache for
EXtractoR and Cryptomatte. The first EXtractoR layer doubled the frame time.

The footage path now reads the picture from the aux-channel cache (`cached_layered_image`),
so the decode it makes is the one EXtractoR and Cryptomatte reuse. (Step 1 of the issue;
reading only the picture's channels and keeping half channels as f16 are still to do.)

Test: after a frame of a layered file is decoded, its aux channels come from the cache
without reading the file again.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 20:40:50 +00:00
Claude 8c74fce5fa GPU: a declined precomp falls back for that layer only; the decline warning is once per comp and reason (#641 follow-up)
A nested comp too deep or too large for a GPU texture now takes the CPU
path for its layer, as before #641, instead of failing the whole frame;
only a device failure does. The warning no longer fires every frame
(which pushed every other warning out of the System Report's log), and
names the no-readback case and regions of interest.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 20:29:58 +00:00
Claude c773c26281 Merge pull request #641 from MatejKozlovsky/main 2026-10-10 20:29:22 +00:00
BFlat f8b2552d1c Merge pull request #645 from storytold/claude/community-pr-review-lwtrlr
Fix 28 community-reported issues (crash, security, AE parity)
2026-10-11 03:25:25 +07:00
Claude 5e2c7560d6 Import: only dropped files default to stills; scripts and agents naming a frame still get its run (#431)
The UI behaves as in After Effects (the Import dialog's Sequence checkbox
starts off, dropped files are stills, dropped folders make sequences), but
file.import without sequence or drag keeps importing a numbered still's
run, so existing scripts and agents keep working.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 20:15:24 +00:00
Claude 8b42f970b6 Merge branch 'fix/f4': community issue fixes 2026-10-10 20:00:37 +00:00
BFlat fddca8c6d9 Merge pull request #643 from storytold/claude/community-pr-review-lwtrlr
Land community PRs, batch 3, with follow-ups
2026-10-11 02:55:09 +07:00
Claude 8d32d67f0e Color: Project Settings ▸ Working Gamma 2.2 / 2.4 with no working space, as in After Effects (#495)
After Effects' `app.project.workingGamma` (2.2 or 2.4, ignored with a working space) sets
the curve an unmanaged project uses: at 2.2 the piecewise sRGB curve, at 2.4 a pure 2.4
power curve. EffectCraft always used sRGB, so linear EXRs (Interpret As Linear Light) from
a 2.4 project came out darker in the shadows (0.18 → 0.461 instead of 0.490).

- `ProjectSettings::working_gamma` (default 2.2, serde default for old projects).
- The colour pipe's unmanaged curve is the Working Gamma's (sRGB, or Rec. 709's pure 2.4
  curve): Interpret As Linear Light encodes untagged footage with it and Blend Colors Using
  1.0 Gamma linearizes with it; it is part of the pipe's cache key. The GPU path takes the
  same conversions.
- `file.projectSettings {workingGamma: 2.2|2.4}` (other values are refused, as in AE),
  `app.project.workingGamma` in scripts and a Working gamma choice in Project Settings.

Test: render test with linear 0.051 / 0.18 / 0.5 footage (0.290 / 0.490 / 0.749 at 2.4,
0.250 / 0.461 / 0.735 at 2.2, the values measured in AE), linear blending at 2.4, a working
space ignoring the setting, and the cache key.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:54:17 +00:00
Claude a220b63bfb Help: the System Compatibility Report says why there is no GPU and names the window's adapter (#596)
With no GPU compositor the report only said "GPU: none", though the host had recorded the
reason (`Session::accel_note`: the compositor's setup error, a retired GPU preview, a
headless start) and the window's wgpu adapter was known. Users with a capable GPU (#596, an
RTX 4070) couldn't tell why it wasn't used.

The report now carries `gpuNote` (the reason, when there is no compositor) and
`windowGraphics` (the adapter, backend, device type and driver the window draws with, which
the desktop app records when it sets up the GPU), and the text shows
"GPU: none (CPU compositing): <reason>" and "Window graphics: …"; the issue line names the
reason too.

Test: the compatibility report test sets a reason and an adapter and checks the JSON and text.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:54:17 +00:00
Claude 3effa9a2ea Timeline: the view follows the CTI past its edges when scrubbing and during playback (#468)
The ruler scrub set the time under the pointer and never moved the visible span, so with
the timeline zoomed in a scrub couldn't go past the edge, and a preview ran the CTI out of
view. As in After Effects:

- zoomed in, a scrub held past either end of the ruler scrolls the timeline that way,
  faster the further past, with the CTI riding the edge (repainting while held);
- during playback, when the CTI leaves the visible span the view pages to start at it
  (not past the comp's end).

Tests: a unit test of the paging and edge speed, and a kittest that holds a scrub past the
right end (the view scrolls, the CTI stays at the edge), then past the left end, then plays
from beyond the view (it pages to the CTI). (#468's Shift+F and black-layer items need
more information and aren't part of this.)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:54:17 +00:00
Claude 7b38d4d74e Dock: a click anywhere on a stacked panel's header bar expands or collapses it (#301)
In the right-hand panel stack only the tab (the name plus its padding) answered clicks, so
the rest of the header bar did nothing, a small target for short names like Audio. As in
After Effects, the rest of the strip now toggles the panel too and focuses it. A stack group
holds one panel, so the bar is unambiguous; the panel menu keeps its own click. The bar
registers the automation id `panel.header.<Panel>`.

Test: a kittest clicks near the right end of Preview's header bar (collapses), its middle
(expands) and its panel menu button (stays open).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:54:17 +00:00
Claude f440adbb5a Dock: dragging a divider past a panel's minimum no longer crashes the app (#306)
A split's first side was sized with `f32::clamp(20.min(avail), (avail - 20).max(0))`.
`f32::clamp` panics when its lower bound exceeds its upper one, which happened for any
split with 0 < avail < 40 pt. Dragging the viewer/Timeline divider up takes the area above
it down to 40 pt, so the splits nested there (the right column's vertical split and
others) got about 36 pt and the layout panicked; no guard covers egui's update, so the app
closed with no dialog.

The split size now goes through `split_first` / `clamp_ordered`, which never panic: when
the room is too small for both minimums the lower bound wins, and non-finite sizes fall
back to it. The gutter drag uses the same clamp.

Tests: a dock unit test lays out every workspace in rects too short and too narrow for its
minimums (it panicked before) and checks `split_first` on hostile sizes; a kittest drags the
divider above the Timeline 2000 pt up and 3000 pt down.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:53:56 +00:00
Claude 1bea11c9f1 Roto Brush: Cmd/Ctrl-drag resizes the brush, and the tool options show its Diameter (#509)
Cause: the Roto Brush and Refine Edge diameters live in the editor state and
the roto.options command sets them, but the UI never did: the Layer panel
only painted strokes and the bottom bar only set the view mode, so the brush
size could not be changed.

Fix, as in After Effects: Cmd-drag (Ctrl-drag on Windows and Linux) in the
Layer panel resizes the active brush (Roto Brush or Refine Edge) without
painting. The circle stays at the press point, its edge follows the pointer,
and the size is shown while dragging. The tools bar shows a Diameter field
(header.roto.diameter) while a Roto Brush tool is active. Both go through
roto.options, and the next stroke uses the new radius. Diameter is in all
four interface catalogs.

Test: roto_brush_ctrl_drag_resizes_the_brush (a 20 px Cmd/Ctrl-drag makes a
40 px brush and paints nothing, the next stroke's radius is half of it, and
the Diameter field is registered).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
(cherry picked from commit b4183c28eea34ba097d7a16153b84fcf7aeea045)
2026-10-10 19:48:25 +00:00
Claude 3ba43b20c7 Effect Controls: file parameters get Choose…, Apply Color LUT asks for its LUT on apply, and text parameters get a field (#514, #473)
Cause: Apply Color LUT's only parameter, `lut`, was ParamUi::Hidden (set only
through commands), so the effect could not be used from the UI. Effect
Controls also had no widget for any ParamUi::Text parameter (only EXtractoR's
channels had one), so Cryptomatte's Selection, the OCIO effects' config file /
file / config names and Colorama's Output Cycle could not be set either.

Fix: a new ParamUi::File { filters } for paths. Effect Controls shows the
file's name (None when empty) and a Choose... button that opens the host's
file dialog with those extensions and sets the path (prop.set, so agents use
the same command). As in After Effects, applying an effect with an empty file
parameter from the UI (Effect menu, Last Effect, drops, Effects & Presets)
opens the file dialog straight away; cancelling keeps the effect without a
file. Apply Color LUT's LUT (.cube) and the OCIO Config File (.ocio) and File
(.cube/.3dl/.csp/.cc/.ccc/.cdl/.spi1d/.spi3d/.spimtx) use it. Text
parameters get a single-line field that sets the value when it loses the
focus (Esc keeps the old text). Saved projects are upgraded on open: a
Hidden or Text parameter whose spec is now a File parameter takes the File
UI and keeps its path (effects::migrate). docs/effects.md regenerated.

This covers #473's first part (the Selection field); picking mattes in the
viewer, the layer popup from the file's metadata, the GPU path and quoted
commas are separate.

Tests: effects hidden_and_text_paths_become_file_parameters (migration); ui
file_and_text_parameters_have_widgets (the LUT is asked for on apply and
inverts red to cyan, the Choose... button picks another LUT, typing into
Cryptomatte's Selection sets it).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
(cherry picked from commit 403863f703999d19d3a83d9976f76bdc7577e682)
2026-10-10 19:48:25 +00:00
Claude 40e87709d5 Import: numbered stills import as stills unless the Sequence option is on; dropped folders make sequences (#431)
Cause: SequenceOptions::from_params defaulted `sequence` to true for
file.import, so any numbered still (IMG_0001.jpg) brought its whole numbered
run from its folder, and several picked or dropped stills with a common
prefix became one sequence. Dropped files never asked, and the Import
dialog's "<format> Sequence" checkbox started ticked.

Fix, matching After Effects: file.import takes picked and dropped files as
stills unless `sequence: true` (the Import dialog's Sequence checkbox, which
now starts unticked; Force Alphabetical Order implies it), while the numbered
stills inside a picked or dropped folder still import as sequences
(SequenceOptions::folders), and `sequence: false` (Alt-drag of a folder)
imports a folder's files one by one. Replace Footage, reload, proxies and
render outputs keep detecting their sequence (SequenceOptions::default and
from_params are unchanged), and mediaBrowser.import keeps bringing a numbered
still's run unless `sequence: false`. docs/footage.md and docs/agents.md
describe the new defaults.

Tests: sequence import_takes_stills_unless_asked_for_a_sequence and engine
numbered_stills_import_as_stills_by_default (one dropped still and two picked
IMG_ stills stay stills, a dropped folder makes one sequence, the checkbox
brings the run); the existing sequence tests pass `sequence: true`; ui
import_dialog_offers_the_image_sequence_option (unticked by default: a still;
ticked: the run) and a_dropped_folder_imports_as_one_sequence_with_sequence_interpretation
(two dropped frames: two stills; the folder: one sequence).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
(cherry picked from commit aa3c31db9ab3a774ee2f8ba867e1bafc1fce7aba)
2026-10-10 19:48:25 +00:00
Claude 86d47dc3fe Snapping and Align: shape layers meet at their visual edges, with no gap (#352)
Cause: render::content_bounds (used by snapping's targets and source, Align,
Distribute, the viewer's bounding box and handles) took shape layers' bounds
from draw_bounds, the rasteriser's safety rectangle: 1 px of anti-aliasing
padding around fills and width x miter limit / 2 + 1 around strokes. Two
squares snapped edge to edge therefore stayed 2 px apart, or much more with
strokes.

Fix: shapes::content_bounds uses visual bounds, as After Effects' layer bounds
do: the path bounds plus half the stroke width (through the draw's group
scale), with no anti-aliasing or miter padding. Rendering keeps draw_bounds.

Tests: render shape_content_bounds_are_the_visual_edges (a 100 x 100
rectangle is exactly +-50, +-55 with a 10 px stroke); engine
shape_layers_align_by_their_visual_edges (widths 100 / 110, Align Left puts
both edges exactly at 0).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
(cherry picked from commit bfd0abd7437ad1d38652e3aef51f9ba5dea9100b)
2026-10-10 19:48:25 +00:00
Claude 9b7f8cf005 Effects: Write-on draws a stroke along the animated brush, honouring Stroke Length and Brush Spacing (#536)
Cause: write_on_plan drew a single dab at the current Brush Position. Stroke
Length, Brush Spacing and the Paint/Brush Time Properties were declared but
unused, so an animated brush showed a moving dot that dropped every earlier
mark.

Fix: as After Effects describes Write-on, a dab is laid every Brush Spacing
seconds from the layer's start up to the current time, each lasting Stroke
Length seconds (0: for ever). Their positions come from the host's parameters
at their times (EffectHost::params_at, which the renderer implements). Paint
Time Properties > Opacity and Brush Time Properties > Size (or Size &
Hardness) keep each dab's own opacity / size; otherwise the current values
apply to all. One frame draws at most 4096 dabs (longer strokes space them
out and join them). Without a host the current dab draws, as before. Colour
and hardness stay one per stroke (the paint plan has one colour and hardness),
so Paint Time Properties > Color and per-dab hardness aren't modelled. The GPU
path uses the same plan.

Test: write_on_accumulates_a_stroke_along_the_animated_brush (the report's
fixture: start and midpoint marks stay at 0.5 s with the end still clear, all
marks at 1 s, Stroke Length 0.25 keeps only the recent trail, Brush Spacing
0.5 gives three separate marks, per-dab opacity).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
(cherry picked from commit c82c9aeb685eab0cb36ca69c0b337781f4bad258)
2026-10-10 19:48:25 +00:00
Claude 77dfd44658 Puppet: a pin added to a bent mesh holds it where it is; nothing jumps (#318)
Cause: puppet::solve binds each moving pin to its nearest mesh vertex and fixes
that vertex at position + (vertex - rest), an offset in rest space with no
rotation or scale. puppet.addPin set a new pin's position to the click, so in
a bent region the bound vertex was pulled to where it would be unrotated, and
the mesh jumped as soon as the pin was added.

Fix: addPin picks the position that keeps the bound vertex at its current
deformed place: the vertex's deformed position minus the rest offset. On an
unbent mesh that is the click itself (round-off is snapped back to it), so
existing pins behave as before.

Test: a_pin_added_to_a_bent_mesh_moves_nothing (two pins, one moved to bend
the layer, then pins added at three places on the bent part: at most a few
edge pixels change from solver round-off; before the fix about 250 did).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
(cherry picked from commit 16151203b304d565b425873fdd33ae082cae483b)
2026-10-10 19:48:25 +00:00
Claude 7e0e641606 Edit: Copy with a property selected copies that property, and Paste sets it on the selected layers (#493)
Cause: edit.copy handled text editing, selected keyframes, effects and shape
items, then fell back to copying the selected layers. Selecting a property
selects its keys, so an animated property copied, but an unanimated one (or
the Transform group, whose keys prop.select doesn't select) copied the whole
layer, and Paste added a duplicate layer instead of setting the property.

Fix: with properties (or the Transform group) selected, Edit > Copy copies, as
After Effects does, each animated property's keys (the selected ones, else all
of them, through keys.copy) and each unanimated property's value into a new
value clipboard keyed by match path. Edit > Paste sets those values on the
same properties of the selected layers (a key at the CTI where the target is
animated) and pastes the keys at the CTI, as one undo step (keys.paste now
honours `merge`). Keys alone still answer with their count, as before.

Test: copy_a_selected_property_pastes_onto_the_same_property (static
Position, then the Transform group with animated Opacity: values and keys
land on layer B, no layer is added, one undo step each).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
(cherry picked from commit 8267519ebe20e889196e538de23b85b04a1fa76a)
2026-10-10 19:48:25 +00:00
Claude 09b3dbaad9 Merge branch 'fix/f3': community issue fixes
# Conflicts:
#	crates/ui-egui/src/widgets.rs
2026-10-10 19:48:24 +00:00
Claude 33dc3e72fa Merge branch 'fix/f2': community issue fixes 2026-10-10 19:48:18 +00:00
Claude d3e34156e5 Merge branch 'fix/f1': community issue fixes
# Conflicts:
#	crates/ui-egui/src/panels/text_panels.rs
2026-10-10 19:48:18 +00:00
Claude fbe0f7e1ce Cache bars: cached frames show at 1/3 resolution (#595)
The cache bars count the frames of `shown_series`, which rebuilds the
series from the frame key's scale in thousandths (333 -> 0.333). The key's
`opts` hashed the render options with the f64 scale, and the frames had been
rendered at 0.3333..., so the series never matched and the bars stayed empty
at Third (and any resolution that isn't a whole number of thousandths, such
as Custom 1/7).

`opts_hash` now hashes the scale rounded to the key's thousandths, which the
key carries anyway.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:32:18 +00:00
Claude e96638093c Workspaces: saved workspaces last between sessions (#320)
Window > Workspace > Save as New Workspace and Save Changes to this
Workspace kept the layout only in memory (`UiState::saved_workspaces` /
`saved_floating`): nothing wrote them, so they were gone after a restart.

They are now written to `workspaces.json` in the settings store (the config
folder on desktop, OPFS on the web) when a workspace is saved, renamed or
deleted, and read back when the app starts; a file that doesn't read is
ignored.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:32:18 +00:00
Matej Kozlovský ced9e97c4a GPU: log why the GPU compositor declined a frame before the CPU renders it, instead of falling back silently (#640) 2026-10-10 21:17:51 +02:00
Matej Kozlovský 8321d099d2 GPU: precomp layers render their nested comp resident on the GPU instead of on the CPU and uploading it every frame (#640) 2026-10-10 21:17:51 +02:00
Matej Kozlovský 21b732875f Render: Renderer::precomp_source and run_layer_effects_on, so an accelerator can render a precomp layer's nested comp itself (#640) 2026-10-10 21:17:51 +02:00
Claude 485e4bb59d Interface: Hangul is probed after the other scripts, so a catch-all face never draws Arabic (#616 follow-up)
Without a Korean font the Hangul lookup can land on Unifont, which also
covers Arabic but can't join it; registered first, it took Arabic too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:17:26 +00:00
Claude 3bde85dfdb Timeline: Ctrl/Cmd+wheel and a trackpad pinch zoom time (#428)
egui turns Ctrl/Cmd+wheel and a pinch into a zoom factor (zoom_delta)
rather than a scroll, and the Timeline only zoomed on Alt+wheel, so
both did nothing over the layers. They now zoom time around the
pointer, like Alt+wheel. (B stays Set Work Area Start as in After
Effects; Split Layer is Ctrl/Cmd+Shift+D.)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:16:48 +00:00
Claude 69bf806ff5 Graph Editor: buttons that don't fit the bar move to a » menu instead of being cut off (#457)
The Graph Editor's bottom bar lays out 17 text buttons in one row
(about 1100 px) on a painter clipped to the graph area, so on a
normal-width Timeline the right-hand ones (keyframe interpolation and
easing) were cut off and unreachable. Buttons that would pass the bar's
right edge now go, in order, to a » menu at its end; picking one there
runs the same action on the next frame. Hidden buttons register as
timeline.graph.more.<id> while the menu is open.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:16:48 +00:00
Claude e152dfd1d1 Essential Graphics: Scale keeps its Constrain Proportions link, on by default (#338)
A Scale property added to Essential Graphics fell into the generic Point
control: two independent fields with no link, so dragging one value
distorted the layer, unlike After Effects, whose Scale control keeps the
chain link. It now draws the link (on by default) with one field per
axis; linked, an edit scales the others in proportion (Alt edits one),
using the Timeline's constrained() (moved to widgets). The link state is
the property's own, shared with the Timeline's chain link.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:16:47 +00:00
Claude 76c9abad50 Viewer: the 3D Renderer menu shows the comp's renderer and switches it (#307)
The Composition panel's bottom bar drew a hard-coded "Classic 3D"
dropdown that ignored the comp's renderer and did nothing when clicked,
so an Advanced 3D comp still read Classic 3D. It now shows the comp's
renderer (translated) and, as in After Effects, its menu switches it
through comp.settings (one undo step); the automation label is the
renderer's name.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
2026-10-10 19:16:36 +00:00