An extension that ships several panels no longer adds one Window menu entry per panel:
`Scripts/ScriptUI Panels/Motion Kit/Animate.jsx` shows as Window > Motion Kit > Animate.jsx,
and a subfolder of `Scripts/` is a submenu of File > Scripts. Top-level scripts are unchanged.
- Script names may be `Folder/File.jsx`: exactly one folder, each part a bare name (no `..`,
`\`, NUL or empty part), never `ScriptUI Panels/...` under `Scripts/`.
- window.scriptPanel, file.runScript, file.uninstallScript, file.scripts.list and
scriptui_panels.json take such names; a panel's tab is titled after its file.
- ConfigStore::list_dirs (DirConfig, MemoryConfig) lists the subfolders.
- menus::dynamic_nodes groups a folder's entries into a MenuNode::Submenu; the egui and native
menus draw it with their submenu code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The PCF tap jitter was keyed on the receiver's 1/8-texel cell in the
shadow map. The GPU computes the receiver's map position with last-bit
differences from the CPU, so a receiver next to a cell line could take
another cell's jitter on one backend, and every tap moved. In a wide
penumbra that changed the coverage by several 8-bit levels:
gpu_matches_cpu_wide_penumbrae failed on Windows with 15 of 4159
shadowed pixels on an NVIDIA RTX 5080 and 22 on an AMD Radeon iGPU,
against a budget of 8.
tap_jitter now blends the hashes at the four corners of the cell by
the receiver's position in it, in shade.rs and in advanced3d.wgsl. At
the lattice points the jitter is the per-cell hash it was before. A
CPU test checks that the coverage does not jump at a lattice line.
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.
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>
`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.