The first test only called draw_color. This one runs paint_draw on an egui
context and checks the colour of each painted shape, so a branch that skips
draw_color fails the test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
eframe runs App::logic without App::ui while the window is minimized or
hidden, and handles a close request then too. The unsaved-changes check, the
device-loss close and craft_quit::on_close_request ran from ui, so such a
close skipped the prompt and the macOS quit through terminate:. They now run
at the end of logic, with the platform hook last, as craft-quit asks.
Scripts in Scripts/Startup and Scripts/Shutdown in the settings folder
were never run. The desktop and web apps now run the Startup scripts at
launch, before a project opens, and the desktop app runs the Shutdown
scripts as it quits, after the project is closed. Each folder runs in
alphabetical order with case ignored, and files larger than 16 MiB are
not run. A Startup script that fails is reported in a toast and in the
log, a Shutdown script that fails is reported in the log, and the next
one still runs.
A Startup script that waits in a modal dialog goes on when the dialog
closes, while the next scripts run. A Shutdown script's dialogs close
as if canceled. With the settings folder on disk, each script runs as
its file, so #include, $.fileName and $.evalFile resolve against the
script's own folder.
The new commands file.runStartupScripts and file.runShutdownScripts
return the scripts that ran, those still waiting in a dialog, and those
that failed. Like script.run, both are refused in confined automation
sessions, in batches and in script steps, which now read one list of
script commands (RUNS_SCRIPTS). The list also holds window.scriptPanel
and window.restoreScriptPanels, which run ScriptUI panel scripts and
were not refused on main either. The apps still call
window.restoreScriptPanels at launch. plugins.md, scripting-parity.md,
gaps.md, target-app-parity.md and agents.md describe the folders. Each script
still runs in its own global scope, so a Startup script's variables are
not available to later scripts.
Poison recovery, OsStr::to_str and char predicates only; S4325 casts are
f64 to f32 and stay.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
paint_draw passed a script's 0 to 1 colour values to egui::Rgba, which
stores linear light. egui then applied the sRGB curve, so every
fillPath, strokePath and drawString colour drew too bright. For example,
#2B2B2B drew as RGB 114 and #F28C28 as 249, 196, 110.
After Effects treats these values as plain screen colours. Convert them
with the existing to_color32 helper instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A deselected layer never keeps an open text edit: duplicate now commits any open
edit through the same route as text.endEdit before the selection moves, from every
route (menu, shortcut, palette, MCP, script). The editor-held keyboard matches the
chord against each command's current binding, like Deselect All already did, so
rebound shortcuts keep working and keys a focused editor must never surrender stay
typing.
`wgpu_options` built the window's configuration from `WgpuConfiguration::default()`, whose surface is
`SurfaceConfig::HIGH_THROUGHPUT` (two frames in flight); eframe's own `NativeOptions::default()` wraps
it with `LOW_LATENCY` (one), and replacing `wgpu_options` wholesale silently lost that. On Metal,
wgpu-hal sets `maximumDrawableCount = latency + 1`: three drawables instead of two, one more frame
between the pointer and the screen on every drag, scrub and slider (8 ms at 120 Hz, 17 ms at 60 Hz).
Start from `.with_surface_config(eframe::SurfaceConfig::LOW_LATENCY)`; everything else in
`wgpu_options` is unchanged.
Measured on an M2 Max (release, `footprint -p`): IOSurface 61 MB → 41 MB (three 21 MB drawables →
two). Interface frame rate during 10 s of playback of a 1080p comp whose frames cost 164–279 ms on
the GPU stays at 120 fps either way (median 120.1 vs 120.0, p10 116–119.5 vs 117–119.4), so one
frame in flight does not starve this comp at 120 Hz.
Test: `the_window_surface_is_low_latency` asserts `LOW_LATENCY`, `desired_maximum_frame_latency ==
Some(1)` and `AutoVsync` for every backend choice, and equality with eframe's default surface.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Image::padded clamps the pad so each side stays within MAX_PADDED_SIDE, but
Buf::pad still added the requested pad to its offset, so a clamped pad left
the pixels and the offset disagreeing (a hostile pad of u32::MAX shifted the
pixels by 5 and the offset by ~4.3e9). The clamp now lives in one place,
Image::effective_pad, used by both padded() and Buf::pad.
The padded() doc now states what the clamp guarantees (output within the
largest image the engine supports) and that memory budgets belong to callers
that pad by a derived amount (DOF_PAD_BUDGET, GPU fits()).
Adds an effects regression test: Buf::pad(u32::MAX) moves the offset by the
clamped pad and the source pixel stays at its layer position (fail-before:
offset off by u32::MAX). cargo test -p effectcraft-effects 457/457,
-p effectcraft-raster 35/35, -p effectcraft-render 238/238; clippy -D
warnings and fmt clean on touched crates.
Triage found the pad bound lived only in the DoF callers: Image::padded was
pub with a doc comment asking callers to bound pad, so a future caller could
reintroduce the exact oversized-buffer OOM this PR fixes. The cap now lives
inside padded(): pad is clamped so each output side stays <= MAX_PADDED_SIDE
(30 000, mirroring project limits::MAX_SIZE; raster cannot depend on the
project crate per layering), overflow unreachable by construction, no panic in
any profile. The caller-side padded_size guards stay for reject-on-error UX.
Adds raster regression tests: hostile pad (u32::MAX) clamps to cap with source
intact at the clamped offset (fail-before: pre-fix sized an oversized buffer);
legit pads byte-identical (anti-over-clamp). cargo test -p effectcraft-raster
35/35, -p effectcraft-render 238/238, clippy -D warnings clean, fmt clean.