Resolve the two conflicts the panel catalogs and the Composition Settings
rewrite left:
- i18n/ui.rs: keep both sides' rows in the Simplified and Traditional Chinese
catalogs, which grew at the same anchor (the PR's 301 panel/dialog rows, then
main's 43 tool and Preview-panel rows). The tables are grouped by feature, not
sorted by key, so the two blocks sit one after the other. Both sides also added
"Frame Rate" once: keep one row per catalog, main's 影格速率 in Traditional
(matching the table's existing 影格速率:) and 帧速率 in Simplified.
- comp_settings.rs: take main's rewritten tab_body (it returns () and matches on
the tab, and show() measures the tabs) and re-apply the wrapping the old layout
had: the three tab labels go through tr(), as Basic, Advanced and 3D Renderer
already have rows. The old horizontal layout and its out accumulator are gone.
cargo test -p effectcraft-ui-egui --lib: 336 passed, 0 failed, 3 ignored.
The renderer applies every ancestor's transform, so a scaled null above a
camera already carries the camera's children. A Scale key with one shared
ease gets a separate Z ease before its speed is scaled.
Position speeds stretch with the path's direction at each key and Scale's Z
speed with the factor, so the ease keeps its shape. A 3D layer under a 2D
parent scales its own Z only when no 3D ancestor's Scale already carries it.
A 2D parent's Scale carries no depth, so its 3D children scale their own Z.
Percentage guides keep their percent exactly instead of drifting by rounding.
Tests now check the specific errors and cover a layer parented to a camera.
Like After Effects' File > Scripts > Scale Composition.jsx: a factor, or a new
width or height that keeps the aspect ratio, in one undo step. Layers whose
parent has a Scale follow that parent; nested precomps and expressions are
left as they are. No dialog yet.
Refs #2208
file.runScript and script.run moved from REFUSED to RUNS_SCRIPTS, which
check_command refuses too; the audit test now looks there as well.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Calculate frame_size from the original rendered dimensions and effective
render scale before cropping, so locked-aspect resize does not apply the
crop margins a second time in its dimension calculation.
Add dimension and decoded-PNG regressions for crop, resize, padding, scaled
ROI, both interpolation qualities, and zero or extreme requested sizes.
AI assistance: OpenAI Codex (GPT-6).
The job reports its times when a frame finishes, so with slow frames the
Current Render strip held the same Est. Remain long enough to look
stalled, then jumped. Between reports both times now count on from the
last one, as a clock does (remaining never below zero, a finished job
stands still), and the panel repaints twice a second while rendering.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the compositor built on a worker thread, the parity test's first
frame could be composited on the CPU while the build ran. Frame keys
don't name the compositor (by design: the disk cache serves frames from
either), so after the hand-over the viewer could keep showing that CPU
frame, and the GPU-vs-CPU comparison would pass comparing the CPU with
itself. The test now waits for the compositor, then opens the project
and sets the time, so every frame it compares was requested with the
compositor attached.
(`viewer_on_gpu()` can't tell here: on the desktop GPU frames are read
back into CPU images, and only the browser draws them from their texture.)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The status line is the toast that clears itself after 5 s, so the
preparing notice went blank while the viewer was still on the CPU for the
rest of a cold-cache build (tens of seconds), though `init_gpu` says the
status bar tells the user so. draw_toast now leaves that one status up
while `gpu_initializing()`; `finish_gpu_build` clears it when the
compositor lands, and the worker's repaint brings that frame, so the
toast doesn't tick meanwhile. A click still dismisses it; every other
status clears as before.
Test: with the build held on its gate, 30 more steps (7.5 s) leave the
status as it was; before this change it was empty by then.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since #2107, Replace Footage keeps an item's kind by default, so
replace() on a sequence item brought the new file's whole numbered run.
In After Effects, replace() swaps in the one file and replaceWithSequence()
brings a run; replace() now asks for exactly that.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The reader reports a track's media duration (mdhd). For H.264 with
B-frames that runs past the last shown frame by the encoder delay the
edit list trims, so a 75-frame 25 fps clip imported as 76 frames / 3.04 s.
The footage's duration now follows the video track's edit list when it
is shorter (never longer; a delayed start keeps the reader's duration),
as the frame rate is already corrected from the header.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The menu only carried Mute, so unmuting meant the timeline's context menu.
Add the second row bound to `keys.setMuted {"muted":false}`, its translation
in the ten catalogs, and a test that runs both rows through Session::execute
with undo and redo.
Add a muted flag to Keyframe. Muted keys stay in the Timeline and Graph Editor (dimmed, selectable and movable) but evaluation skips them, so the active neighbours interpolate as if they were not there; auto-Bezier and eases use the active neighbours. When every key is muted the property falls back to its static value.\n\nkeys.setMuted {muted} mutes or unmutes the selected keys in one undo step. The Timeline context menu offers Mute/Unmute Keyframes and the Animation menu has Mute Keyframes. Muted keys have no Bezier handles and motion-path points are dimmed.\n\nLottie export omits muted keys (with a warning) because Lottie has no mute state; scripting keeps seeing them in numKeys/key().\n\nTests: keyframe evaluation (linear, auto-Bezier spatial, all muted), serialization and value replacement, engine command + undo, Lottie export.
`init_gpu` built the compositor on the UI thread in the first frame, and eframe shows the window
only after that frame has painted. The compositor's ~235 compute pipelines take tens of seconds to
compile on a cold Metal shader cache (first launch, after an OS update, after an update that touched
a shader): about 40 s of no window at all, measured 39–41 s on an M2 Max.
On the desktop the compositor is now built on a worker thread (`gpu-compositor`), inside the same
`catch_unwind`, and attached when its result arrives; the worker requests a repaint, so nothing
polls. Until then the viewer composites on the CPU, as it already does without a GPU, and the
status line and `help.systemInfo` say "Preparing the GPU compositor…". A device failure during the
build discards the result; a thread that can't be spawned, and the browser, build in place as
before. `Hooks::before_gpu_build` lets tests hold or fail the build; `gpu_initializing()` exposes
the pending state.
Measured (release, cold cache, A/B interleaved): time to the window 39.0/40.9 s → 1.9/0.5 s, and
the control channel answers right after; with a heavy project opened at launch 38.3 s → 0.64 s
(first frame 88 ms after the app was created). Warm: 776 → 355 ms. The compile itself is not
faster; #1705 (effect kernels on first use) is the complementary half.
Tests: the interface draws, composites a CPU frame and reports the preparing state while the build
is held on a gate, then attaches the compositor; a build that panics leaves the viewer on the CPU
with the reason; the existing GPU/CPU parity test waits for the asynchronous hand-over.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The worker stores a report and then wakes the interface; the test took
the report as soon as it was there and asserted the wake count right
after, which can run before the wake. `next` now waits for the n-th wake
and then takes the stamps, as the engine tests' `wait_for` does.
Checked with 50 ms between the worker's report and its wake: this test
passes; the previous one fails at `wakes == 1`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two engine tests leaned on elapsed time. "It keeps stamping while the
window is in front" slept 300 ms and wanted more than three stamps: a
worker held up on a loaded machine stamps fewer. "Nothing watched,
nothing stamped" slept 60 ms after sending the empty set and then wanted
no stamp at all, but a worker already past its wait with the old set
stamps that round once more, whenever it gets to run.
The first now waits (at most 10 s) until the stamps arrive. The second
checks the invariant instead: after `set_watched(∅)` at most one round of
the old set (one file) can still come, so the count may grow by one and
no more; a worker that ignored the empty set stamps every 20 ms and still
fails it.
Checked with the worker slowed (100 ms after each wait, 50 ms between the
report and the wake): these tests pass; the previous ones fail at
"it keeps stamping while the window is in front".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Settings ▸ Import ▸ Reload Footage Changed on Disk used to spawn a stat thread every two seconds
from the UI thread, request a repaint 100 ms later to collect it, and request another repaint every
2 s in between, for every project with items, focused or not: the whole interface redrew on a timer
for footage that hadn't changed.
`footage_reload::Watcher` is one long-lived worker: the interface tells it what to watch (recomputed
when the project revision or the preference changes) and whether the window is in front; it stamps
the files every 2 s while the window is in front, every 10 s while another app has the front, at once
on focus regain and on a new set, and hands the stamps over — calling `ctx.request_repaint()` — only
when one differs from its last look. Nothing watched means the thread sleeps. `apply_stamps` and the
reload semantics are unchanged; `start_scan`/`poll_scan`/`Session::footage_scan` are gone. No new
dependencies: the stat loop is also what footage on a network share relies on, where FSEvents would
not report another machine's writes.
Tests: the watcher wakes the interface for the first stamps and for each change and never for an
unchanged file; behind another app it rests until the window is back; nothing watched is nothing
stamped and a new set is reported whole (and dropping it ends the thread); the host test
`the_watcher_notices_a_footage_file_changed_on_disk` saves a PSD from "another app" and sees it
reloaded once.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>