Its setup wrote the Timeline's old global reveal field, which no longer
filters on its own since reveals are kept per layer.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The navigator drew handles on both ends but the whole bar was one drag
area that scrolled. Each end is now a handle (timeline.navigator.start /
.end) that moves that side of the visible span while the other stays:
pulled in, the ruler zooms in; stretched to the track's ends, it shows
the whole comp again. The middle still scrolls at the current zoom.
; (timeline.zoomFrameToggle) zooms in to frame level around the current
time and, pressed again, back out to the whole comp. timeline.zoomFit
stays as the plain fit command (no shortcut). Both go through set_zoom.
Tests: navigator_ends_drag_to_zoom and
semicolon_toggles_frame_level_and_the_whole_comp (real pointer drags).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Alt+wheel's zoom took its lower limit from the current zoom rather than
the comp's fit and kept the ruler's start where the pointer had put it,
so on long comps it stopped zoomed out on a slice from the middle of the
comp (0:05:02–0:10:27 of a 10-minute comp). The = / - zoom used a
different limit again.
Both now go through set_zoom: the limit is the zoom that fits the comp
(fit_pps, the same formula the ruler and the zoom slider use), and
reaching it switches the ruler back to fitting the whole comp from 0.
Test: alt_wheel_zooms_out_until_the_whole_comp_shows (real Alt+wheel
events on a 10-minute comp); it failed before (Some(1.135) px/s, start
still offset).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Layer ▸ Time ▸ Enable Time Remapping (and the layer menu's entry) added
the property and its two keyframes but left the layer twirled closed, so
nothing visibly changed. When the command turns remapping on, the
layers that have it now twirl open on Time Remap, through the same
per-layer reveal as RR. Turning it off reveals nothing.
Test: enabling_time_remapping_reveals_time_remap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
U (and P, S, R, T, A, E, M, F, L) kept one reveal filter for the whole
Timeline and replaced the set of open layers with the selected ones, so
revealing on one layer closed every other layer and dropped their
reveals.
Each layer now has its own filter (TimelineState::layer_reveal; an open
layer without one shows its whole tree). A shortcut applies to the
selected layers (all layers with none selected) through apply_reveal and
leaves the others as they are; `reveal` stays the last shortcut's filter
for toggling and Shift-adding. U's results replace only the target
layers' earlier picks, so U after UU shows the keyframed properties
again. anim.reveal tells the Timeline which layers it looked at, so a
layer with nothing keyframed closes. Twirling a layer by hand, Ctrl+`
and Collapse All clear the filters they cover.
Test: reveal_shortcuts_leave_unselected_layers_alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#154: renderQueue.add (and render.addOutputModule) applied `format`, then
dropped it before applying the output path, so `--format hevc --out
x.mp4` saw the .mp4 extension and switched the module back to H.264
(and ProRes / VP9 for .mov / .webm). An explicit format now wins and a
mismatched extension follows it (x.webm → x.mp4 for AV1), as choosing a
format in the Output Module renames Output To; an extension that agrees
with the format keeps the name as given. The format error lists hevc and
av1 everywhere (one FORMATS string).
#155: a `comp` that names no composition (name or id) failed with "no
active composition". comp_id now reports it as `no composition "Nope"`
(EngineError::NoSuchComp), and comp_for_queue passes that on instead of
collapsing it.
Tests: an_explicit_format_wins_over_the_output_extension and
an_unknown_comp_is_named_in_the_error (engine). Checked with the CLI:
the files are hevc/hvc1 and av1/av01 and decode cleanly with ffmpeg.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With installed families whose names contain the query (JetBrainsMono
Nerd Font and its variants) the test expected exactly one match and
failed. It now checks that every match contains the query, the count
agrees, the query filters, and the bundled JetBrains Mono is there with
its Regular style.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Tools table gains history and script_ui, get_layer's depth,
add_keyframe's comp and the bridge screenshot's max_side and path.
agents_doc_tools_table_is_current (effectcraft-automation) fails when a
tool or an argument of its input schema is missing from the table, as
effects_doc_is_current guards docs/effects.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
How a release is cut (version bump, the gate, pushing main to release,
the draft, publishing), test runs, what gets built where, the Linux
glibc baseline behind the ubuntu-22.04 runners, and the optional signing
secrets. release.yml's two pointers now resolve.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A line that is not a JSON object with a string `method` (an HTTP request
line, other non-JSON text, a JSON array, invalid UTF-8, a line over 4 MiB)
now gets one error reply and the connection is closed, so a browser's
cross-origin fetch can't smuggle a command in its body. Connections are
capped at 16. Same transport as LightCraft (storytold/lightcraft#129) and
FilmCraft (storytold/filmcraft#123).
The ui_angles evidence (#141) moves next to G2's other regression
evidence; the Stability row notes the parent-chain hang, the label colour
panic and the contained GPU failures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#139's bounded read_buffer polled the device without waiting and slept up
to 10 ms between polls, adding up to 10 ms to every readback (the desktop
viewer reads every GPU frame back): the gpu crate's tests took 227 s
against 119 s before. It now waits with PollType::Wait and the remaining
deadline as its timeout (a timeout still retires the context), then takes
the result, giving another thread that polled the same device up to 1 ms
to deliver its callback. The tests take 71–79 s.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gaps.md records the seven reports of 6 October (#85, #88, #89, #93, #103,
#106, #117), what they found in features marked done, their fixes of 7
October and the regression tests; parity.md's Interface, Animation and
Preview rows, the ROADMAP's reliability note and the README's panels
paragraph (drag and drop between panels) follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Composition viewer ignored drags from other panels. Now, as in After
Effects:
- A Project item dropped on the viewer becomes a layer centred where it
lands (above the selected layer, like any new layer); while dragging, the
viewer is outlined and the layer's frame follows the pointer.
- Media Browser files do the same; files dropped from the OS file manager
do too when the platform reports where the pointer is during the drag
(otherwise they import as before).
- An effect from Effects & Presets goes on the layer under the pointer,
which is outlined while dragging.
layer.addItem and layer.newModel take `position` ([x, y] comp px).
file.import takes `addToComp` with `time`, `index` and `position`;
mediaBrowser.import shares it (file::add_to_comp): layered files go in as
their comps, the comp they were dropped in stays open, and items that
can't be layers are reported in `errors`. viewer::layer_at is the hit
test pick() and the drop share.
Tests: ui_drag_drop.rs (project item and effect dropped on the viewer)
and imported_files_go_into_the_comp_where_they_were_dropped (engine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Project item (or Media Browser file) dropped in the Timeline always
went above the selected layer, starting where new layers start.
Now, as in After Effects, a line shows where it goes in the stack (above
the first layer row whose middle is below the pointer, else below the
last), and over the time graph a marker with a timecode tag shows its In
point, which follows the pointer (frame-snapped) or, with Shift, the
current time. The layer reorder drag shares the insertion logic.
layer.addItem and layer.newModel take `index` (1-based stack position);
mediaBrowser.import passes `time` and `index` on, one layer under the
other.
Tests: project_items_land_where_they_are_dropped_in_the_timeline
(ui_drag_drop.rs, real pointer drags: outline, time graph, Shift, below
the last layer) and add_item_at_a_stack_index_and_time (engine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The desktop app shares egui-wgpu's device, which had no uncaptured-error
handler, so wgpu's default one panicked on an out-of-memory error on
every frame thread; the panicking frame stayed "in progress" forever and
never rendered again. The GPU frame budget was a fixed 1 GiB.
- GpuContext::new logs uncaptured errors once its pipelines are built
(errors while building them still panic there, which the app catches
to fall back to CPU compositing).
- Gpu::within_memory runs GPU work in an out-of-memory error scope; on an
error the result is discarded and uploaded layers and pooled textures
are freed.
- Frame workers render each frame within it; out of memory, the frame
renders on the CPU and the RAM preview keeps half as many GPU frames
(down to 64 MiB), since wgpu can't tell how much video memory there is.
- A frame whose render panics renders again on the CPU, or is released,
so the viewer asks for it again.
Tests: a_frame_whose_render_panics_is_released (failed before),
running_out_of_video_memory_halves_the_gpu_frame_budget and
within_memory_returns_the_work_when_there_is_room.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Commit c9d5e733 (M13.4, hevcenc deblocking) committed 207 files under
crates/hevcenc/target/agent-codec-hevc/: an agent's CARGO_TARGET_DIR with
rlibs, rmetas, build-script outputs and .rustc_info.json (~36 MB). They
were tracked because .gitignore only ignored the root `/target`.
- git rm --cached the directory; nothing references it.
- .gitignore: `/target` -> `target/`, so a per-crate or per-agent target
dir is ignored anywhere in the tree. No tracked file lives under a
directory named `target`, so nothing else becomes ignored.
The blobs stay in history; this keeps them out of every new checkout and
stops the next agent run from committing more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Toggling a layer's Audio, Lock or Shy switch (or Hide Shy Layers) threw
away every cached frame: the RAM preview identity hashes each comp's
address, which any edit replaces, and the layer and disk cache keys
hashed every switch. None of these change pixels.
- Switches::pixels, Layer::same_pixels and Comp::same_pixels (exhaustive
field lists, so a new field must be considered) and Comp::pixel_form.
- Layer cache keys hash Switches::pixels; the disk cache's comp content
key hashes Comp::pixel_form.
- Frames::identity: when a new revision's comps draw the same pixels as
those of the comp's last new identity, that identity (and the project
holding its comps' addresses) is kept, so the frames stay, also when the
switch is toggled back.
With audio on, playback skipped frames that weren't rendered yet. Now the
sound waits: until the frames ahead (to the end of the preview, or what
fits in the RAM preview) are cached, every frame shows as it renders,
silently, like a preview without audio; then the sound starts in sync.
Reaching an uncached frame stops the sound instead of skipping frames.
Tests: audio_lock_and_shy_switches_keep_the_frames (frames.rs, nested
comps and toggling back), audio_lock_and_shy_keep_cached_layers (engine),
the disk key in tests_disk_cache, and
preview_with_audio_shows_every_frame_and_sounds_once_cached.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An angle's revolutions were painted text: only the degrees could be
dragged or typed, so full turns needed the dial or typing past 360.
fx_widgets::angle_field draws `0x+45.0°` as two hot numbers: the
revolutions (widgets::hot_int_at, ten pixels a turn, fractions adding up
between frames so slow drags still step; a typed value is truncated) and
the degrees, each keeping the other. Effect Controls, the Timeline and
the Properties panel use it, so they read and edit angles alike (the
Timeline showed `-1x-40.0°` and Properties had its own split).
Automation ids: timeline.prop.<uid>.revolutions,
effectControls.prop.<uid>.revolutions, properties.prop.<uid>.revolutions.
Regression test: angle_revolutions_scrub_and_take_typing drags and types
the revolutions in all three panels.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Choosing Draft Settings (or any entry) in a Render Queue item's Render
Settings menu did nothing when the menu had been moved up to fit the
window. egui hit-tests a fixed-position Area where it was asked to go,
not where it was moved, so the popup's "pressed outside" check was true
over its own entries and the menu closed on the press, before the click.
widgets::pressed_outside tests the press against the rect the popup was
drawn at. Every popup with the same check uses it: popup_menu (Render
Queue, Effect Controls, dropdowns), the colour popup, the Preview and
viewer popups, the panel menu, the Flowchart, OpenType and font menus.
Regression test: ui_render_queue.rs picks Draft Settings from a menu
moved up to fit a 1000 px window; it failed before the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#94 asked for native Windows on ARM builds and for their availability and limits to be
documented. The builds landed in #101 (an arm64 entry in the release matrix, the PE checks in
package.ps1 and the windows-arm64 workflow that installs the MSI on ARM64 hardware), but the
README never said they exist. Get started now lists the installers on the Releases page for
each platform, says the Windows ARM64 build runs without x64 emulation, and says what CI checks
on ARM64 hardware and what it doesn't yet (the app's window and a native test run).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018iLqXiCVkUULoN4eWTD7cB
Use checked hex slices so prefs.set follows the existing invalid-colour fallback when a component splits a UTF-8 character. Cover invalid Unicode placements and valid mixed-case colours through the command.
Co-Authored-By: OpenAI Codex <noreply@openai.com>
effect.apply / effect.applyLast run through the menu dispatcher (Effect
menu, Last Effect, an effect dropped on a Timeline layer or on Effect
Controls) now bring up Effect Controls, opened if closed and brought to
the front without taking the focus, on the layer the effect went to
(selecting it if it was dropped on an unselected layer, keeping the new
effect selected). Double-clicking an effect's name in the Timeline opens
it in Effect Controls (layer.select, prop.select, window.panel) instead
of twirling it, as in After Effects.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 04bd305b9dccbb34d8c70fe870513208cdae0b7b)
Opening a comp (Project double-click, Open Composition, a Timeline tab)
now reopens the Composition panel if it was closed and brings it (and
the Timeline) to the front, as in After Effects. A closed Composition
panel also reopens in the centre (with the Layer panel or other viewers,
else above the Timeline) instead of beside Effects & Presets, which also
fixes Window > Composition.
App::raise_panel opens/fronts a panel without moving the focus;
show_panel builds on it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit adf2c6dac78ce426ea0bf6b4199b7238d8ad99dd)