chore: sync upstream (1205 Commits) #14

Merged
alf merged 1205 commits from chore/sync-upstream-20261030 into main 2026-10-30 07:59:54 +00:00
Owner

Upstream-Sync von upstream/main (1205 Commits). Fast-Forward.

  • 0555b526 Merge pull request #2668 from nanox333/codex/gradient-stroke-width
  • a16f37d8 Merge pull request #2667 from storytold/land/2663
  • d242bab1 Merge pull request #701 from vbasky/fix/687-adjustment-footprint-key
  • 3a8ff240 Merge pull request #2669 from storytold/fix/ui-egui-tests-one-binary
  • ace7d563 Merge pull request #1287 from vbasky/fix/1218-wheel-scrub
  • f0b5e4ee #2668: cover mixed strokes and stretched layer timing
  • 18ab2e3f Merge pull request #2664 from zitann2020/ui/2589-unselected-self-drop
  • 4ced49aa Merge remote-tracking branch 'origin/main' into HEAD
  • 697556f2 ui-egui: the accessibility and keyboard tests join the one test binary
  • 0ea9867f Merge remote-tracking branch 'origin/main' into HEAD
  • e9ca27e5 Merge remote-tracking branch 'origin/main' into HEAD
  • 742a085f Merge remote-tracking branch 'origin/main' into HEAD
  • 1f15f8a5 Merge pull request #2665 from storytold/docs/gaps-ci-gate
  • c6858118 Merge remote-tracking branch 'origin/main' into HEAD
  • 36c8d8a7 UPD.1: honor app pretend versions in update checks (#2663)
Upstream-Sync von `upstream/main` (1205 Commits). Fast-Forward. - 0555b526 Merge pull request #2668 from nanox333/codex/gradient-stroke-width - a16f37d8 Merge pull request #2667 from storytold/land/2663 - d242bab1 Merge pull request #701 from vbasky/fix/687-adjustment-footprint-key - 3a8ff240 Merge pull request #2669 from storytold/fix/ui-egui-tests-one-binary - ace7d563 Merge pull request #1287 from vbasky/fix/1218-wheel-scrub - f0b5e4ee #2668: cover mixed strokes and stretched layer timing - 18ab2e3f Merge pull request #2664 from zitann2020/ui/2589-unselected-self-drop - 4ced49aa Merge remote-tracking branch 'origin/main' into HEAD - 697556f2 ui-egui: the accessibility and keyboard tests join the one test binary - 0ea9867f Merge remote-tracking branch 'origin/main' into HEAD - e9ca27e5 Merge remote-tracking branch 'origin/main' into HEAD - 742a085f Merge remote-tracking branch 'origin/main' into HEAD - 1f15f8a5 Merge pull request #2665 from storytold/docs/gaps-ci-gate - c6858118 Merge remote-tracking branch 'origin/main' into HEAD - 36c8d8a7 UPD.1: honor app pretend versions in update checks (#2663)
alf added 1205 commits 2026-10-30 07:59:52 +00:00
Adds docs/user/ (published at getartcraft.com/docs/effectcraft), the docs-check
and docs-publish workflows (copied from craft-launcher), and a "User docs" rule
in AGENTS.md. Verified against 0.8.0.

Source notes per page:
- index: README, crates/engine/src/menus.rs, website crafting-apps.ts entry,
  docs/gaps.md (.aep).
- getting-started/install: README Downloads, gh release v0.8.0 assets,
  packaging/windows/effectcraft.wxs (per-machine, Start Menu, desktop shortcut
  box), package.ps1 (portable zip contents), launcher crates/core catalog.rs
  (EffectCraft listed), launcher docs install guide.
- guides/make-a-composition: menus.rs (Composition, Layer, Help), comp.rs
  shortcut, panels/comp_settings.rs (labels, presets, tabs), panels/home.rs,
  engine/src/learn.rs (Animate a title tutorial).
- guides/animate-with-keyframes: learn.rs, panels/timeline.rs (stopwatch, Add
  or remove keyframe), commands/prop.rs (Easy Ease F9), menus.rs (Animation).
- guides/add-an-effect: menus.rs (Effect, Window), panels/effects_presets.rs,
  commands/effect.rs, effects/src/misc.rs (Gaussian Blur parameters).
- guides/render-a-video: panels/render_queue.rs (labels), project
  render_queue.rs (format labels, default H.264, template, supports_alpha),
  commands/render_queue.rs (Cmd+M), README (Lottie).
- guides/import-footage: docs/footage.md, menus.rs, engine/src/prefs.rs
  (Sequence Footage), README (Wayland drop).
- troubleshooting/gpu-not-used: issues #596, #613; prefs.rs (Window Graphics),
  commands/app_more.rs (System Compatibility Report text), ui-egui menus.rs
  (GPU Information dialog text, checked at v0.8.0).
- troubleshooting/footage-missing: docs/footage.md (relink), commands/
  file_more.rs (Find Missing Footage), issue #652 and PR #676.
- faq/after-effects-projects: README "Where it stands", docs/gaps.md.
- faq/price-and-platforms: README (license, platforms, status), LICENSE files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sources: crates/engine/src/menus.rs (EffectCraft > Settings... on macOS, lines
~475-495; Edit > Preferences [!mac], ~658-675), crates/project/src/render_queue.rs
(RenderQueueItem.render defaults to true), issue #613 (RTX 5060 Ti comment),
docs/web.md (browser private storage).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Items B-EffectCraft 1-2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Part 1: verified = latest release tag policy in docs/user/AGENTS.md; released-pages sentence in root AGENTS.md. Part 6: re-pad platforms table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Parenting a camera or light (layer.setParent, compensate on) now re-expresses its Position and
Point of Interest in the new parent's space, like the other 3D layers. The 3D loop in parent_fixes
skipped cameras and lights, so their positions stayed in the old space and the camera moved by the
null's offset. Orientation is still left as authored; a later commit re-expresses it.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
uniform_parent_pose took the Y angle from asin, which loses precision within about 1e-6 degrees of
+-90. The Y angle now comes from atan2 on cos(Y), as the X and Z angles already do. The gimbal
branch and the principal-range output are unchanged.

A new test parents a static Orientation with Y 1e-6 degrees off gimbal lock under a uniform
parent. Its largest corner displacement was 3.3e-8 px before this change and 1.5e-14 px after.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Parenting a camera or light to a rotated uniform null now re-expresses its static Orientation, so a
one-node light or camera keeps its direction. A two-node camera or light also keeps its roll: its
Orientation is re-expressed against its new aim (aim_correction), so Up does not drift. Keyed
Orientation is still left as authored; a later commit re-expresses it.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Parenting a 3D layer under a rotated uniform parent now re-expresses keyed Orientation at every key,
not only the static value. A Z-only correction shifts Z exactly, so the paths between keys stay
exact. Other corrections keep each key's authored turns, so the Euler values stay continuous. This
changes keyed 3D layers under any rotated uniform parent, not only cameras and lights.

The boundary test now sets the non-uniform parent before the animated and static cases, so both
keep their authored pose.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Builds the desktop app and the CLI from source with `nix build`, and offers
packages, an overlay, `nix run`, a dev shell and a formatting check.

- Cargo.lock drives the dependencies (git sources via allowBuiltinFetchGit).
- Installs the desktop file, hicolor icons, MIME types and AppStream metadata
  from packaging/linux/, validated at build time.
- Wraps the binaries so the dlopen'd libraries (libwayland-client,
  libxkbcommon(-x11), X11, Vulkan loader, libEGL, libdbus for the portal file
  chooser) are found on NixOS: native Wayland on GNOME and Hyprland, X11 as
  fallback.
- Fonts from storytold/craft-fonts are pinned to the release workflow's revision
  where the app embeds them.
- doCheck is off (the suite needs corpora and a GPU); `nix build` was verified.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Markers counted as pixels in a comp's frame identity, so every marker edit
dropped the cached frames; the playing preview then hit an uncached frame
and stopped its sound. They now count only when an expression mentions markers.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Project::reads_markers and Comp::pixel_form_with are shared by the RAM and disk frame caches.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Pro Levels2 keeps its Alpha controls one group down, under Alpha, and a script's
effect.property("Alpha Input White") only searched the effect's direct children, so it returned null.
The lookup now searches the groups nested below the direct children, up to three levels, after the
direct match fails. Direct children keep their priority. A regression test checks the name on the
effect and the same control through the Alpha group.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
addProperty("ADBE Text Tracking Amount") returned the first property added to the animator, which is
the Tracking Type companion, so the script changed the wrong value. The engine now reports the
requested property's position in the Properties group, and the script returns that property. An
animator's Properties group also finds its properties by their After Effects names, such as
ADBE Text Tracking Amount, which the name table already had in the other direction. A regression
test checks the property returned, its value, and the lookup by name.

Not covered: the Advanced and Wiggly selector children and the Expression amount, which still keep
their own names.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Euler angles are blended between keys, so re-expressing only the key values cannot keep the motion
between them under a tilted parent. Keyed Orientation is now re-expressed only for a Z-only
correction, which shifts Z exactly at every time; static Orientation is unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Under a tilted parent, the converted keys alone give a different path between them, since Euler
angles are blended. Keys are now added wherever the blend drifts more than 0.05 degrees from the
exact rotation (halving each segment, at most 10 times and 4096 keys). The segments become straight
blends with the original easing baked in; hold segments are kept.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Cause: a preview with sound and video, without Cache Before Playback,
held its sound while the playhead advanced silently through frames as
they rendered, and started the sound at the frame it had reached once the
frames ahead were cached or rendering had kept up for a quarter second.
So the opening (about the first third of a second) was never heard, and
those frames played silently. With Video off, or Cache Before Playback,
the sound started at the first frame.

Fix: starting such a preview now holds the playhead on its first frame
while the frames ahead render (the prefetch window, at most what fits in
the RAM preview: not the whole work area), then starts sound and picture
together from that frame, the sound's clock driving the picture. Without
an output device the picture plays from that frame on its own clock. A
stall later on still stops the sound and catches up silently. Stop clears
the startup state.

Tests (ui_audio_scrub): the two startup tests no longer accept silent
frames first: the playhead must hold frame 0 until the sound starts, and
the sound must start at frame 0 (also for a 30 s work area, without
caching it all). A tone lasting only the first 100 ms after a start at
0 s and at 1 s reaches the output device. Video off still starts at
once; no device plays the picture; Stop while preparing leaves nothing
playing. The three startup tests fail on the previous code (the playhead
had moved on to frame 1, silently).
The start held the first frame until `ahead.min(fit)` frames were cached,
which could never happen in two cases, holding it for ever:
- the prefetch requests offsets 0..min(ahead * 3, fit - 1) from the
  playhead, one frame fewer than that when the RAM preview limits it, and
  with the playhead held nothing else requested the last one;
- `fit` counts comp-sized frames, but Extended Viewer frames render
  larger, so fewer of them fit in the budget than it needs at once.

It now waits for startup_frames(ahead, window, budget, frame bytes):
ahead, no more than the prefetch window requests, and no more than fit
in the budget at the size the first frame really rendered at, at least
the current frame.

Tests: startup_frames unit cases (a 4-frame budget needs 3; a 160×120
comp's 320×248 Extended Viewer frames need 2); ui_audio_scrub starts from
a cold cache with budgets of 2, 3, 4, ahead - 1, ahead and ahead + 1
frames, with and without a device, and with Extended Viewer frames larger
than the comp: each leaves its startup, from frame 0 (both hold the first
frame for ever on the previous commit). Also: a device whose start fails
plays the picture; a device that consumes the sound moves the picture on
and a stall still stops it; the 30 s preview starts without caching the
whole work area; Stop after a step of preparation never opens the output.
The startup hold waited for the current frame to be cached before
anything else, so a frame that can never be cached held the first frame
for ever: a zero RAM preview budget, or a single frame larger than the
budget (which the comp-size estimate misses for Extended Viewer frames).

It now gives up at once when the budget can't hold even one comp-sized
frame, and otherwise after STARTUP_WAIT (2 s from pressing play): the
sound starts from the first frame, and the preview goes on as a stalled
one does. frame_bytes() is factored out of frames_that_fit() for it.

Tests (ui_audio_scrub): with a budget of 0 or half a frame the sound
starts on the first step; with an Extended Viewer frame larger than the
whole budget (the estimate says one fits) the first frame holds for about
two seconds, then the sound starts. Both wait for ever on the previous
commit. The Extended Viewer setup is now shared by its two tests.
Upstream's #1167 fix and this branch both stop marker edits from invalidating
cached frames. Kept this branch's shared Project::reads_markers/same_pixels_with/
pixel_form_with (viewer frames, disk cache, nested comps) and folded upstream's
rules into them: any expression counts as reading markers, protected time
markers with a duration still change the frames.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- verified: 0.9.2 on every page, checked against the v0.9.2 tag and release assets
- install, price-and-platforms: Windows x86 builds, riscv64 deb/rpm, macOS CLI zip
- gpu-not-used: GPU Information now names the adapter or why the GPU is off;
  OpenGL means CPU compositing; #613 still open
- import-footage: TGA, HDR, DPX, Cineon sequences; dropped PSDs ask; drop on
  an empty viewer makes a comp
- render-a-video: QuickTime (Uncompressed) format
- make-a-composition: Layer > New > Text starts typing; save and delete presets
- CLAUDE.md: regular file instead of a symlink

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since #659 every text or shape layer with an enabled video effect grew its
effect buffer to the comp-sized layer rectangle before the stack ran, so
glow and blur — which only need their own margins — allocated a 1920x1080
RGBA32F texture per layer per frame in flight (~5 GB peak VRAM in parallel
export, measured on an RTX 3090).

Effects that actually place pixels outside the painted content already
grow the buffer themselves by exactly what they need (CC Tiler, CC Smear,
CC Mosaic sharp via layer_cover_pad / Buf::pad), on CPU and GPU. The
blanket target.cover(layer_size) in Renderer::run_effects_on, the
FxTarget::cover trait method with its CPU default, and the GPU
GpuTarget::cover texture allocation are removed; Buf::cover/cover_plan
stay as the effects-crate helpers they were designed as.

The #659 regression tests (tiling across the layer region, centred-shape
edge pinning) and the render/effects/gpu suites pass unchanged.
The Character panel's pipette (#938) only sampled text. Every color picker now has an eyedropper in its hex row: click it, then click in the Composition panel to take the color under the pointer. The picker is one shared popup, so Solid Settings, Fill and Stroke, attached images, effects and the rest get it without caller changes.

- color_picker: the eyedropper arms the picker. Escape or a second click disarms it and the picker stays open. While armed, the viewer shows a crosshair, a "Click to sample color" chip and a swatch under the pointer, and the next press on the viewer samples into the picker. A press over the picker or a dialog is not a sample, and a press elsewhere closes the picker as before.
- The Character panel's pipette glyph moves to color_picker::draw_pipette, so both show the same icon.
- Automation id colorPicker.eyedropper.
- Modal dialogs (modal_fit) and the picker's popup register their rects as overlays, so a press on them never samples the viewer beneath.

Not compiled, tested or rustfmt-checked: the machine it was written on has no Rust toolchain. Run cargo xtask ci before relying on it. Pushed to the fork for testing only.
layer.setTrackMatte took one layer. It now also takes `layers`, set in
one undo step; `layer` and the first selected layer still work as
before. `kind: "preserve"` keeps each layer's own matte mode (Alpha for
a layer without one), so giving several layers a new matte doesn't turn
a mix of Alpha and Luma mattes into one mode; without `kind` the mode
is Alpha, as before. A layer can't be its own matte and an empty list
is refused, before anything changes.

Test: several_layers_take_one_matte_and_keep_their_modes.
On a selected layer's row, the Parent menu and pick whip and the Track
Matte menu now set every selected layer's parent or matte, as moving a
selected layer's bar moves them all; on an unselected row they set that
layer's alone. Each is one undo step. A selected layer chosen as the
parent or the matte is left out of the layers it links, and parenting
that would make a loop is refused as a whole.

A new matte layer keeps each layer's mode (layer.setTrackMatte with
kind: preserve); a mode and No Track Matte apply to all of them.

The Track Matte column gets a pick whip, like the parent's
(timeline.layer.<id>.trackMattePickWhip, with a translated tooltip):
dropped on an audio-video layer's row, it makes that layer the matte.
The layers a parent or matte pick whip links are taken when its drag
starts.

Tests (ui_batch_links): the Parent menu and pick whip, and the Track
Matte menu, mode, No Track Matte and pick whip, with real clicks and
drags: selected layers change together and unselected ones don't, modes
are kept, one undo puts links, compensated transforms and the matte's
visibility back, and a refused cycle changes nothing. Both fail on main.
With several layers set at once from one selected row, a selected
layer locked after it was selected (its Lock switch doesn't deselect
it) had its parent, its compensated transform or its matte changed too.

layer.setParent, layer.setTrackMatte and layer.trackMatte now leave
locked layers as they are, the rule the other layer edits follow
(unlocked): among several, the others change; alone, it is an error
that changes nothing. Done in the engine, a layer locked while a pick
whip is being dragged is covered too.

Test: locked_layers_keep_their_parent_and_matte.
The Track Matte menu and pick whip applied to every selected layer,
nulls, cameras and lights included, though their rows have no Track
Matte. A mode chosen for selected solids then went to a selected null
too: at the top of the comp it had no layer above to take as its matte
and the whole change was refused; at the bottom it took the layer above
as its matte and hid it.

Track Matte edits now go to the selected layers that can have a matte
(matte_layers); parenting still includes nulls, cameras and lights.

Tests (ui_batch_links): a selected null on top or at the bottom gets no
matte and changes nothing else; a layer locked with its Lock switch
stays selected and keeps its parent, transform and matte through the
other row's menus and pick whips. Both fail before.
A property driven by an expression showed its value in the ordinary blue;
After Effects shows it red (refs #1072). Tokens::for_value(expression)
gives the value fields the expression colour (expr_text, dark and light)
for both hot_text and accent_hover, so a hovered or dragged value stays
red. The Timeline's value_editor, Effect Controls' parameter rows and the
Properties panel's transform rows pass these value tokens to their
number and angle fields (and the Timeline's vector separator and plain
enum number), from Property::has_expression() every frame: a disabled,
removed or empty expression is blue again. Names, stopwatches, keyframe
buttons, link icons and dropdowns keep the ordinary tokens. Red means an
expression drives the value, not that it evaluated; errors keep their
own reporting.

Tests (ui_expression_values, reading the painted text colour): a vector
(Position, with its separator), an angle (Rotation, both parts) and a
scalar (Opacity), on the layer and on the Transform effect, are red in
all three panels, in dark and light themes, and Scale (no expression)
stays blue; disabled, removed and whitespace-only expressions are blue
and enabling again is red; a hovered expression value stays red while a
hovered ordinary one takes the hover accent. All three fail without this
change. ui_effect_controls and ui_inline_edit still pass.
Each checked property now names the panels that must show it: the layer's
Position, Rotation and Opacity in the Timeline and Properties panel, the
Transform effect's in Effect Controls. Every component rectangle is
required, and the painted text must include both numbers of a vector (and
the Timeline's comma), the revolutions, sign and degrees of an angle, and
the number of a scalar. These presence checks run in both themes and after
every expression state change.

Hover now covers all three panels in dark and light themes, and a new test
scrubs Opacity in the Timeline and Effect Controls, confirming a drag is
active before checking the colour. All four tests fail on main.
pixel_form_with borrowed the comp unchanged when it held only comp markers and no layer
markers, so a lone plain comp marker still reached the disk key. Tests: a lone comp marker
keeps the key; a protected marker with a duration (and its duration) on a stretched precomp's
source changes the key of the comp that nests it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
With Separate Dimensions, X Position and Y Position are separate scalar
properties. The new test puts an expression on X Position only and checks,
in the Timeline and Properties panel and in both themes, that X is red and
Y keeps the ordinary colour. It fails on main.
ui_accessibility_audit walks the AccessKit tree and counts unnamed or roleless controls per panel (zero required for Project, Effects & Presets, Settings and the Render Queue; a ratchet for the rest). Rows are named tree items with selected / open state, the page list is a list with positions, controls carry label and value, the lists take the arrow keys, Home / End, Enter and Space, popup lists take the focus when they open. ui_contrast checks the theme tokens against WCAG 2.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
With the Render Queue active, Cmd+W ran comp.close on the active composition, so the composition
next to the queue closed and the queue stayed open. Close now follows panel focus, as Clear does:
the queue closes and the composition stays open.

Covered by menus::tests::cmd_w_in_the_render_queue_closes_the_queue_not_the_comp.

Claude-Session: https://claude.ai/code/session_0136kZtXrYo5VUWEFxWduwUD
The Graph Editor had no menu for keyframes. Right-click now opens the same key menu as the
Timeline, for the commands that apply from the graph: Edit Value, Copy and Paste, selecting
equal or neighbouring keys, Hold, Rove Across Time (spatial properties only), Easy Ease,
interpolation and Delete. The items run the existing keys.* commands, so undo and the
automation ids are unchanged.

A right-click on an unselected key selects it first, as a click does.

Not covered by an automated test; checked by compiling and by reading the menu against the
Timeline's.

Claude-Session: https://claude.ai/code/session_0136kZtXrYo5VUWEFxWduwUD
Double-click a shape tool in the toolbar to add a shape layer the size of the composition, at
its centre, for backgrounds, mattes and overlays, as After Effects does. It uses layer.newShape
with the tool's kind, so the undo label and the command are the same as drawing one.

Covered by tests::a_comp_sized_shape_layer_sits_on_the_comp_centre; the double-click itself is
not covered by an automated test.

Claude-Session: https://claude.ai/code/session_0136kZtXrYo5VUWEFxWduwUD
The report says a roving key goes back to its original time after other keys are edited. Editing
the last key's value and then its time keeps the roving key at its share of the path, as the
Rove Across Time description says, so this adds a regression test for both edits. It passes on
this branch; the reported sequence is not reproduced yet, so the report still needs the exact
steps.

Claude-Session: https://claude.ai/code/session_0136kZtXrYo5VUWEFxWduwUD
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
essential.templateInfo was missing from confine.rs's READS, so its
`path` got no named check, and the fallback only scans absolute-looking
strings: a relative path (or one through `..`) escaped the read roots
and the command parsed and returned a template outside them.

An audit of every registered engine command with a parameter named
like a file found the same gap in more commands, now classified:
- READS: essential.templateInfo, file.importProject,
  file.watchFolder.poll, footage.relink, layer.newModel,
  mediaBrowser.fileInfo / list / import / removeFavorite,
  face.model.install, roto.model.install, templates.create (`path`).
- WRITES: file.exportTimeline, file.createProxy (`path` / `output`,
  which may be a file-name template, as for render.preRender).
- MIXED_WRITES: templates.create writes `projectPath` and `footageDir`.

Tests (confine.rs):
- every_command_with_file_parameters_is_classified fails when a
  command takes a path, paths, folder, file, output... parameter and is
  neither confined, refused, checked by a lookup, a Render Queue output
  (checked when the queue renders) nor listed in NOT_FILES (property
  paths and the like). New file commands can't slip through.
- relative_and_outside_paths_of_newly_listed_commands_are_refused:
  relative, `..` and outside paths are refused and in-root ones allowed,
  directly and in a batch; writes go only to write roots; createProxy
  accepts a name template. It fails on main (templateInfo with
  `t.ectemplate` passes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EffectHost::param_at(effect_index, id, layer_time) evaluates a single
parameter (by its flattened id, such as birthRate or group/param) of the
effect at that index in the layer's stack. A simulation can then sample
one parameter at every step without flattening the whole effect each
time.

The effect index is passed in, rather than read from the renderer's
running-effect index as params_at does: GPU-batched effects never update
that index, so a batched effect would read another effect's parameters.
The renderer's implementation walks the effect group by match id and
evaluates just that property (keyframes and expressions) at the comp
time of the layer time.
SimCache::run_where takes a check on each checkpoint (its step and
state). A simulation whose steps depend on inputs outside its key, such
as a parameter's values over time, resumes only from a checkpoint
reached with the same inputs. A fresh checkpoint now replaces one at the
same step, so a changed history doesn't leave stale checkpoints behind.
run() is run_where with no check.
psim::births_at builds the GPU's birth schedule (the birth step of every
particle id) from the rate of each step, with the CPU step's carry
arithmetic. births() for a constant rate now uses it. It stops as soon
as the cap would be passed, before adding a step's births, so a huge
rate can't build a huge list first.
The simulation runs from layer time 0, but it used the Birth Rate of the
rendered frame for every step. A rate keyed from 3 down to 0 simulated
no births at all once it reached 0, so the burst's particles vanished
that frame although their Longevity had not run out. A ramped rate was
simulated as if its current value had always applied.

Each step now samples Birth Rate at its own layer time through
EffectHost::param_at (with the effect's own index). Without such a host
the current value applies throughout, as before. Particles already alive
keep moving and ageing whatever the rate. The carry and particle ids
continue across rate changes.

Birth Rate is left out of the simulation cache key, so an animated rate
no longer restarts the simulation every frame. In its place:

- The CPU state keeps a hash of the rates it was stepped with (hist). A
  checkpoint is resumed only when that matches the current history, so
  moving an earlier key is never served from a stale checkpoint.
- The GPU gets the birth schedule of the sampled rates (births_at). Its
  checkpoints record which births they hold, and are resumed only when
  the request's births before that step are the same.

A constant Birth Rate gives exactly the particles it did. CC Particle
Systems II and CC Mr. Mercury keep their constant-rate simulation.
A burst lives on after the rate drops to 0: as a constant rate during
it, only its particles afterwards, still moving, gone after their
Longevity. Another effect's history isn't used. A ramp bears what each
moment asked for. Seeking backward and forward matches a simulation
from 0. Moving an earlier key isn't served from stale checkpoints with
the same current rate. A constant rate is unchanged. The birth schedule
matches the CPU's ids.
The issue's scene: a Birth Rate keyed 3 until 0.5 s then 0 matches the
constant rate during the burst, still draws the burst at 0.9 s with no
new births, and is empty once Longevity has run out. Moving the earlier
key changes the 0.9 s frame through the layer cache. Layers offset by
0.5 s or stretched 200 % sample their own layer times.
With a Birth Rate keyed into a burst, the GPU's particles match the CPU
simulation through the burst and after it. A second history with the
same system key, whose burst starts later (so the same ids are born at
other steps), isn't resumed from the first history's GPU checkpoints.
Matej's follow-up kept the known-margin set as SKIPS_COVER and the renderer looked the ids up. Ask EffectSpec::skips_cover instead so a new margin effect opts in on the spec rather than in run_effect_stack. Tests pin align_plan to cover_shift mod 64 and that Twirl still takes the full cover.
In After Effects, holding Alt while dropping the expression pick whip
refers to an effect's property by its index, effect("Color Control")(1),
instead of its name, ("Color").

prop.pickWhip takes byIndex: the effect keeps its name and each
property inside it is selected by its one-based place among its group's
children (nested groups too), which is what an expression's (1) reads.
Other properties, dimensions ([1]), insertion at the cursor and match
names (Compact English off) are as before. An index still reads a
renamed property, but another one if properties are reordered.

Tests: pick_whip_by_index_selects_effect_properties_by_their_index
(engine, the references); effectcraft-host pick_whip (by index and by
name read the same values, nested and per dimension).
A property or expression pick whip dropped with Alt held, onto the
Timeline or Effect Controls, now sends byIndex: an effect's property is
referred to by its index, as in After Effects.

Test: an_alt_pick_whip_refers_to_an_effect_property_by_index (Alt and
plain drops onto Effect Controls and the Timeline, into the expression
being edited with a dimension, one undo step each).
FxHost::param_at mapped the requested layer time to comp time, and
EvalCtx::value mapped it back. Under a fractional stretch (33.3 %,
133.33 %) that round trip truncates a tick early, so a Hold key at
0.5 s read its previous value at the step starting there: a Birth Rate
cut to 0 at 0.5 s bore one more step of particles (six).

EvalCtx::value_at_layer_time reads the keyframes at the given layer
time exactly, and still runs an expression at the comp time.
EvalCtx::value now uses it, and param_at reads through it.

EffectHost::param_is_static says when a parameter has no keyframes and
no expression. CC Particle World then skips sampling its Birth Rate at
every step, since it's the same throughout.
The simulation checks its checkpoints against the Birth Rate history,
but a cached layer is returned before the effect runs. The layer key
sees the layer's keyframes and its current values, not what an
expression returned at earlier times: a Birth Rate expression reading
another layer could change the history while the current rate, 0, stayed
the same.

effects::reads_param_history names CC Particle World. The renderer
already declines to cache layers whose time effects (ec.time.*) meet
expressions. It now treats Particle World the same way, in the layer key,
the input key (only for the effects that input runs) and the dependency
walk.
A Hold cut at 0.5 s reads 3 just before and 0 at 0.5 s under 33.3 %,
133.33 % and 200 % stretch, and the frame matches the unstretched one.
A Birth Rate expression reading a Controller layer's Opacity before
0.5 s: changing the Controller changes the 0.9 s frame, and the cached
render matches the uncached one.
The two histories now bear the same 180 particles by 0.9 s at different
steps (3/s from 0 to 0.5 s, and from 0.25 to 0.75 s). So a checkpoint
check by birth count alone fails the test, not only no check. A
test-only dispatch counter asserts each GPU state really came from the
GPU rather than the CPU fallback.
Both simulate from layer time 0, as CC Particle World does, and used
the Birth Rate of the rendered frame for every step. A rate keyed down
to 0 emptied them the frame it got there, although their particles (and
Mercury's blobs) were within their Longevity.

CC Particle World's simulation path becomes Stepped: Birth Rate sampled
at every step (rate_history), a cache key without it, and checkpoints
resumed only with the same rate history. Particle World, Particle
Systems II (60 particles a second per unit, CPU or GPU) and Mr. Mercury
(10 blobs a second per unit, CPU) now all use it. A constant Birth Rate
gives exactly the particles it did.

The constant-rate step, key and simulation (Phys::step, sim_key,
simulate) are now only the tests' reference. Mr. Mercury's physics move
into mercury_phys.
For both: a burst lives on after Birth Rate drops to 0, and is drawn
(particles, blobs) where a constant 0 draws nothing. Its particles move
and age, and are gone after their Longevity. Seeking matches a
simulation from 0, and moving an earlier key with the same current rate
and cache key isn't served from stale checkpoints. A constant rate gives
exactly the constant-rate simulation's particles, with or without a
host.
They now read their Birth Rate at earlier times too, so
effects::reads_param_history names them along with CC Particle World. A
layer key can't see what an expression returned then.
Through the renderer, a keyed burst draws as the constant rate during it,
still draws after the rate drops to 0 (unlike a rate of 0 throughout),
and draws as nothing born once Longevity has run out. A Birth Rate
expression on any of the three stepped particle systems keeps the layer
out of the layer cache; without one it is cached.
The keyed Birth Rate comparison is shared (keyed_agree) and now runs on
CC Particle Systems II too. The same two histories bear 90 particles by
0.9 s at different steps: the GPU's particles match the CPU's, and each
state really came from the GPU.
The Gradient Editor edited a draft and set the property only on OK, in a
centred dialog over a dimmed, greyed-out window, so the canvas could not show
the gradient while it was edited. In After Effects the dialog sits at the lower
right, the window stays at full brightness, and the canvas updates live
(Preview checkbox).

- Preview (on by default): each edit is applied with `prop.set` and one merged
  undo step; OK keeps it, Cancel and Escape roll back to a checkpoint of the
  project and undo history taken on open (the Layer Styles dialog's mechanism,
  now shared). Preview off sets the property on OK only, as before; switching
  it off takes back what it applied.
- The editor opens at the lower right (`Placement` for modal dialogs), without
  the dark backdrop, and the window behind is no longer faded
  (`Dialog::leaves_window_undimmed`); clicks still don't reach it.
- Other dialogs are unchanged.

Claude-Session: https://claude.ai/code/session_013fTnVxnXTv2MBW6uj2PPyU
The Gradient Editor's Preview now covers the other settings dialogs that edit
something that exists: Solid Settings on a solid layer, Camera Settings, Light
Settings and Composition Settings. A change of the draft reaches the project at
once, so the canvas shows it while it is made.

- One shared mechanism (`panels::live_preview`): each step goes back to a
  checkpoint of the project and undo history taken when the dialog opened, then
  runs the dialog's command once. So the project is always "as it opened, plus
  one command": OK leaves one undo step, Cancel and Escape restore the
  checkpoint, Preview off applies nothing until OK, and switching it off takes
  back what was applied.
- A previewing dialog opens at the lower right, without the dark backdrop, and
  the window behind it is no longer greyed out; it can still be dragged by its
  title. The same dialogs creating something new (New Solid, New Camera, New
  Light, New Composition) are unchanged and have no Preview.
- `Placement` is public, and `EffectcraftApp::dialog_state` gives tests and
  automation the drafts.

Claude-Session: https://claude.ai/code/session_013fTnVxnXTv2MBW6uj2PPyU
Problem: the built-in animation preset library had no Behaviors category
(gap 7: 54 presets against After Effects' 621).

Fix: 22 original Behaviors presets in crates/engine/presets/behaviors.json:
Soft Wiggle, Shake, Float, Sway, Orbit, Figure Eight, Bouncing Ball, Drift
Right, Drift Diagonal, Pendulum, Wobble, Spin Slow, Spin Fast, Pulse,
Heartbeat, Breathe, Rubber Band, Jitter Scale, Blink, Strobe, Flicker, Fade
Pulse. Each puts a time-based expression on one transform property or
opacity (no effects), built on the property's own value so it keeps running
for the layer's whole duration. Listed under * Animation Presets > Behaviors.
Sidecar, ATTRIBUTION.md, gaps.md row 7 and parity docs updated.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Two English surfaces the translation work so far could not reach.

The dock's tab titles: `PanelKind::title()` is drawn by `dock_ui.rs`'s `tab_titles`, which listed only
the tabs that name a comp, a layer or a script and let every other one fall back to `title()`'s English
- so every panel tab (`Timeline`, `Preview`, `Properties`, `Effects & Presets`, ...) stayed English. The
Window menu already carries a (`window.panel`, title) row in every catalog, so the dock reads that pair
and falls back to the panel catalog for `Markers` (the one panel only it names); `History` and
`ScriptUI Panel` have no row anywhere and keep their English names. `app.auto`'s labels and the dock's
serialized state keep the English title, which the control channel needs.

The scan's blind shapes: `converted_panels_have_no_unwrapped_literals` only sees a literal that follows
a call token, so a label passed to a helper, the third argument of `Painter::text`, a `(key, label, ...)`
table, a `let` binding and a `match` arm were all invisible. The two that can be checked without
guessing are added (`helper_and_painter_labels_are_translated`), with the same single-character
exemption the call scan makes, and the 25 sites they find now ask the catalog (`Apply`, `Analyze`,
`Cancel`, `Create Camera`, `Add Point`, `Remove Point`, `Clear`, `Render`, `Stop`, `Save Current as
Template...`, `New Composition From Footage`, the three search fields, `(no composition)`,
`per second`, `Waveform`, `steps`), with 15 rows added to the Simplified and Traditional catalogs in
the conventions those tables already use (Render 算圖, Apply 套用, Add Point 添加點).

The `uic!` labels and the Keyboard Shortcuts dialog are a different shape and are still to decide
(#1451).

Verified on Linux: cargo fmt --all --check, cargo test -p effectcraft-ui-egui (every suite passes),
cargo clippy -p effectcraft-ui-egui --all-targets -- -D warnings clean. Reverting the dock's catalog
lookup fails `the_dock_draws_every_panel_it_can_in_the_interface_language`; unwrapping a helper or a
painter label fails `helper_and_painter_labels_are_translated` ("panels/project.rs:434: the interface
text \"Search project\" that widgets::search_field draws is not in the catalog").
Problem: the hardware-decoding work (#819) needs a newer FilmCraft than the
pinned 2c5ba7a, and the pin bump should be reviewable apart from it.

Fix: move all filmcraft-* deps to 4e2d0bc2. Adapt to MediaInfo.audio ->
audio_streams and VideoStreamInfo.hdr; ask the pool for a quarter into the
frame because FilmCraft now rounds requests half up; adapt rotation and the
AAC downmix; use Decoder::with_threads(1) in the VP9 tests. No
filmcraft-platform, no hardware setting.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Fonts installed after EffectCraft launched (dnf, GNOME Font Viewer, a
drop into ~/.local/share/fonts) stayed invisible until restart: only the
startup background scan ran, and the report in #1089 could not tell
discovery gaps apart from a stale scan.

- The font menu's filter row gains a Rescan Fonts button (Character and
  Properties panels share the popup) that runs the existing
  text.fonts {rescan} command and reports the number added in the status
  line. The press doesn't flip the Variable-fonts filter (the button is
  above the row's click area and closes the popup on top of it).
- system_scan_finds_fonts now asserts, on a native fontconfig desktop
  (any machine with /etc/fonts, which includes the Fedora reporter's and
  the CI runner), that the scan lists installed families rather than
  silently returning only the bundled fonts: 870 faces in ~10 ms on an
  Arch box.

Tests: ui_font_search 4 passed (new rescan test drives the real popup);
effectcraft-text lib system scan green; fmt and clippy --all-targets -D
warnings clean. Translations for 'Rescan Fonts' in all six catalogs.
The 180 px font menu pinned Rescan Fonts over "Variable fonts only", and the click walked every fontconfig directory on the frame thread. The button is now its own row, and the scan runs on a background thread that the frame polls. The Linux face-count assert is left to the native-fonts gate.
A 30 000 px comp (the repaired MAX_SIZE on load) at resolution 4.0
produced 120 000 px sides, which overflow u32 in the exporters'
w * h * 4 (debug panic, release wrap then slice panic) after already
requesting a ~57 GB frame. The resize path in the same file clamps to
30 000; output_size now does too.

Regression test: output_size_never_exceeds_the_comp_limit — on main it
returns (120000, 120000); with the clamp (30000, 30000), through both
RenderSettings and the OutputModule wrapper.
Clamping each side on its own broke the aspect ratio (a 30000x15000 comp
at 4x came out 30000x30000). The effective scale min(s, MAX_SIZE/w,
MAX_SIZE/h) keeps both sides within the comp limit, and the export
pipeline and the locked-aspect width derivation render at that same
scale, so nothing requests a frame larger than output_size reports.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The row has been in every catalog; `format!` built the label in English instead.

Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
"N minutes ago" and "N hours ago" have rows in all seven catalogs; a Japanese
interface read `保存日時:… {m} minutes ago`.

Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
The merge of main brought a double-click label hit area on the Properties
transform rows that had no accessible name; give it one so the audit ratchet holds.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Moves the pin past FilmCraft's AVI / OpenDML reader (Motion JPEG, H.264,
HEVC, uncompressed; PCM, MP3, MP2, AC-3). No EffectCraft API changes.
FilmCraft's codecs now turn on symphonia's AIFF reader themselves.
FilmCraft reorders AAC channel configuration 12 (7.1) to WAVE/SMPTE order,
as it does 3 to 6, so 7.1 AAC's left channel was mixed as the centre. The
downmix oracle test gains 6.1 (a program config element, left in AAC's
order) and 7.1 AAC fixtures.
One list of movie extensions, in effectcraft-project, with AVI added:
FilmCraft reads AVI 1.0 / OpenDML. Tests against ffmpeg fixtures: Motion
JPEG with PCM audio, an unsupported codec (imports and says why it has no
picture), a file that isn't an AVI (an import error), and a damaged one
(a truncated recording imports as far as it goes; a garbled packet loses
its own frame only).
Import and Set Proxy build their filters from the shared movie list, so
they offer AVI; the web build's file picker gets the same list.
The picture comes from an item's proxy while Use Proxy is on, but the
Project panel, the Viewer, project.summary and footage.info asked only
about the item's own file: a proxy that couldn't be decoded (an AVI with
an unsupported codec, say) left the layer empty with no reason given.
They now ask FootageSource::item_problem, which gives the proxy's reason,
said to be the proxy's, for footage and compositions alike.
Quit EffectCraft and Finder's Quit send `ViewportCommand::Close`, and
the close button sends the window's close request. In each case eframe
drops the window while AppKit's run loop is still running. On a Mac
with a Touch Bar, AppKit's Touch Bar code then raises an uncaught
Objective-C exception (`-[_NSTouchBarFinderObservation invalidate]`)
and the app crashes (storytold/craft-libs#3).

After the unsaved-changes check, `close_or_quit` calls
`craft_quit::on_close_request` from craft-libs, pinned to the rev that
craft-docpath already uses. When nothing holds the window open, it
cancels the close and quits through `-[NSApplication terminate:]`,
which still runs `on_exit` (window position, workspace, crash-recovery
cleanup). The prompt for a modified project still holds the window.
The window close after a lost graphics device takes the same route.
On other platforms it does nothing. pdfcraft adds craft-quit the same
way (storytold/pdfcraft#1533).

A quit while the window is minimized or hidden still closes the window
as before: eframe does not run `ui` then, and the unsaved-changes check
and the new call both run from `ui`.

`terminate:` ends the process before `eframe::run_native` returns, so
the log line for the closing window moves from `main` into `on_exit`.
CPU renders kept one frame per thread in flight with no cap, so heavy comps
could be OOM-killed. EFFECTCRAFT_CPU_INFLIGHT=N renders at most N frames at
once through the existing capped path; unset keeps today's behaviour. With a
GPU cap too, the smaller cap holds for the whole batch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Evaluate the third cube coordinate symmetrically to retain the intended centerline tie. Keep the existing pixel oracle and tolerance.

Scope the Mem fixture mutex unwrap allowance so the original strict CI can validate this base.

Co-authored-by: Codex <codex@openai.com>
A named marker drew its name as bare text beside the flag. After Effects puts
it in a small outlined box. The box uses the theme's field fill, field outline
and text tokens, so it follows Dark, Darker and Light instead of a fixed grey.

It applies to every marker with a name, composition markers included; the dimmed
names of nested-comp markers on a precomp layer's bar are unchanged.

Split out of the WAV markers change (#1423), where the review asked for it to be
its own decision. Test: ui_marker_names renders the timeline in Dark and in
Light, samples the box's fill and outline pixels, and checks they equal that
theme's tokens and differ between the themes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A WAV's markers were never read, so its audio layer showed none. The importer
now reads the file's cue points (`cue ` chunk named by `LIST/adtl` `labl` and
`note`, sized by `ltxt`) and Adobe's XMP markers (`_PMX` chunk, `xmpDM:markers`
entries as elements or attributes, in frames of the track's `xmpDM:frameRate`).
A marker present in both encodings is listed once. The reader is bounds-checked
and caps the marker count and XMP size.

- `Footage.markers` (optional in the file format) holds them at footage time; a
  layer made from the footage starts with them.
- Layer ▸ Markers ▸ Update Markers From Source also works on footage layers.
- Markers from a file have no label colour.

Tests: reader unit tests (cue, note, ltxt, both XMP forms, both encodings,
truncated and hostile files), a probe test, and an end-to-end test through the
fully wired session. Not checked against a WAV written by Adobe's tools.

Claude-Session: https://claude.ai/code/session_013fTnVxnXTv2MBW6uj2PPyU
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
# Conflicts:
#	crates/ui-egui/tests/ui_inline_edit.rs
Under load the first frame could take longer than STARTUP_WAIT to be
cached. The start then gave up, started the sound on that uncached
frame, and the stall path stopped it in the same step, so the opening
played silently.

STARTUP_WAIT (2 s) now counts from when the first frame is cached, and
only bounds the wait for the frames ahead. A new STARTUP_LIMIT (10 s
from Play) still ends the wait for a first frame that never gets cached.
The sound is never started on a frame that isn't cached: the preview
goes on as a stalled one does, and the sound starts once frames are in.

Tests (ui_audio_scrub): a first frame held uncached for 3 s still gets
its sound from frame 0, and the device keeps playing (fails on the
previous commit). The two give-up tests now check that no sound is
started and dropped.
startup_finishes_when_extended_viewer_frames_are_larger failed on busy
machines. The harness clock moves 1/30 s per step however long a frame
takes, so a slow first frame could use up the startup's limits in
harness time. The test now waits for the first frame in real time
before stepping on, as it is about the wait for the frames ahead, and
checks the fake device was started and not stopped.
Exporting still frames via comp.saveFrameAs previously flattened the
rendered frame against the composition background color, stripping
the alpha channel. Single still frames queued in the Render Queue also
defaulted to an RGB-only PNG sequence output module.

Preserve straight alpha by default in comp.saveFrameAs (while honoring
transparent: false if requested), and set TemplateSlot::Still to default
to 'PNG Sequence with Alpha'.

Fixes #1815
In After Effects, text animator Tracking Amount adds the value in absolute
pixels between neighbouring characters, independent of font size. EffectCraft
previously scaled tracking by 1/1000 em (matching static Character-panel
tracking), causing tracking reveals to have significantly reduced spread.

Apply Tracking Amount directly in pixels during glyph positioning layout,
update existing tracking tests to reflect pixel units, and add a regression
test verifying font-size independence.

Fixes #1798
Review of #1763:
- With EFFECTCRAFT_CPU_INFLIGHT set, each window is the CPU cap instead of
  one frame per thread, so rendered images held before encoding count
  against the cap too (a cap of 2 on 16 threads held up to 16 images).
- Once the GPU stops for good, the GPU cap lapses and the CPU cap alone
  applies, instead of the smaller cap (often the GPU default of 1) holding
  for the rest of the batch.

The caps are now an `Inflight { gpu, cpu }` that `map_frames_while`
re-evaluates per window and chunk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep the existing regression and tolerance while preserving upstream
WireRemoval and test-service recovery changes.

Co-authored-by: Codex <codex@openai.com>
When a named marker's duration ended inside the name box, the box's fill covered
the duration's end cap and most of its bar, so the duration could not be seen.
The box now steps past the end cap in that case (and stays at the flag when the
bar is longer than the box, or ends before it).

The new test puts a 0.1 s duration on a named marker, takes the timeline's scale
from a second marker, and checks that the cap and the bar are drawn and that the
box starts after the cap; it fails with the previous drawing. The two render
tests now take a lock, because two tests creating a GPU device at once crash
some Vulkan loaders.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Two findings from the review of #1423.

RF64 (WAVs over 4 GB): a chunk too big for 32 bits has a size field of
0xFFFFFFFF and its real size in the file's `ds64` chunk. The reader took the
field literally, so it stopped at the `data` chunk and never reached the
`cue ` and `_PMX` chunks that follow it. It now resolves such sizes through
`ds64` (the `data` size, and a table of other chunks, capped at 1,024 entries),
with checked arithmetic, and leaves the field as it was when there is no
usable `ds64`.

The `adtl` list: every `labl` record scanned the growing list of names, which
is quadratic in the number of records, and nothing bounded how many were
kept. The cue points are now read first, names and lengths are kept only for
those ids in hash maps, and at most 40,000 `adtl` sub-chunks are looked at.
The duplicate check between the cue and XMP markers uses a set instead of a
scan. What a file yields is unchanged for any file within the bounds: a `labl`
still wins over a `note`, and the first `ltxt` still gives the length.

Tests: an RF64 file whose markers follow a sentinel-sized `data` chunk, sizes
taken from the `ds64` table (and not taken when the table has no entry),
hostile `ds64` sizes and tables and every truncation of an RF64 file, and a
100,000-record `adtl` list that is read in linear time, with the cap applied.
The RF64 tests fail with the previous reader.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The end-to-end test called Update Markers From Source only through the
selection. It now also passes the layer as the bound `layers` parameter (what a
script or the menu binding supplies), checks the markers by name, and checks
that the update is one undo step: undo removes the restored marker again and
redo brings it back.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Resolve conflicts in the Effects & Presets panel, the arrow-key nudge and the
docs; update two a11y tests to the prefs-based favourites.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Fixes #1457

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Glow Based On Color Channels, After Effects gives the glow the layer's whole
alpha (blurred), not the thresholded part, and Intensity scales only its colour:
a layer below the threshold darkens what lies under its edge. Fitted to AE 2026
renders of a grey solid over red (cases 524, 807-809): the halo is the same at
Intensity 1 and 3 and with Composite Original Behind or On Top, and its edge is
an erfc step of sigma = 0.384 x Glow Radius at radius 10 and 30 (was radius / 2).

524 (Glow defaults on a 50 % grey solid): EffectCraft had no halo (max 116/255
off at the edge); now red 248/234/208/165/140 at 7.5/5.5/3.5/1.5/0.5 px outside
vs AE 249/235/206/164/140, max 2/255 over the frame, CPU and GPU. The blur
width applies to every glow; alpha-based glows keep their coverage rule
(thresholded alpha, scaled by Intensity). Not matched yet: the brightness of glows above the
threshold (AE about 3.7 x brighter for a white solid, cases 806, 810, 811).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Glow Colors = Original Colors, After Effects thresholds each channel of
the straight colour separately and the glow is 8 x (value - threshold +
0.061) per channel (times Intensity), nothing below: fitted to AE 2026
renders of grey 0.65 / 0.8 / 0.9 / 1, orange (1, 0.5, 0) and (0.2, 0.6, 1)
solids at Threshold 40 / 60 / 80 % and Radius 10 / 30 (36 channel values,
exact to the 8-bit capture; an orange solid glows red only). EffectCraft
scaled the colour by a luminance threshold (k = (L - thr) / (1 - thr)), about
3.7 x too dim on white. Cases 806 / 810 / 811 (32 bpc): outside the layer
now within 3/255 of AE (806 white at Intensity 1: 119 = AE, was 32); grid
0 points off for 806-810. The glow's alpha (the blurred layer alpha) and the
Arbitrary Map / A & B colours and alpha-based glows are unchanged.

Stacked on fix-glow-halo (same lines). Not changed: Composite Original's
semantics are swapped against AE's names (EffectCraft's "On Top" adds the
glow over the original, which AE calls "Behind"; AE's "On Top" keeps the
original over the glow), so imported AE Glows at the default Behind show
no glow inside the layer (811 interior 204 vs AE 255).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Effects' Radial Blur keeps a layer's edges: Zoom 20 on a flat grey 960x540
solid over red (case 713) stays flat and opaque to its edge, and a zoomed
full-frame ramp (case 829) keeps the frame opaque (2/255 at x = 2, 253/255 at
x = 1917 on row 540). EffectCraft sampled transparency outside the layer, so its
edges faded and the red below showed through (713: 3.9 % of the frame off, max
64/255). Now 713 is identical to AE (max 0/255), CPU and GPU. CC Radial Blur
(already matching AE) is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Effects measures the blinds' phase from the layer's centre and softens
both edges of the transparent band with a Gaussian of sigma = Feather / 2; the
band runs from the origin's phase to Completion x Width. EffectCraft measured
from the buffer corner and ramped only one edge linearly over Feather (a hard
edge on the other side). Fitted to AE 2026 renders of Completion 40,
Direction 30, Width 50, Feather 5 on a 960 x 540 and a 480 x 270 layer (the
origin is the centre + 0.6 px): case 715 was 499 of 5184 grid points off
(max 91/255, 7.1 % of pixels > 8), now 28 (0.056 %), all within ~6 px of the
layer edge where AE differs (up to 48/255, not modelled); away from it within
6/255 (test: one AE row of the 480 x 270 render). CPU and GPU (new erf-based
kernel, same term count). Completion 0 / 100 are unchanged, so the
Transitions presets keep their start and end frames.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Effects feathers both edges of the wiped wedge (the start ray too) with
a Gaussian whose width is constant in pixels: Completion 40, Start Angle 30,
Counterclockwise, Feather 20 (AE 2026, case 904 E12) fits sigma = 0.4 x
Feather = 8.0 px at every radius (mean alpha error 0.0002 over the frame).
EffectCraft ramped only the moving edge, linearly over Feather degrees, so
the edge was 14 px wide near the centre and 140 px wide at 400 px out, and
the start edge was hard: 378 of 5184 grid points off (max 111/255, 6.6 % of
pixels > 8), now 1 (max 3), 0.009 % of pixels > 8 (near the centre). CPU and
GPU (the kernel now measures distances to the two rays). 0 % and 100 % are
unchanged, so the three Transitions presets keep their end frames; their
Feather (6 / 10) now means pixels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Effects 2026 draws Fractal Noise's Block noise at Scale 100 and
Complexity 1 in cells of 66.67 px (the value changes every 66.67 px along a
row), not 100 px; Soft Linear noise has the matching correlation length.
EffectCraft's lattice is now Scale x 2/3 px (NOISE_CELL), CPU and GPU. Case
904 E03 (Soft Linear, Complexity 4, Scale 100): the noise's autocorrelation
at 32 px was 0.68 (AE 0.41), now 0.54; pixel values cannot match (other
seeds). Still open: AE's noise spreads about 20 % wider around its mean (std
48 vs 40/255 in E03). Existing projects and the Backgrounds presets with
Fractal Noise get finer noise (two thirds of the former cell size).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Effects draws the Transform effect as position + K * R * S * (p - anchor)
with the skew K = rot(-axis) * shearY(tan skew) * rot(axis) applied after the
rotation, and keeps what lands outside the layer's bounds. EffectCraft skewed
before rotating (shearX about the axis) and clipped to the layer.

Measured in AE 2026 on a grey 960x540 solid (cases 723, 819, 820): 723 (anchor,
position, scale 120/80, skew 10 axis 20, rotation 30, opacity 70) differed in
11.25 % of the frame and now in 0.065 % (edges, max 16/255), CPU and GPU alike;
819 / 820 (skew 15 axis 60, rotation -25, scale 130/70, anchor) match to the
edge. The buffer grows on each side by that side's overshoot (also for the
motion-blur samples), capped at 16 384 px a side and 16 Mpx in all so a layer
moved far away costs a bounded allocation (regression test); adjustment layers
keep their size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The mask coverage was rasterised into the layer-sized buffer and then
blurred, so coverage outside the layer counted as 0: a feathered mask
past the layer edge faded at that edge, and one just outside the layer
gave nothing (the comment said the buffer grew; nothing grew it).

A feathered mask is now rasterised on a grid padded by the blur's whole
support (3 sigma + 3 for the Gaussian, the box radius for a linear
falloff), blurred there and cropped back to the buffer. Wide feathers
(sigma > 32 px) use a grid coarser per axis so the blur spans ~16 grid
pixels, which keeps the allocation near the buffer's size (feather 1000
on 1920x1080: a 160x134 grid instead of ~20 Mpx); the grid is capped,
feathers are clamped and non-finite ones ignored. Unfeathered masks and
masks inside the layer give the same coverage as before. The GPU uses
these CPU masks (adjustment footprint, masked sources): no GPU change.

Tests: case 398 of the .aep corpus rebuilt in the project model (red
solid, adjustment layer with a default Gradient Ramp, a 1920x200 mask
below the layer, feather 1000) within 3 levels of the Gaussian and
within 14 of After Effects' render; a mask right of a solid, a mask
larger than the layer (edges stay opaque), linear falloff, half
resolution, and huge or broken feathers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Smooth feather is three separable box passes of 2 round(0.37 F) + 1
pixels (a quadratic B-spline reaching ~1.11 F either side), fitted to
After Effects 2026 renders of feather 100, 300 and 1000 (rows, columns,
corner diagonal, case 398): max error under 1/255, where a Gaussian of
any width misses by 3/255 or more (it was sigma = F / 2). The coarse
grid for wide feathers picks the step whose three windows add up
closest to the full-resolution ones. Linear is unchanged (unmeasured).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Effects gives Repeater copy i the opacity Start + (End - Start) * i / Copies,
so the last copy stops one step short of End Opacity. EffectCraft reached End
Opacity at the last copy (i / (Copies - 1)). Measured in AE 2026 (case 709: 3
copies, End Opacity 40): copies at 100 / 80 / 60 % (EffectCraft 100 / 70 / 40);
the overlap of the two copies 92 % (EffectCraft 82 %). Now within 2/255 at the
sampled pixels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Effects draws a Rectangle Path clockwise from its top-right corner, or
with round corners from the end of the top-right arc on the right edge;
Trim Paths measures from there (AE 2026, case 930: 400 x 300 rectangles,
Trim 0-25 % and 70 / 30). EffectCraft built the path with kurbo, starting at
the top-left corner, so trimmed rectangles drew another segment: case 930
was 86 of 5184 grid points off (max 255), now 5 at stroke edges. Start > End
was already drawn swapped, as in AE. Convert to Bezier now gives AE's vertex
order (top-right first); the engine test follows. Lottie rectangles share
the path and now start where lottie players start them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Effects puts the first baseline of paragraph (box) text the height of the
font's lowercase "d" (in whole thousandths of an em) below the box top, at the
text's nominal size; superscript keeps the line there and only raises its glyphs.
EffectCraft used the font ascent of the drawn size plus the superscript rise.
Measured in AE 2026 (case 818, box at y -200): Times New Roman 100 px -130.50
(EffectCraft -110.9), 50 px -165.25, superscript -163.80 (EffectCraft drew it
15.8 px lower), Arial 100 px -128.40, superscript -161.70; now within 0.15 px.
517 (four wrapped lines, Times New Roman superscript): the line bands now land
on AE's rows exactly (336-388, 434-486, 532-584, 643-682; were 16 px lower).

A face without a "d" outline, and vertical type (not measured), keep the font
ascent. The paragraph-box test now
expects the caret (full ascent) to start a little above the box.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Justify All After Effects stretches the paragraph's last line too, and
its every-line composer breaks the paragraph so that line is not a lone
word: a 400 px box that the other justifications break into 6 / 6 / 1 words
breaks 4 / 4 / 4 (AE 2026, EXPORTER read-back t6). EffectCraft's composer
left the last line's slack out of its cost, so "twelve" stood alone on a
stretched line; it now counts that slack for Justify All and every word
starts within 1 px of AE's (what remains in that box is the known box-text
baseline offset). Other justifications and the single-line composer are
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Effects 2026 lays out right-to-left point text whose paragraph carries a
justified code (justify last left / centre / right, justify all) flush at
its start, the right, like right-aligned text (EXPORTER read-back t7: point
rows with codes 3-6 at -152..-8 from the anchor). EffectCraft placed it by
the last-line side (left / centre / left). Left-to-right point text and box
text are unchanged; AE's slightly wider right-to-left box wrapping (1-2 px
narrower word gaps) stays open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A text animator's Tracking Amount is in pixels in After Effects 2026, not in
1/1000 em like the Character panel's tracking: Amount 30 on every character
adds 30 px between neighbours (half before, half after each) at 80 px and at
40 px. And a Subtract range selector over characters the first selector
leaves out keeps them at 0 instead of selecting them negatively. With both,
the EXPORTER's t8 layer D (Tracking 30 on characters 2-4 by index, Subtract
over the last 20 %) puts every glyph where AE does (it ended 88 px short,
1553 vs 1641; now 1641).

The "Tracking In" text preset animates 40 -> 0 px (was 400 -> 0 thousandths
of an em, about the same at 100 px text); Lottie export writes a style run's
tracking (1/1000 em) as pixels for its tracking animator, which Lottie
players add in pixels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With per-character 3D on, After Effects turns each character by Rx * Ry * Rz
(Z first), as it turns a 3D layer's X / Y / Z Rotation (case 931, AE 2026:
"H" at 400 px with animator X / Y / Z Rotation 40 / 30 / 20). EffectCraft
composed Rz * Ry * Rx, so the glyph faced another way: 23 of 5184 grid
points off (max 255), now 3 at the anti-aliased edge (mean 0.015/255). The
same order fix-orientation uses for layers; texts with one rotation axis
are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Above MAX_GRID_PX the padded feather grid fell back to the unpadded buffer, so a
feathered Add mask just outside a very large layer (e.g. 8192 x 8192, feather 40)
covered nothing and the whole layer vanished (CodeRabbit on #1874). The grid is now
coarsened by 2, 4, ... until it fits; radii and padding scale with it, so the feather
of an edge outside the layer still fades into it. The doc comment on the Linear falloff
no longer says it is unchanged: wide linear ramps now go through the coarse grid.
Regression tests: a_grid_too_big_is_coarsened_and_keeps_its_padding,
a_coarsened_grid_still_feathers_a_mask_outside_the_buffer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kernels.wgsl repeats Glow's bright-pass gain 8.0 and knee 0.0611 as literals; they now
name effects::GLOW_GAIN / GLOW_KNEE (exported) and the test
glow_constants_match_the_cpu (no GPU needed) fails if they drift. docs/effects-parity.md:
the summary row no longer says no effect is compared with After Effects while the
Blur & Sharpen row reports Radial Blur's comparison (CodeRabbit on #1873), and the
Radial Blur changelog row says CPU only (it has no GPU path).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- first_ascent: a "d" shorter than 0.3 x the font ascent (a symbol or icon font) no
  longer puts box text's first baseline almost at the box top; the font ascent is used,
  as before the "d" rule (CodeRabbit on #1875). Test:
  a_short_d_falls_back_to_the_font_ascent.
- CharXf::track_before / track_after are pixels, not 1/1000 em (CodeRabbit).
- The Lottie export of style-run tracking is back to what it was: the pixel conversion
  was not checked against a player, so it is left for a separate, verified change.
- The Subtract clamp in Mode::combine is intended for every animator property: the
  selectors combine into one selection per character for the whole animator, as in
  After Effects; the read-back observed it through Tracking (now said in the code).
- Per-character 3D rotation notes that #1876 adds Mat4::rotation_xyz for the same order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sample-box search rescanned every candidate box and the grain statistics
blurred the whole image three times per layer, although Match Grain only reads
the 80 sample boxes (~6.5k pixels). On a 4K frame that analysis was ~90 % of
the effect.

- measure the statistics from small crops around each box (3 px halo, the
  Gaussian's support), deduplicating overlapping boxes so the sums match the
  previous masked version;
- search the candidate box rows in parallel (the sort is a total order, so the
  chosen boxes are unchanged);
- compute the "own" boxes and stats only when Compensate for Existing Noise is
  on, and clone the frame for Preview composition only in Preview mode;
- run Plane::transpose/map_rows on the calling thread for small planes: the
  rayon dispatch dominated per-box crops (240 blurs of ~15x15).

Release, Ryzen Threadripper 1920X, noisy source / flat target, defaults, best
of 4 (1080p / 4K):
- grain statistics: 151/463 ms -> 1.2/2.8 ms
- sample boxes (source): 43/179 ms -> 16/71 ms
- end to end: 266/1416 ms -> 99/465 ms (Compensate 50 %: 392/1830 -> 104/482)

Tests: the boxed statistics match the masked ones (overlapping and corner
boxes, alpha holes), plus the existing Match Grain/Remove Grain suites.
The crop kept 3 px of halo, which makes the blur exact inside a box, but the
lag-1 correlation also reads the right neighbour's high-pass residual — one
column outside the box, still polluted by the crop edge when another box
covers it (adjacent boxes are the norm with a step of size/2). The size
estimate drifted by ~5e-5 in the parity test; a 4 px halo removes it.

The parity test now holds std to 1e-6 and size to 1e-7 (was 5 % for size),
and the boxed statistics still cost 1.8 / 3.1 ms at 1080p / 4K.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Copying keys or unanimated properties of one layer records them as After Effects keyframe text: its Transform properties, one row per key with the frame in comp time (start offset and stretch applied) or a frameless row for a static value. EffectCraft's own keyframe clipboard is unchanged.
Edit > Copy with keys or Transform properties selected puts the After Effects keyframe text on the system clipboard, for other applications such as AE2Blend, in place of the 'EffectCraft: N keyframes' note. Ctrl+V in EffectCraft still pastes from its own clipboard.
Found while testing crates/geom/src/lib.rs:252-264

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A table of After Effects' index and unit for the effect parameters its keyframe text gives differently from EffectCraft's rules, read from After Effects 2019's text for 230 effects. All 1,530 parameters the two share now match; 949 did before.
macOS does not install the on-demand PingFang SC (macOS 27 on an English
install ships Hiragino Sans GB, Heiti SC, Songti SC and STSong instead),
and the Simplified Chinese fallback list named only Windows and Linux
families. A Chinese character therefore fell through to the exhaustive
search, which picks the first installed face that covers it — Hiragino
Sans, the Japanese face — so Chinese text in a composition was drawn
with Japanese glyph forms (#453). Japanese kana and kanji were already
handled, and a Chinese-only character the Japanese face lacks (e.g.
页) still found a Chinese face, which is why the symptom looked
selective.

Add the macOS Chinese faces to the fallback lists: Hiragino Sans GB,
Heiti SC, Songti SC and STSong for Simplified Chinese, Songti TC for
Traditional (Heiti TC was already named). The interface language still
chooses between the scripts and an English interface keeps the
Japanese-first order for shared Han ideographs, as designed in #312;
what changes is that a Chinese-locale request has an installed Chinese
family to answer it on macOS.

Tests: a text-layer render test draws Japanese and Chinese text and
checks glyph coverage (skipped where no CJK face is installed); a layout
test checks every CJK glyph comes from a face that has the character, so
a missing-glyph box cannot creep back; and the fonts test fails without
the list change ("Hiragino Sans GB missing from the Han list"), passes
with it, and walks the Han branch's list to prove the first installed
Simplified family is a Chinese face, not a Japanese one.

Verified with CARGO_BUILD_JOBS=2 RUST_TEST_THREADS=2:
effectcraft-text 64 passed; effectcraft-render 233 passed, 1 ignored;
cargo clippy -p effectcraft-text -p effectcraft-render --all-targets --
-D warnings clean; cargo fmt --all -- --check clean. A 900x240 render of
"骨直真具画 中文测试" with the zh-hans request now uses Hiragino Sans GB
and differs from the Japanese render, which still uses Hiragino Sans.
The panel catalog behind the menu bar translated the dialogs and
Preferences, but much of the work area was still English: panel tabs, form
fields, panel context menus, the tracker and graph controls, text-panel
options, the File panel's footers and the credits. Add 300 rows to the
Japanese catalog — and the matching Simplified and Traditional Chinese
sources the tests keep it in step with (the other languages may lag, an
untranslated row falls back to English):

- panel tabs (dock.rs / dock_ui.rs titles), the Project panel's columns,
  footer tooltips and column menu;
- form and dialog labels and choice options (the forms the panels open —
  import, interpret footage, render queue, trackers, text animators…);
- panel context menus (timeline layer/property menus, graph editor);
- the tracker, graph editor, text-panel, animator and Content-Aware Fill
  labels;
- the Preview panel's transport tooltips, credits table headers and sort
  options, the composition-settings tabs and the About links;
- the engine's text-animator menu labels, added as a data source like the
  settings pages (they are interface text the panels draw).

Strings are wrapped where they are drawn with the existing `tr`/`tr_args`
mechanism (the label helpers already ask the catalog, so the data tables
need no new ids); the catalog tests keep the three complete languages
listing the same sources, reject duplicate or empty rows and keep the
placeholder counts. A new test assertion in
`japanese_rows_translate_the_panels` pins a sample of this step.

The panel catalogs are now 993 Japanese and 995 Chinese rows (the two
Chinese catalogs are the complete source list); with the 628-row menu
catalogs Japanese is 1,621 of ≈ 4,200 interface strings (39%, up from 27%).

Still English: effect parameter names (≈ 1,650 registry names, drawn from
the model, not the catalogs), Home-screen literals, tooltips drawn from
engine data, engine error messages, and the rows other languages lack.

Docs: localization-parity (header, per-language table, overall share,
revision history), ROADMAP (language table, progress log, revision history).
The camera's Set Focus Distance to Layer command compared only 'd <= 0.0', which NaN passes, and two finite but extreme positions can overflow their difference to infinity; a stored non-finite focus distance serializes as null and leaves the project unsavable. Reject it, fall back to the zoom plane in the camera evaluation, and count a non-finite focus or depth as in focus. The Classic 3D blur radius itself was unbounded: a 0%-scaled or very distant layer scales it by up to 1e9, and the padded blur buffer sized from it aborts the process. Cap each plane's radius to a padded buffer that fits the renderer's 16 Mpx layer budget, don't blur planes whose pixels cover under a thousandth of a comp pixel, saturate the padded sizes, and refuse padded buffers past the project's maximum layer size. Regression tests for each guard.
The macOS fallback test selected the first installed family from the
Simplified list, so a Mac that also has PingFang SC (or Microsoft YaHei /
Noto Sans CJK SC) picked that one and then failed an assertion that only
accepted the three macOS faces. Accept any family from the list -- every one
is Chinese -- and check that each installed macOS face covers the shared
ideographs instead.

Also exercise fallback_for itself under a zh-CN request, which the direct
resolve_in lookup bypassed, and serialise the process-global locale with the
layout CJK test so a cached family order cannot leak between them.

Tests: effectcraft-text 75 lib + 3 integration passed; effectcraft-render
233 passed, 1 ignored; cargo fmt --all -- --check and cargo clippy -p
effectcraft-text --all-targets -- -D warnings clean.
Review follow-up to the panel/dialog step: the composition-settings tabs
translate "Basic" and "Advanced" through `tr(variable)` and the Preview
transport translates "Play/Stop", but none of the three had a catalog
row, so they stayed English. Add them to the Japanese, Simplified and
Traditional Chinese catalogs and pin them in
`japanese_rows_translate_the_panels`.

The shared "Type" key means "kind" in the Project column and the Camera
Settings dialog, so the Chinese rows read 类型/類型 instead of 文字
("text").

Keep the graph button bar's automation labels English: translate only
the drawn label and tooltip, and translate the stored English strings
when the hidden-button menu draws them.

Recount the docs from the catalogs: 628 menu rows, 996 Japanese and 998
Chinese panel rows; count Simplified Chinese's 306 effect names in its
numerator (46%) and average the ten After Effects languages the table
actually names (21%, not the stale 27%). Update the measurement basis,
the cross-references in gaps.md and target-app-parity.md, and ROADMAP.
1,626 is the Traditional Chinese total translated-string count; the panel
catalogs carry 998 source rows each (996 in Japanese), as
docs/localization-parity.md measures.
The test checked that Japanese text falls back to an installed Japanese
family, but left the process-global CJK locale at the host's: on a zh-CN
host with a Chinese face installed, han_order puts the Chinese families
first for the shared ideographs and the Japanese-family assertion fails.
Set ja-JP under the TEST_CJK_LOCALE lock and clear the request when the
test ends, as the zh-CN test beside it does.

Tests: effectcraft-text 75 lib + 3 integration passed; cargo fmt --all -- --check passed.
The per-accessor count checks from #1235 already reject the audit's trigger
(a MAT4 accessor with count 10^11); a regression test now pins it. What was
left: accessors without a bufferView were capped at 16M components each, but
nothing backs them, so one capped accessor referenced from many primitives
allocated 64 MB per reference. The cap is now one budget for the whole file.
Per-element offsets use checked arithmetic as well.
148 of the 307 ids. Names from AE 2026's Japanese interface and from Adobe's Japanese help,
which is what settles the `CC` rows staying English and `Smart Blur` being `ブラー(詳細)`.
Three rows rest on elimination and are called out in the PR.

Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
eval, Function and the generator/async function constructors now check the nesting of their source like the expression text. join, toLocaleString, flat and JSON.stringify check the run's stack use per level, results nested past the detached levels become [object], and Proxy/Reflect are no longer available to expressions. Each of these overflowed the stack and aborted the app.
Tint is 色合い, Levels is レベル, Exposure is 露出, Stroke is ストローク and Strobe Light is
ストロボライト. The list used for cross-checking carried Photoshop's words for three of them.

Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Adobe's per-effect pages say 色合い / レベル / 露出 / ストローク / ストロボライト, but its effect
list says 色かぶり補正 / レベル補正 / 露光量 / 線 / ストロボ, and the list is what AE's
interface follows. The interface wins.

Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
285 of 307 ids. The 22 that stay English are the ones no source names: the OCIO set,
Screen Key, Unmult, Curl Noise, Puppet, Twirl (Legacy), Stereo 3D Controls, the two face
tracking effects, Mocha shape, Paint, and three audio effects.

Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Key Cleaner is キークリーナー and Advanced Spill Suppressor is 高度なスピルサプレッション —
neither is a brand name, and Adobe's keying page names both. The doc comment claimed the
VR and OCIO sets repeat their English names here; the VR rows are Japanese now and the OCIO
rows do not exist, so it says what is true instead.

Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An array replacer now copies each source object once per call, so a cycle stays a cycle and the native check throws TypeError, while shared objects still serialize. Documents which threads evaluate expressions, what the 256 KiB guard covers (tested on a 512 KiB thread), that it does not apply on wasm32, and why eval is indirect.
Read from the Effect menu: OCIO CDL 変換, OCIO 色空間変換, OCIO 表示変換, OCIO ファイル変換 and
OCIO Look トランスフォーム. AE draws 稲妻(高度) and 輝度&コントラスト with half-width marks while
平均化(イコライズ) keeps full-width ones, so the punctuation follows the menu per effect.

Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Adobe help calls it エクスプレッション制御 in prose; the Effect menu says 式制御. The menu is what
a user reads, so it wins.

Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
The menu is the authority and it disagreed with this table in three ways: five rows had been
translated that AE leaves in English (Key Cleaner, Advanced Spill Suppressor, Unmult,
Twirl (Legacy) and mocha shape), eight use a half-width ampersand, and a handful differ only in
spacing (Cineon コンバーター, 3D カメラトラッカー, 基本3D). Five effects the table skipped do have
names: コンプレッサー, 歪み, ゲート, カールノイズ and VR 平面として投影.

Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Reflect is not an expression global, so Reflect.construct never reached the
nesting check; the proxy case asserts Proxy is undefined.
Found while testing crates/svg/src/pathdata.rs:228-233

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Conflict in crates/text/src/layout.rs (line metrics): main's partition_point
slice from #2193 plus this branch's first-baseline maximum. rustfmt applied to
main's paragraph() signature.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	crates/text/src/fonts.rs
#	docs/localization-parity.md
# Conflicts:
#	ROADMAP.md
#	crates/ui-egui/src/dock_ui.rs
#	crates/ui-egui/src/i18n/ui.rs
#	docs/gaps.md
#	docs/localization-parity.md
# Conflicts:
#	crates/ui-egui/src/i18n/ui.rs
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.
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.
`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>
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.
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>
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.
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.
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>
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.
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>
Found while testing crates/time/src/lib.rs:201

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Fixes #2128

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
File ▸ Scripts gets a Recent Script Files submenu with the ten scripts run
last, newest first: script files run with Run Script File… or a command, and
the installed and bundled scripts of File ▸ Scripts. ScriptUI panels are not
listed. The list is kept in the settings. Each entry runs its script again
through `file.runScript {"recent": i}`, and Cmd+Alt+Shift+D runs the most
recent one; the key can be changed in Edit ▸ Keyboard Shortcuts. An entry
records whether it ran a file or an installed script, so a file named like a
bundled script runs again as that file.
A subfolder of a Scripts folder can be a link to another folder (an extension's own checkout).
`file.uninstallScript {"name": "Motion Kit/Animate.jsx"}` then deleted Animate.jsx in that
folder, outside the settings folder. Uninstall now refuses `Folder/File.jsx` names and removes
only scripts at the top of the Scripts folders, where Install puts them; listing and running
through a linked folder still work.

Regression test: uninstall_never_deletes_through_a_linked_subfolder (failed before the fix).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A script run from a relative path is stored with its absolute path, so
choosing it again runs the same file after the working directory changes.
The web build keeps its file keys as they are (Services::full_path).

Two recent scripts, projects, footage files or presets with the same file
name are listed with their paths instead of the name alone.
It fell back to any script, so a script outside the ScriptUI Panels folder opened as a panel and
was remembered, then reported missing and forgotten at the next launch (restore looks only for
panels). Review feedback from CodeRabbit on #2289.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DPX and Cineon footage showed its log code values as stored, with no
way to convert them. Interpret Footage now has the Cineon Settings that
After Effects' help page describes: Logarithmic Conversion, the
Converted Black and White Points, the 10 Bit Black and White Points,
Current Gamma, Highlight Rolloff and Units. With Logarithmic
Conversion on, the code values of each decoded frame are converted
from printing density to the Current Gamma between the converted
points, before Separate Fields, Frame Blending, Interpret As Linear
Light and the working space use them. With it off (the default), the
values stay as stored.

file.interpretFootage and file.interpretProxy take the settings as
parameters and report them. They return an error for out-of-range
values, and for Cineon parameters when none of the items is a DPX or
Cineon file. Apply Interpretation copies the settings between DPX and
Cineon files.
The close decision moved to App::logic, which runs before App::ui, while
the queued file-dialog answers still ran in ui. An answer that arrived in
the frame that closed the window changed the project after the decision,
so the window could close without asking to save. The answers now run in
logic, before the decision.
execute_checked refuses any key a command's params doc does not list, so
MCP, the CLI and the control channel could not pass preserveRgb,
overrideStart, startTimecode or guessAlpha to file.interpretProxy. The
command reads all four, and the shared Interpret Footage form sends
preserveRgb for every proxy, and overrideStart and startTimecode for a
movie or sequence proxy. file.interpretProxy's doc now lists them. The
docs of file.interpretProxy and file.interpretFootage list premulColor
beside matteColor, which both commands read as the matte color. The
docs of file.interpretFootage, file.applyInterpretation and
file.reloadFootage list `item` beside `items`, which the three commands
already read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
settings.rs: keep upstream's measured page-list width, text_fit and
translated page titles, and the PR's per-page ids, list-item roles,
keyboard focus and focus ring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Footage panel, the Project panel thumbnail and the native-size source
frames that tracking, mask tracking, scene detection and auto-trace read took
DPX and Cineon frames straight from the file, so they showed the log code
values while a composition showed the converted ones. Each now applies the
footage's Cineon Settings, as the composition renderer does.
Opening a project runs migrate::upgrade_instance on every effect, so an
instance saved before a parameter was renamed, moved into a twirl-down
group, added or had its popup reordered takes the current shape.
Apply Animation Preset added a preset's effects as saved, without that
upgrade. A Change to Color saved with its single Tolerance applied
with that old property and without the Tolerance group, so the effect
did not read the saved tolerance.

apply_preset now upgrades each preset effect before adding it, sized by
the target layer's effect bounds as upgrade_effects does. A lone effect
property saved at a renamed or moved parameter id
(effects/ec.color.changetocolor/tolerance) is rewritten to the current
id through migrate::current_param_id, so it is applied instead of being
listed in skipped.
Every pixel of a (2r+1)^2 window shares one fractional offset per level
and iteration, but lk_point called Plane::sample (with a libm floor, a
bounds test and the clamp path) up to three times per pixel. The offset
and the clamped column/row index tables are now computed once per
window and the pixels run the same lerps on integer indices.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The feature mask tested every non-skin window pixel against the 48-edge
face outline (about 25 M edge tests per frame). A scanline hits the same
edges for every pixel of a row, so the crossings are now computed once
per row with the same expression and comparisons as inside_polygon, and
a pixel's parity follows from how many lie to its right. The face-frame
rotation is also worked out once per frame instead of once per blob
pixel.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Store the two four-element DLT rows in arrays while preserving arithmetic and accumulation order. This removes two heap allocations for every triangulated view.

Fixes #2326
step() cloned the whole RGBA frame to remember it (33 MB at 1080p,
133 MB at 4K) and then cropped that clone, although only the region
around the outline is ever read. The tracker now stores the previous
frame already cropped to the region the next step will use, computed
from the updated outline with the same margin and rounding as before,
so the crops, and therefore the tracking result, are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The normalized cross-correlation only had a fast path for an exactly
identity shape, which tracked footage never has after the first frame, so
every candidate went through Plane::sample. All candidates of a search are
the reference centre plus integer offsets, so each template pixel has the
same fractional offsets for all of them. Build one table of taps per
(level, seed, shape) and score each candidate with integer-indexed lerps,
with one bounding-box check up front (outside the plane the old path is
used).

lk_refine only reads the level-0 gradients around the match, so compute
them for that window (with an interior fast path in Plane::gradients_in)
and fall back to the full gradients if the iteration leaves it.

The results are unchanged: same lerp form and accumulation order.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Use a bounded inline array for the current camera parameters and preserve the existing accumulation order. Extra parameters spill safely if the parameter set grows.

Fixes #2327
Return errors for invalid bundle observations and oversized workspaces, reject non-finite projections and mismatched correspondence slices, and prevent track frame overflow. Cover the reported failures with synthetic regression tests.

Fixes #2330
Unfiltered, list_commands returns ~134 KB and list_effects ~480 KB, more
than MCP clients accept in one tool result. With `compact: true`,
list_commands (and its command_list alias) returns only each command's
id and label, and list_effects only each effect's id, name and category
(~40 KB and ~23 KB). Default output is unchanged; filter and
enabled_only still apply.
Detect narrow lower-triangle bandwidth and skip structural zeros while preserving dot-product lanes and signed-zero arithmetic. Keep a dense fallback for wide systems and compare factors and solutions against the original algorithm.

Fixes #2325
Share the unchanged Jacobi factors between full SVD and null-vector callers. Select the same right singular vector directly instead of normalizing U and sorting copies that the caller discards.

Fixes #2329
The footage page said the 10 Bit White Point becomes the Converted White
Point. That holds with Highlight Rolloff at 0; at the default 20 the top of
the converted range is bent into a curve, as in the Cineon Converter effect,
and the white point converts to a lower level.
Solve expensive camera systems with matrix-free Schur products and a block-preconditioned iterative solver. Check the true linear residual and fall back to the direct solve when convergence fails. Keep short systems on the existing path and compare reprojections, camera parameters and outlier decisions against direct solves.

Fixes #2328
Prepare the default plan through the effects cache after analysis instead of discarding it. Plan from the serialized analysis so the first rendered frame retains identical floating-point inputs and output. Test bitwise render equality and cache reuse.

Part of #2322
For a precomp layer whose composition had a proxy, the renderer
decoded the proxy file with frame_with and drew it as decoded.
Interpret Footage > Proxy's Invert Alpha, Interpret As Linear Light
and Separate Fields had no effect on the render, and neither did the
layer's Frame Blending. A footage item's proxy already goes through
footage_frame and gets all of them.

The composition proxy now goes through footage_frame too, so it is
decoded the way a footage proxy is.
Keep parsed analyses, their FNV key and source text in a bounded shared cache. Compare source contents on hits instead of rehashing the JSON, so cloned parameter strings remain fast while in-place edits invalidate the cache. Cover analysis replacement, key equality, same-allocation edits and retention bounds.

Part of #2322
Near the borders a bubble's refraction (liquid) or reflection (metal) sample
lands outside the layer. sample_bilinear returns transparent there, so the
pale stand-in colour for transparent pixels showed through, a band of washed-out
bubbles along every edge. Sample with edge pixels repeated instead, on the CPU
and in the GPU kernel; the pale colour stays for genuinely transparent pixels.

The CC Bubbles golden hashes are re-pinned (the half-resolution cases pad the
buffer with transparent pixels and are unchanged).
Reuse consecutive caller-owned pyramids for regular Warp motion and trajectory tracking. Preserve the original three-level motion search by setting aside the trajectory-only extra level, without cloning planes. Keep detailed analysis at its existing distinct resolutions and test bitwise motion and complete JSON equality against separate pyramids.

Fixes #2322
The fine stage searched a 5x5 neighbourhood per pyramid level and seed.
When the best coarse match is reliable (NCC score >= 0.8) and lies within
one top-level pixel of the motion prediction, a 3x3 neighbourhood is
enough; otherwise the 5x5 search stays.

Fixes #2349

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A shape layer's Contents > Add menu had every After Effects path operation
except Wiggle Transform. It is now a shape item (layer.addShapeItem kind
wiggler, "ADBE Vector Filter - Wiggler" for scripts) with Wiggles/Second,
Correlation, Temporal Phase, Spatial Phase, Random Seed and a Transform
group whose Anchor Point, Position, Scale and Rotation set how far each
transformation wiggles.

The values come from the Wiggly selector's noise, with each Repeater copy
as one unit: the copies of a Repeater above the operation, in its group or
a nested one, wiggle independently as far as Correlation allows, and the
copies of a Repeater below it wiggle the same way. A paint above it whose
paths all move by one matrix moves its gradient and stroke scale with them.
The layer cache keys a layer with a Wiggle Transform by time.
Edit ▸ Keyboard Shortcuts now lists the scripts File ▸ Scripts lists, under
File ▸ Scripts, and a key given to one runs it as its menu entry does. The
key is bound to the script's file name (file.runScript {"name": ...}) and is
saved with the preset. File ▸ Scripts and the macOS menu show it.

The shortcut table lists the Scripts folder again when a script is installed
or uninstalled, when the editor opens and when shortcuts.list runs.
shortcuts.set and shortcuts.reset look a script up in File ▸ Scripts' list
as it is now, and a script it doesn't list is an error instead of a key for
Run Script File....
Conflicts in project/src/lib.rs and ui-egui/src/frames.rs: Comp::same_settings
takes with_markers and is shared by same_pixels_with and changed_span (#1641);
frames identity uses same_pixels_with/reads_markers and drops
same_pixels_ignoring_markers and project_has_expressions.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
encode_pulses rebuilt the full V(n, k) table with saturating u64 adds for
every coded band and summed the prefixes through a closure. Build the table
once (n <= 176, k <= 128, saturated at u32::MAX, 91 KB) and sum the prefixes
over a contiguous row slice. Shapes outside that range, which the band
layout never produces, fall back to a local table. The unused scratch Vec
is gone from BandEnc.

The bitstream is unchanged; a test keeps the old per-call encoder and
compares the coded bytes on random codewords over the whole (n, k) range.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The inverse transform computed its bit-reversal permutations and loop
bounds at run time and went through one generic path for every size.
inverse_dct, the ADST permutations and inverse_2d are now const generic
over the size and inlined, so the permutations fold at compile time.

forward_2d zeroed a 4 KiB tmp buffer on every call, even for 4x4. It now
dispatches to a fixed-size version for 4, 8 and 16 (tmp is exactly n*n
floats); 32 keeps the run-time version, which measured faster than a
const 32 variant. The float operation order is unchanged.

Output is bit-identical.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The LPC to LSF conversion scans a 1024-point grid for sign changes of two
cosine series and called libm cos for every series term, about 21,000 calls
per channel and frame (30-40 % of SILK and hybrid encode time). Compute
cos(m * w) for every grid point and multiple once, with the same expression
as before, so the values are bit-identical and the bitstream does not change.
Only the bisection steps still call cos.

A test compares the new root search against the previous one bit for bit on
random symmetric polynomials; a2nlsf rejects orders beyond the table.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Each root was refined with 40 bisection steps, which shrinks the bracket to
about 3e-15 rad. A Q15 NLSF step is 9.6e-5 rad, so 20 steps (3e-9 rad) are
far more than the quantiser can see. The saved cos calls are the remaining
bulk of the root search after the grid table.

The coded packet streams are byte-identical to the 40-step search on 24
configurations of 90 s: speech-like and music-like input, mono and stereo,
SILK NB 8k, MB 12k, WB 16k and 32k, hybrid SWB 32k and FB 64k.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The contour search tries 5 base lags times 34 codebook contours times 4
subframes, 680 normalised correlations, but only about 4 x 30 distinct
(subframe, lag) pairs occur. Cache them per call. The values are the same
f64 results as before, so the bitstream does not change (cmp on 24 SILK and
hybrid configurations of 90 s, mono and stereo, speech and music).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The quantiser in try_tx called libm floorf for every coefficient, estimate_bits
and the motion-vector cost called f64::log2, and coefficients that quantise to
zero still went through an f32 division.

- (x / q + rnd).floor() as i32 becomes (x / q + rnd) as i32: the operand is
  never negative, so truncation equals floor.
- log2 of small integers comes from a table built with the same f64::log2
  call (estimate_bits and the MV cost in motion_search), so the values are
  bit-identical.
- Coefficients below an exact per-(q, rounding offset) threshold are zero
  without dividing. The threshold is found once per Decider by a binary
  search over f32 bit patterns of the monotone quantiser expression.

The encoded stream is unchanged. Unit tests compare the new quantiser with the
old expression at the threshold boundary and on random values for every
quantiser index, and the table with f64::log2.

Fixes #2373

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Every FFT stage was a generic DFT butterfly: p * p complex multiplications
per butterfly (16 for radix 4) and a modulo on every twiddle index. Add
kiss_fft-style kernels for radix 2, 3 and 4 (three twiddle multiplications
and eight additions for radix 4, a sqrt(3)/2 constant for radix 3, +-j done
by swapping parts). The generic butterfly stays for radix 5, which occurs
once per transform, with its roots precomputed. The modulo is dropped: the
twiddle index q * k * tw_stride is always below n.

The results differ from before at f32 rounding level, so a few packets
change on speech and hybrid input (CELT music stays byte-identical). A new
test checks the FFT against a direct DFT for every radix combination up to
the 480-point frame size.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Decider::new built the half-sample planes (8,0), (0,8) and (8,8) with
three full 2-D 8-tap passes (subpel_plane), each with per-tap clamping.
half_planes computes them together: the horizontal half-sample pass is
shared by (8,0) and (8,8), the phase-0 kernels are exact shifts
(Round2(128*v, 11) = Round2(v, 4), Round2(16*s, 11) = Round2(s, 7)), and
interior pixels skip the clamps.

subpel_plane is kept as the test-only reference; a new unit test checks
half_planes against it bit for bit (including 1x1, 5x3 and 10 bit).
The only change in decide.rs is the call in Decider::new.

Output is bit-identical.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
pvq_search scored every coefficient in a serial compare-and-select chain per
pulse. Score all coefficients branch-free in 8-lane chunks instead (square
over energy, one division), take the maximum, and run the original exact
cross-multiplied test only on the entries within 1e-5 of it, which is
normally a single entry whose index comes from a lane-wise count. The chosen
pulse, and with it the bitstream, is unchanged; a test compares the result
with the old scalar search on 6000 random, sparse, tied and extreme-scale
vectors. The pre-projection also uses the truncating cast instead of a floor
libm call (the value is non-negative).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The decimator computed each output as one sequential f32 dot product over
167 taps, which is bound by the 4-cycle latency of the float addition. Use
eight independent partial sums (combined pairwise) so the compiler can
vectorise the loop.

This changes the input of the SILK encoder at rounding level, so the
bitstream differs from before; decoded quality and size are unchanged to
within noise (see the PR for SNR, segmental SNR and size at several
bitrates). A test checks the dot product against an f64 reference.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
try_tx and reconstruct allocated six Vecs per candidate (residual, coefficients,
levels, dequantised coefficients, inverse residual, reconstruction), the
coefficient writer allocated its level map per transform block, and the loop
filter level search cloned the luma plane for every candidate level.

- Decider keeps a boxed TxScratch (32x32 arrays) that try_tx and reconstruct
  write into; the levels and the reconstruction of a candidate stay there and
  the caller copies out only what it keeps (the winning intra candidate, or the
  levels that go into the leaf). intra_leaf uses stack arrays for its
  predictions and a fixed array for the mode ranking and transform types.
- TileWriter reuses one level-map Vec, and the partition CDF at frame edges is
  read through a slice instead of a copy.
- The level search filters one working copy of the luma plane and restores it
  from the reconstruction before each candidate.

The encoded stream is unchanged.

Fixes #2374

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Bitrate mode gave key frames keyint-dependent extra bits (about 6.8 frame
budgets at the 60 frame default) but left the inter frames at the full
frame budget, so every group of frames overshot by (boost - 1) / keyint,
9.6 % at keyint 60 (up to +187 % at keyint 5 on some content). The
accumulated deviation was tracked against the boosted budget, so the
feedback never saw it.

Now a group of keyint frames adds up to keyint * frame budget: inter
frames get (keyint - boost) / (keyint - 1) of it. Further changes to the
frame level control:

- The quantizer follows the size error with a measured slope (quantizer
  steps per halving of the size, taken from consecutive inter frames)
  instead of a fixed 12 steps per doubling. On noisy content the curve is
  several times steeper than that and the old gain oscillated between
  1 kB and 42 kB frames.
- Key frames have their own quantizer offset, adapted from key frame
  sizes only, instead of feeding the key frame error into the inter
  quantizer. A key frame after the first that misses its budget by more
  than 19 % is coded again (at most twice more), with a corrected
  quantizer found in the log domain of the quantizer step.
- A deviation from the budget is paid back over 16 frames, bounded to
  0.35x..2x of the budget.

Constant quantizer mode is untouched (byte identical output). The
encoded stream in bitrate mode changes.

Adds a test that the rate stays within 5 % of the target over 180
frames for three 8 bit and one 10 bit setting.

Part of #2391

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Bitrate mode coded the first frame four times with a fixed gain. Now the
first pass still gets the old correction (30 quantizer steps per halving
of the size, which fits the starting range), and from the second
correction on the next quantizer is placed in the log domain of the
quantizer step, where the frame size is close to a line: false position
between the nearest passes that enclose the target, otherwise a secant
step with the measured slope. The search stops as soon as the size is
within 3 % of the target, when no untried quantizer is left between the
enclosing passes, or when it would repeat a quantizer (a target the
content cannot reach, where the old code coded the same frame at the
lowest quantizer up to four times). At most four passes remain.

The first frame is within 3 % of the target on the same cells as before
and nearer to it on most of the others; two cells where the size jumps
by a factor of 4 or more between neighbouring quantizers (noisy content)
land further away than with the old search. Total passes over the 28
test cells: 112 -> 70.

Part of #2390

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Layer > Create > Create 3D Layer Instance, as After Effects (public docs):
3D model layers take no masks or effects, so the command puts a comp-sized
solid at the top of the layer stack with CC Composite referencing the
selected model, and switches the model off (back on to keep its shadow) -
a 2D copy of its render, through the comp's camera, that 2D effects can be
applied to.

- CC Composite (ec.channel.cccomposite) gains an optional Layer parameter:
  with a layer chosen it takes the original's place; In Front replaces the
  solid's pixels, alpha included, the other Composite Original modes blend
  with them. The GPU compositor runs such an instance on the CPU.
- FxHost serves a referenced model or primitive layer as After Effects
  does - the layer rendered on its own through the comp's camera
  (Renderer::solo_3d); the owner keeps no cache entry for it (it moves with
  the camera and lights, without a trace in its own properties).
- layer.create {op: instance3d} skips layers that are not 3D model or
  primitive layers (the ones that take no effects); one undo step, the copy
  selected, the model's video off.

Tests: tests_instance3d.rs (behaviour, pixels, undo/redo, serde, 2D effects
on the copy), a CC Composite layer-parameter unit test; docs/effects.md
regenerated (3033 parameters).
solo_3d passed the layer to the 3D pipelines whatever the comp time, so an
instance (CC Composite referencing a model layer) rendered before the source
layer's in-point or after its out-point. Like any other layer reference, there
is nothing to read outside the range: return the transparent canvas.
The op ran Session::edit once per selected layer, so each copy was a history
step of its own: undoing removed them one at a time. Collect the eligible
models first and create all the copies in a single edit — one undo (or redo)
removes (or restores) them all, and each copy keeps a name of its own.
"შემოუერთდით ArtCraft-ის Discord-ს" ran past the right edge of the
fixed-width Discord button at the foot of the Home sidebar (v0.17.0,
macOS). "შემოგვიერთდით Discord-ზე" ("Join us on Discord") fits; the Help
menu item keeps its full wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Home quick-start tile clips its label to the tile; "სადემონსტრაციო
პროექტის გახსნა" was cut off after "პროექტის". "დემო-პროექტის გახსნა"
fits, like the Import Footage tile; the File menu item (file.openDemoProject)
keeps the full wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Measured on a second machine (Noto Sans CJK JP), the ink-bottom gap after the
correction is 1.125 / 1.9375 / 1.71875 pt at 12 / 15 / 18 pt, so the old bound
failed at 15 and 18 where the interface is right. Without the correction the kana
sits 3-5 pt above the Latin ink, so the sign separates the two and the magnitude
only has to allow the face's own kana overshoot.

Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
epaint exposes no baseline, but a capital without a descender has its ink bottom on
it, so the Latin half of the pair is load-bearing. Suggested by FeiLiuEM in #1309.

Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
278 new rows (417 → 695), the Chinese catalogs' source list, keyed by the English
source string: panels, dialogs, tooltips and the Settings pages stop falling back
to English for every string the zh-hans catalog covers, including the six keys
the batch merges added on top. Terminology follows After Effects' Brazilian
Portuguese conventions; brand names stay English.

AI-assisted translation (OpenCode agents, GLM); not yet native-speaker proofread.
The batch merges grew Japanese 410 → 693, both Chinese catalogs 506 → 695,
Ukrainian 505 → 524, Czech 501 → 518 and Spanish 513 rows (its #996 landed)
without touching the doc's counts or shares; the overall figure was still the
10-October measurement. pt-br was the last count to update, so the row now
mirrors the code at this tree.
Decided in the native-speaker review of the screens (all-t01).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMTAbGNRxnMEzRvYnQGphF
Keep the font rescan poll on the logic tick after main moved frame work out of App::logic.
The native menu's state key hashed only the number of shortcut
bindings, so changing a binding from one key to another left the
number the same and could keep the old accelerator on the item. The
key now hashes each binding and its keys.
# Conflicts:
#	crates/effects/src/camera_tracker.rs
#	crates/effects/src/warp_stab.rs
Wiggle Paths had Size, Detail, Points, Wiggles/Second, Correlation,
Temporal Phase and Random Seed, but no Spatial Phase, which After
Effects lists among its properties (ADBE Vector Spatial Phase). Spatial
Phase now moves each point's own displacement along its subpath: 360
degrees is once around, and a fraction of a point is interpolated
between neighbors. At 0 degrees a path draws as before.

A project saved without the property gets it at 0 degrees, after
Temporal Phase, when it opens. Scripts can read and set every Wiggle
Paths property by its match name.
The 16 quarter-sample phase planes are built with 16-bit lane filters (the same
narrowing trick as the decoder), edge replication copies rows instead of
clamping every sample, and mc::predict reads straight from the reference for
blocks away from the edges. The output is byte-identical.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The encoder's loop filter runs three trial passes per frame with a scalar
filter. Filter the eight positions along an edge together with 16-bit lanes
(ported from the decoder's loop filter); the scalar filter stays as the test
oracle and a new test compares both on 20000 random cases. The output is
byte-identical.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The 16 outputs of the 4x4 forward transform are accumulated together in the
same order as before (no serial add chain per output) and the quantiser
truncates instead of calling floor on a non-negative value. The output is
byte-identical.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The 8x8 and 16x16 SAD uses per-column 16-bit sums, and the cost of a motion
vector (a pure function of the vector) is remembered for the vectors the
search tests again. The output is byte-identical.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- delta_for no longer searches 0..255 for every probability: a 256x256 table is
  built once.
- BoolEncoder remembers where the trailing run of 0xFF bytes starts, so a carry
  no longer rescans the output (the cost was quadratic over long runs).
- Vp9Encoder::new clamps width and height to 65536; larger sizes used to wrap
  in the 16-bit frame size fields and write a corrupt stream.

The output is byte-identical for all sizes up to 65536.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Resolve conflicts for the egui 0.37 migration: Id::unique ids, kept the
accessibility names and roles; stop giving unnamed check boxes, twirls and
toggles a role so the kittest accessibility check passes; refresh the audit
baseline.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
With Show Font Names in English off (the default), the Character and
Properties font menus draw each family's own-language name in the
interface font. On Windows, Microsoft YaHei's Simplified Chinese family
name is 微软雅黑, and the Han face of an English interface can lack 软.
The app drew a missing-glyph box in its place, and the font search
tests in the effectcraft-ui-egui it binary panicked with "No glyph for
"软"".

A row now draws its own-language name when the interface fonts have a
glyph for each character, and its English family name otherwise. The
row's hover text follows the same rule. Search still matches both names.
upgrade_shape_contents now visits nested groups with a work list, so a
project with very deeply nested shape groups does not run out of stack
while Wiggle Paths gains Spatial Phase. The new test nests a Wiggle
Paths 100,000 groups deep and upgrades it on a 2 MiB stack.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Found while testing crates/model/src/gltf.rs:83-96 (uri_decode)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/color/src/lib.rs:100 (rgb_to_hsl)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
# Conflicts:
#	Cargo.toml
#	crates/ui-egui/src/lib.rs
# Conflicts:
#	Cargo.toml
Found while testing crates/time/src/lib.rs:390 (format_time, TimeDisplay::Seconds)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
File ▸ Scripts entries take keys in Edit ▸ Keyboard Shortcuts, so G8 says so and keeps the Startup and Shutdown folders and .jsxbin as missing.
An Environment light had no visible effect, and its Light Settings dialog had
no Casts Shadows option — both halves of #2436.

No effect had two causes:

- Classic 3D (the default for new comps) skips Environment lights entirely,
  so creating one did nothing. Adding one (or changing a light's type to it)
  now switches the comp to Advanced 3D in the same undo step, with a toast,
  exactly as model and primitive layers already do; switching back to
  Classic 3D with an Environment light in the comp explains itself too.
- In Advanced 3D, a light with no Source and no Environment Layer produced no
  environment at all. It now falls back to a flat default environment of the
  light's colour × intensity, as After Effects does, so the light always
  lights the scene per its parameters.

Cast Shadows was missing because the dialog and the light's property groups
shared one gate (Falloff and Shadows together) that excluded Environment.
The two are now split: every light but Ambient gets Casts Shadows, Shadow
Darkness and Shadow Diffusion (stored, edited and carried over on type
changes), while Falloff stays on Parallel/Spot/Point only — an environment
light shines from every direction at once and has no use for a falloff.

Tests: the renderer lights a card from a sourceless Environment light (colour
and intensity drive it, zero intensity lights nothing), the engine switches
Classic comps to Advanced 3D on creation and type change (one undo step,
toast, and the Classic 3D switch-back toast), Environment lights take and
carry shadow options but no Falloff, and the dialog ticks Casts Shadows on
a new Environment light.
After Effects lists Targa among its still-image output formats; EffectCraft
imported Targa (#745) but could not render to it.

- OutputFormat::TargaSequence ("Targa Sequence", `.tga`, numbered like the
  other sequences; `tga` / `targa` as a format name or output extension).
- Written by the `image` crate's Targa encoder (its `tga` feature, already
  enabled in effectcraft-media; no new crates, Cargo.lock unchanged), with
  run-length compression off: an 18-byte header, top-left origin, 24-bit BGR
  for RGB, 32-bit BGRA with 8 alpha bits in the image descriptor for
  RGB + Alpha (straight or premultiplied), 8-bit grey for Alpha.
- The render queue's size estimate counts the uncompressed bytes.
- CLI usage, the renderQueue command specs, README, architecture.md and
  file-format-parity.md name the new format.

Tests: each Targa frame holds exactly the pixels of the PNG sequence of the
same job, with the expected header, numbering and size; the frames read back
through the Targa importer as the same picture (straight, and premultiplied
when interpreted so); the image-sequence round trip, the format parser and
the renderQueue commands cover it too.
Review (CodeRabbit on #1875): with the paragraph direction unset, a right-to-left
paragraph resolved by BidiInfo was still aligned as left-to-right justified point
text, so it stayed left instead of flush right. eff_align now takes the resolved
direction. The box-text first baseline used the font ascent whenever any run of
the text was vertical; it now looks only at the runs of the first line.
Two regression tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict: docs/ui-parity.md (Estimate). Kept main's 60-110 h total
without the multi-monitor line and this branch's accessibility figure
(12-25 h, first slice done).

Adapted to main's shared docking layout (#2432):
- tests/ui_accessibility_audit.rs: Preview now gets a short frame at the
  top of the right column and its controls below it stay in the tree,
  counted against the panel underneath. The harness closes Preview, and
  the per-panel pass closes it again after its own scenario. The
  not-yet-fixed BASELINE numbers are re-measured in the new layout
  (panels are taller, so more controls are visible); the fixed panels
  stay at zero.
- tests/ui_effects_presets_keyboard.rs: the taller Effects & Presets list
  now draws the starred Gaussian Blur row in Blur & Sharpen, so the
  closed-Favorites check counts Gaussian Blur tree items (at most one)
  instead of requiring no "Remove from favorites" star at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Casts Shadows option Environment lights got is only half a fix if
nothing shades for it: an image lights the scene from every direction,
and what occludes that light is the image itself — its dominant bright
region, the sun in a sky map.

- dominant_direction finds it: the radiance-weighted mean texel
  direction (solid-angle weighted, rotation applied). A uniform image
  cancels to zero — a flat default environment shines from everywhere
  at once and casts no shadow, which a uniform environment test pins.
- The shadow is one orthographic map over the scene from that
  direction, after the lights' maps, like a parallel light's: PCF
  widened by Shadow Diffusion, darkened by Shadow Darkness, cleared
  when nothing casts, the image is directionless, or the texel budget
  drops it.
- It darkens the environment's own diffuse and specular light on
  Accepts Shadows surfaces and counts in Cast Shadows Only. CPU and
  GPU (WGSL) do the same; Globals carries the environment's sun
  direction and shadow settings (40 → 44 floats, preflight updated).

A bright-disc environment turned to shine from behind the camera now
shadows a sphere onto the floor behind it, Cast Shadows off lights it
evenly again, and the GPU scene's sky gradient takes a shadow-casting
Environment light. Also: the renderer-switch toasts read "...renderer,
which draws 3D models", the dialog's Casts Shadows checkbox gets an
auto id (dialog.light.casts) the tests click, and the docs' Environment
notes cover the shadow.
Three small fixes from the review of the shadow commit:

- light_settings switched a Classic 3D comp to Advanced 3D whenever the
  edited light was an Environment light (the resulting kind), so editing
  the intensity of one in a comp the user had switched back to Classic 3D
  dragged the renderer back. The switch now happens only on the
  non-Environment to Environment transition (the sources before and
  after), matching the docs; ordinary edits leave the renderer to the
  user.
- dominant_direction's thresholds let NaN through (every NaN comparison
  is false): a NaN or infinite texel produced a NaN sun direction and
  NaN shading with it. A non-finite weight or length now returns no
  direction — garbage casts no shadow.
- add_environment walked the full-resolution image for the dominant
  direction even when the light casts no shadows, where nothing reads
  it. Only a shadow-casting light pays for the pass now.

Tests: editing an existing Environment light in a Classic 3D comp keeps
the renderer and stays toast-silent; a NaN and an infinite environment
image yield no sun direction.
Review (CodeRabbit on #1875): with the direction unset, Hebrew text resolved to
right-to-left took its start indent on the left. The paragraph direction is now
resolved once from BidiInfo before the margin and alignment helpers, so both use
it. Regression test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A panel with a BASELINE entry that is not drawn now fails the audit instead of recording a zero row.
A count below its BASELINE entry now fails until the entry is lowered, so the old allowance cannot hide
a later regression. The star test's walk to "Blur & Sharpen" has the same 40-step guard as the others.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflict in crates/text/src/layout.rs (paragraph direction at the top of
paragraph()), against the closure-to-method-path cleanup (4e9fa51f): kept this
branch's direction resolved once from BidiInfo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	crates/ui-egui/src/panels/graph.rs
#	crates/ui-egui/src/panels/misc.rs
#	crates/ui-egui/src/panels/project.rs
Review of #2158: a buffer-backed accessor is checked against its buffer, but
each primitive that references it reads it again and keeps its own copy, so a
3 MB file listing 50,000 references to one 100,000-vertex accessor asked for
about 80 GB. Every accessor read, with or without a bufferView, is now
charged to one budget for the whole file, as is what a primitive keeps
besides its reads: generated indices when `indices` is missing, the triangle
list (3(k-2) for a strip or fan of k), flat-normal vertices and computed
tangents. Going over it is the usual malformed-file error.

The cap is a fixed 2^28 components (about 1 GiB as f32/u32), not a multiple
of the buffer size: real assets share one vertex buffer across many
primitives, and a 10M-triangle scan reads about 90M components. Buffer-less
accessors keep their tighter 16M sub-budget.
The Media Browser opens on the home folder, so a runner with fewer files finds fewer controls and
the 'improved, lower the entry' check failed in CI. The entry stays an upper bound.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
End-to-end regression tests for the issue's report, through the real command
path (what the Light Settings dialog runs) and the real viewer: adding an
Environment light to a Classic 3D comp switches it to Advanced 3D and changes
the viewer's pixels — a red light at full intensity, the default white light,
and the same on the GPU compositor (skips without an adapter, like ui_gpu).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
After Effects' default environment map casts shadows out of the box: an
Environment light with Cast Shadows on shadows the scene with no Source
set. The flat default cancelled its dominant direction to zero, so the
option did nothing until a Source image arrived. Give the default a soft
studio gradient (bright above and toward the initial camera, dark below
and behind), level-normalised so the light's colour x intensity still
sets the overall brightness; its dominant region drives the existing
shadow pipeline, CPU and GPU alike.
# Conflicts:
#	Cargo.lock
The default studio environment's level-preserving mean of 1 acted like a
100% white Ambient light: with only the Environment light on, a white floor
rendered at full white (95% of the frame saturated) while its shadow core
went to pitch black — a fog over the scene, nothing like After Effects'
default. Three changes bring the look in line, CPU and GPU alike:

- The default environment is a dim studio now: a solid-angle-weighted mean
  of 0.3 (and a stronger key gradient), so a white floor reads a mid gray
  and the light's colour x intensity still scales the level.
- The shadow occludes only the image's directional (key) fraction of the
  light — |sum of dir x radiance| / sum of radiance, computed beside the
  dominant direction and carried in the scene — so the fill keeps shining
  inside the shadow: an area-light shadow, partial like After Effects'
  (a lone bright spot on black still shadows almost fully; a uniform image
  not at all).
- The environment shadow map's penumbra has a floor — eight output pixels,
  or four map texels where those are coarser — because the map covers the
  whole scene bounding sphere rather than the view, and its texels would
  otherwise stair-step the edge (all the more in a draft render's flat 512
  budget). Shadow Diffusion widens it further, as before.

Tests: the key fraction tracks directionality; the default keeps its dim
level, its direction and its key; the default and a bright key both shadow
the floor (the default partial, the key deep) with Cast Shadows off lit
everywhere; and the full look — a mid-gray lit floor (no fog), a partial
core, and a soft edge ramp in quality and draft renders alike.
Upstream moved crates/ui-egui/src/menus.rs into menus/*.rs; the dynamic-menu change that
draws script folders as submenus now lives in menus/bar.rs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WebConfig had only the empty list_dirs default, so a script stored as Scripts/Tools/Hello.jsx
could be run by name but was not listed in File > Scripts or file.scripts.list. Review feedback
from CodeRabbit on #2289.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PR #2491 moved the modules; the instance3d changes follow the new layout:
- tests_instance3d.rs lives in src/tests/instance3d.rs now (declared in src/tests.rs)
- solo_3d moved from render/src/lib.rs to render/src/pipeline.rs, pub(crate),
  and its FxHost::layer caller moved to render/src/fx_host.rs
- cmd/create.rs, effects/channel2.rs, gpu/fx_pixel2.rs and render/cache/dependencies.rs
  followed their moves automatically (rename detection)
A soloed model keeps its Solo switch with its video off, and any_solo
hides non-soloed layers: the copy was excluded and the comp rendered
blank. The copy now takes the Solo over; the source's video stays off.
Covered by instance3d_keeps_the_copy_visible_when_the_source_is_soloed.
supported() mapped only ja, uk, es, cs, ka, pt and zh, so Match System
on a ru, de or fr system (ru-SK, de-DE, fr-CA) fell back to English
although all three have menu catalogs. And entry() returned the shown
label before the catalog lookup for every language without its own
undo/redo wording, so choosing Deutsch, Русский or Français by hand
still left the menus in English (#1449).

supported() now maps ru, de and fr. entry() keeps the undo/redo wording
as an Option: a language without it keeps the English undo/redo label,
and its other entries go through the catalog. Russian gets its undo and
redo wording.
Two review points on the merge:

- env_pixels returned None both for an unconfigured light (no Source, no
  Environment Layer) and for a failed source (a deleted layer, missing
  footage, a failed decode), so a selected source that failed silently
  swapped in the default studio environment — relighting and reshadowing
  the scene behind the user's back. It now tells the three apart
  (EnvPixels::Absent/Image/Failed): only the unconfigured light uses the
  default; a failed source contributes nothing, exactly like a layer whose
  source fails renders nothing. The repair the user can act on stays where
  it already lives — missing footage is flagged on the item, with the
  relink commands to fix it.

- The docs claimed only a uniform image casts no shadow. A non-uniform one
  can be directionless too: opposing bright regions cancel, and anything
  under 1% directional is rejected. agents.md, architecture.md and the
  sun/dominant_direction doc comments now say an image without a
  sufficiently strong dominant direction.

Tests: an unreadable Environment Layer (auto-picked), an unreadable
selected Source and a deleted one all light nothing — pixel-equal to the
unlit render, no studio default, no default-environment shadow — while an
unconfigured light still lights; two equal bright spots at opposite
azimuths cancel to no sun and no key, and tilting the balance re-emerges
pointing at the brighter spot.
# Conflicts:
#	crates/ui-egui/src/i18n/ui.rs
# Conflicts:
#	crates/text/src/fonts.rs
#	crates/text/src/layout.rs
dominant_direction weighted texels by their raw channel sum before the
light's colour tinted the radiance, so equally bright red and blue regions
on opposite sides cancelled to no sun and no key — while a red Environment
light's tint removed the blue contribution and left the scene lit
directionally red, with no shadow for its Cast Shadows to show. The tint
now weights the direction sum (a white light weights [1; 3], a coloured
one its normalised colour), so the sun and key are derived from the
radiance the light actually emits; add_environment computes the tint once,
before the direction pass, and reuses it for the texel tinting. An
achromatic image is unaffected (its weights scale uniformly), so the
default studio and white-lit scenes keep their shadows exactly.

Tests: opposed red and blue texels cancel to a white light but a red tint
points the sun straight at the red one (a blue tint, at the blue one); end
to end, a red Environment light over an image with equally bright red and
blue regions on exactly antipodal positions casts the shadow a white light
over the same image does not — and the shadow falls opposite the red
region, the side the tinted radiance comes from.
# Conflicts:
#	crates/ui-egui/src/credits.rs
#	crates/ui-egui/src/panels/tracker.rs
render: upstream moved the tests_*.rs modules into crates/render/src/tests/; the branch's
tests_mask_feather.rs now lives at tests/mask_feather.rs and is registered in tests.rs.
path: rect() keeps the branch's explicit-outline version with an added NaN guard on the
roundness clamp bound (main's fix for the same panic).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Keeps the already-pushed branch history; the resolved tree is unchanged: render tests live in
crates/render/src/tests/ (mask_feather.rs registered in tests.rs).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Conflicts: crates/ui-egui/src/menus.rs (upstream split it into menus/{bar,commands,entries,frontend,invoke,shortcuts,tests}.rs) and panels/settings.rs (ComboBox is now craft_ui::ComboBox).

- menus.rs: took upstream's slim module; re-applied the branch's two hunks in the new locations: nudge() now also skips Effects & Presets and Render Queue (menus/invoke.rs), and Enter is left to those panels in handle_shortcuts (menus/shortcuts.rs).
- panels/settings.rs: kept the branch's accessibility lines (shown_name, value_text) on top of craft_ui::ComboBox.

The accessibility audit BASELINE still matches the measured findings.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
text/layout: upstream split paragraph() into Para/place_line helpers and moved the tests to
layout/tests.rs. The branch's changes are re-applied there: the resolved paragraph direction
is passed to margins_for/avail_for/align_for (RTL point text with Justify sits flush right),
first_ascent/plausible_first and ParaLine.first/style_first (box first baseline is the d
height, vertical text keeps the ascent), and the Justify All last-line cost in
optimal_breaks. The branch's seven new layout tests moved to layout/tests.rs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Conflicts came from upstream splitting crates/project/src/lib.rs and moving the
frames tests. Re-applied the branch's change in the new places:
- Layer::same_pixels_with now in crates/project/src/layer.rs
- Comp::same_pixels_with, same_settings(o, with_markers), pixel_form_with and the
  timing-marker helpers in crates/project/src/comp.rs (same_pixels_ignoring_markers
  stays removed)
- Project::reads_markers in crates/project/src/item.rs
- crates/ui-egui/src/frames.rs uses reads_markers/same_pixels_with; the branch's
  two marker tests replace the old ones in crates/ui-egui/src/frames/tests.rs

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Main moved the engine's topic tests to src/tests/ and commands/ to cmd/. The
engine tests go to tests/text_edit.rs, opening their edit with its editing()
helper; ui_text_edit.rs keeps both sides' tests, this branch's first.
Opening the editor keeps the property selection, so Cmd+D while typing duplicated an
effect, shape item or text animator selected before and left the edit open. An open edit
now comes first, as it does for Clear: Duplicate ends it and copies the edited layer. With
no edit open, selected effects and items keep their precedence, and the command's
description says so.
Duplicate passes over the property selection an open edit kept and copies the edited layer,
but left that selection in place on the original. With the copy selected, the next Duplicate
copied the original's effect, shape item or text animator, and Clear would have removed it.
Ending the edit through Duplicate now clears that property selection along with it.
The engine's text-edit tests repeated one layer-count closure in five places; they now use
the layer_count helper the 3D tests already had. The doc of forwardable_during_text_edit and
of the Type-tool test helper still spoke of Deselect All alone, though the editor now forwards
Duplicate too.
Opening the editor keeps the keyframe and mask-vertex selection as well as the property one,
and Duplicate cleared only the properties. With the copy selected, Clear then deleted a key or
vertex of the original instead of the copy. During an edit the copies now take the selection the
way a click on them does: layer.select's rule, moved into one helper both use. With no edit
open, Duplicate's selection is unchanged.
Upstream split lib.rs files into modules and moved engine commands/ to cmd/; this branch's change merges without textual conflicts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Upstream split lib.rs files into modules and moved engine commands/ to cmd/; this branch's change merges without textual conflicts.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
`renderQueue.add` and `render.preRender` without an `output` take the default
from where the project sits, which for a project that has never been saved is
the home folder — outside every `--automation-read-root` / `--automation-write-root`.
The item was queued anyway, and only `renderQueue.render` refused it, so an agent
learned about it one command too late and with a queue it had to clean up.

Both commands now look the project up first (`Lookup::DefaultOutput`) and are
refused on an unsaved project, naming `output` as the way through. A project
saved inside the roots keeps its old behaviour: the default goes beside its file
and what it writes is checked when the queue is rendered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`script.run` and the Scripts menu are refused while file access is confined,
but a ScriptUI panel's script was not: `window.scriptPanel` runs it to build
the panel, `window.restoreScriptPanels` runs every remembered one,
`scriptui.click` / `set` / `mouse` / `close` dispatch events to its handlers,
and `script.runTasks` runs its `app.scheduleTask` tasks. A script reads and
writes files and reaches the network, so none of that may start while confined.

`prefs.set` is refused for the `scripting.*` settings for the same reason: the
engine already stops a script from granting itself the access the user withheld
(`Scripting::same_permissions`), and a confined agent writing it to the settings
file would hand it to every script run afterwards, long after the session.
`prefs.get` and the read-only `scriptui.list` / `scriptui.get` are untouched.

This is the "Running code" part of the F-11 audit; the other parts of #2131
are separate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The CLI installed no logger, so GPU warnings (a declined frame, out of
memory, fallback to the CPU) were dropped and an export silently rendered
on the CPU. Install the engine logger with a `warn` default (RUST_LOG
still overrides). Split out of #1415.
`file.createProxy` builds a Render Queue item exactly as `renderQueue.add`
does, with `[compName]_proxy.[fileExtension]` as its output, so an unsaved
project sent it to the home folder too.

Its `path` / `output` had to join `WRITES` for this: the confinement only
knows an output was named through that table, and it was reading them as a
read before, so a proxy could be written into a read-only root (part of the
F-11 audit in #2131).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
In After Effects a layer is a property group: thisLayer("transform"),
thisComp.layer("Other")("effects")("Blur"), thisLayer(1). In
EffectCraft's expression runtime a Layer was a plain object, so those
failed with "TypeError: not a callable function", including the
references the pick whip writes with Compact English off
(thisComp.layer("Other")("effects")("ec.noise.fractal")(...)).

A Layer is now callable like a property group (built as __mkGroup
builds groups), looking up its own properties by match name, name or
index, and keeps its methods, name and index. The places that took a
layer for a plain object (the result reading as the layer's index,
thisComp.layer(layer, offset), a Layer Control's value handed through
the property proxy) accept it.

Test: layers_are_callable_property_groups (fails before).
A Layer is now a callable function with its own prototype, which costs
more to make than the plain object it was. __layer(c, l) makes each
layer once per evaluation (kept in __S, which __begin resets), so an
expression naming the same layer many times builds it once. That covers
thisComp.layer(...) in a loop, .parent, a Layer Control's value and
thisLayer.

The checks for a layer (the result, thisComp.layer(layer, offset), the
property proxy) read __isLayer only on functions. Numbers and other
primitives are no longer boxed to look the property up.

Timing (release, Windows, medians of 2000 evaluations, PR vs main):
- one evaluation building one layer: about 22 vs 16 us;
- thisLayer.transform.opacity: about 200 vs 170 us;
- 20000 thisComp.layer(1) lookups in one evaluation: 95 vs 138 ms.
EffectCraft writes its own templates as `media/<item id>/<file>` entries, and
extraction trusted both halves. `base_name` splits on `/` and `\`, but the item
id was taken with `split('/')` alone, and it is pasted in front of a base name
another item already used. So a second `media/..\..\..\Startup\x/clip.mp4`
entry became `<dir>/..\..\..\Startup\x-clip.mp4`, which on Windows is three
folders up, with the attacker choosing how the name ends.

The id is now used only when it is one, and the name goes through
`safe_file_name`: separators, drive colons and NUL become `_`, and `.` / `..`
become `footage`. Reached from `templates.create` with `path`, from File ▸ New
from Template, and from a template dropped in the user Templates folder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	crates/ui-egui/src/panels/effect_controls.rs
`Prefs::set` makes a JSON pointer of the key (`key.replace('.', "/")`), so
`scripting/allowScriptsWriteFiles` resolves as well as the dotted spelling, and
the page on its own (`{key: "scripting", value: {…}}`) replaces every setting on
it — `coerce` takes an object for an object, and the page deserializes with
`serde(default)`. A prefix check on `scripting.` let both through. The page is
now read from the key's first segment, split on either separator.

Found by CodeRabbit on the pull request; reproduced before fixing, with
`prefs.set {key: "scripting/allowScriptsWriteFiles", value: true}` answering
`{"scripting/allowScriptsWriteFiles": true}` under confinement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`render.preRender` takes an Output Module template where a path goes, and the
confinement skips the path check for one. That made `given` true, so the new
lookup was skipped — and the template's own output is relative, so it resolved
exactly like an output the agent never gave: `render.preRender {output:
"Lossless"}` on an unsaved project queued `~/Movies/Lossless.mov` and
`~/Movies/Lossless_RenderLog.txt`.

The lookup now asks whether a *file* was named, so a template name takes the
same route as no output at all.

Found by CodeRabbit on the pull request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	crates/project/src/lib.rs
#	crates/render/src/lib.rs
#	crates/ui-egui/src/menus.rs
# Conflicts:
#	crates/ui-egui/src/lib.rs
A green stroke over an earlier Alt (red) stroke now restores the area: where
strokes overlap, the one painted last sets the hard constraint, for the cut,
the colour samples and the cleanup seeds alike.
A Refine Edge stroke over an earlier Alt (erase) stroke now restores the band
there; the band carried from the previous frame stays under this frame's
strokes.
A trained model is no longer prompted with both labels at one spot: points
along a stroke that a later stroke of the other kind painted over are
dropped, and the propagated matte's points, box and mask take this frame's
strokes over it.
Segmentations and rendered frames are keyed by their strokes, which now
segment differently where strokes overlap; bumping both versions keeps old
entries from showing the old result. Frozen mattes are stored in the project
and are untouched.
Green, Alt (red) over part of the object, then green there again, as the
Layer panel sends them through roto.stroke: the render follows the last
stroke, and Undo / Redo step between the two results.
Dropping the points a later stroke of the other kind painted over could leave
a stroke with none, even though most of it was still painted: a small Alt tap
in the middle of a large foreground dot removed its only point, and the
selected model was skipped for the classical cut. Points are now kept by the
resolved ownership under them, and a stroke that loses every sampled point
but still owns pixels is prompted from the one deepest inside them. A stroke
painted over completely still prompts nothing.
`file.createProxy` and `renderQueue.add` read a single string (`str_p`), so
`{path: ["…/a.mov"]}` leaves them on their default while the confinement saw a
path it could check and let the queue go ahead: on an unsaved project that was
`~/Movies/A_proxy.mov` again. Only a plain string now counts as an output the
agent named.

Found by CodeRabbit on the pull request, which reported it fixed by 9cc23209;
it was not, and the case is in the test now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The fallback name was never checked against the names already taken, so an
entry called exactly what the fallback produces took the same file: `media/1/a:b.png`,
`media/2/a_b.png` and `media/3/2-a_b.png` extracted to five files for six
entries, the third overwriting the second, and both items' footage pointing at
it. Deduplication now runs on the name actually written and counts up until it
is free.

The collision predates making the names safe (two entries named `a_b.png` and
`2-a_b.png` collide on their own), but sanitising gives more ways in, so it
belongs with it.

Found by CodeRabbit on the pull request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	crates/effects/src/sim.rs
#	crates/render/src/lib.rs
Two names that differ only in case are one file on Windows and on macOS as they
come, and Win32 drops a trailing dot or space, so `clip.png` with `CLIP.png`,
and `a.png` with `a.png.`, still landed on each other: ten entries extracted to
nine files on APFS, one footage item holding the other's picture. Collisions are
now looked up by a key with the case folded and those characters off the end,
while the name written stays as the entry asked.

Found by CodeRabbit on the pull request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Turbulent Displace read its noise through value_noise, whose quintic fade also
eased the evolution axis. Near every whole revolution the pattern hardly moved:
Evolution 0 and 37 degrees (or 360 and 400) differed by well under a hundredth of
the frame, so different Evolution values looked identical, and the motion sped up
and slowed down within each revolution (the #1816 symptom).

Add value_noise_linear_z (x and y keep the fade, z is linear) on the CPU and in the
WGSL kernels and use it for Turbulent Displace on both paths. value_noise is
unchanged, so the other effects and the pinned simulation goldens are unaffected.

Tests: CPU test that non-equivalent Evolution values (fractional, past 360, past
720) all differ and that Cycle Evolution still matches 0/360; unit test of the
linear z axis; GPU-vs-CPU cases at small and large Evolution.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYXhNN7W7nB6sKjwwU7CDM
Exports converted the float frame to 8 bits by plain rounding, so a smooth
gradient rendered at 16 or 32 bpc came out as flat bands, the same as at
8 bpc: raising the depth changed nothing visible. After Effects dithers
colours converted to 8 bpc for render and export; do the same.

- raster: dither_u8 and Image::to_rgba8_dithered / to_rgba8_over_dithered
  round each channel up or down against a hash of the pixel position with
  a fixed seed (deterministic, no clock or global RNG). A value already on
  an 8-bit code, as everything rendered at 8 bpc is, is unchanged.
- export: Cx::pixels dithers the colour for every 8-bit output (stills,
  H.264, ProRes, HEVC, AV1, VP9); GIF keeps plain rounding (palette).
- export: the Render Settings' Color Depth now overrides the project's
  depth for the whole render instead of only quantising the finished
  frame, so a 16 bpc render of an 8 bpc project keeps its precision.

Tests: crates/export/tests/it/banding.rs (ramp export at 8/16 bpc and with
Color Depth overrides), raster unit tests for dither_u8.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYXhNN7W7nB6sKjwwU7CDM
The ROI crop clamped every converted edge to +/-20_000 px, which changed
valid crops: a supported 30_000 x 100 frame at scale 1 with ROI
[10000, 0, 20000, 100] produced 10_000 x 100 instead of 20_000 x 100.
Valid edges reach about 120_000 px (30_000 px at render scale 4.0).

Clamp converted edges to +/-1_000_000 instead, subtract from the frame size
in i64 and narrow to i32 with saturation, and widen the arithmetic in
OutputModule::frame_size to match. Hostile quads (huge, inf, NaN) still
degrade to a valid 1 px frame without overflow.

Regression test next to hostile_roi_cannot_overflow_crop_math covers the
30_000 x 100 case, a scale 4.0 wide case and hostile inputs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYXhNN7W7nB6sKjwwU7CDM
# Conflicts:
#	crates/effects/src/sim.rs
`backpressure` stopped writing at the first WouldBlock. On macOS the
accepted stream does not keep the listener's 4 KiB receive buffer (it
gets about 114 KiB), so that WouldBlock only meant the 4 KiB send buffer
was full; it drained into the peer, and the 64 KiB write in
a_write_to_a_peer_that_stops_reading_fails_after_the_timeout and
a_blocked_socket_doesnt_hold_up_the_others_and_close_interrupts_it then
succeeded instead of blocking. Both tests failed on every run.

The helper now stops only after writes have stayed blocked for 200 ms,
still within its 16 MiB and 5 s limits. The socket code is unchanged.
An 8 bpc render has no finer values to dither, so its exports stay byte for byte what
they were (the pinned H.264 and ProRes files) and keep their size; 16 and 32 bpc renders
are still dithered on the way to 8 bits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYXhNN7W7nB6sKjwwU7CDM
Found while testing crates/lottie/src/import.rs:48-51 (hex_color)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
# Conflicts:
#	crates/effects/src/sim.rs
The merge of main put craft_quit::on_close_request inside
EffectcraftApp::tick, so the control channel, autosave, screenshots and the
desktop host's file dialogs still ran after it in the same logic callback.
The tick keeps the unsaved-changes check; each desktop host's logic now calls
panels::unsaved::quit_on_close last, after the device-loss close as well.
Found while testing crates/svg/src/lib.rs:510-520 (parse_css)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Found while testing crates/engine/src/cmd/align_data.rs:75 (parse_stamp)

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Resizing a rectangle or ellipse while keeping its proportions (a circle
that stays a circle, a 16:9 card that stays 16:9) needed W and H typed
or keyed separately. The Timeline's Size row of a shape's Rectangle
Path and Ellipse Path now has the chain link that Scale and Mask
Feather have. It starts off; turned on, typing or scrubbing W or H
keeps the ratio (Alt still edits one value).

Scale and Mask Feather keep their linked-by-default state in `unlinked`;
Size, which starts unlinked, records a turned-on link in a new `linked`
set, so neither default changes the other's saved state.
Enforce Rust coding standards via Clippy
# Conflicts:
#	crates/ui-egui/src/panels/timeline.rs
Review follow-up: the shape Size test linked only the rectangle, so a
change that handled rect and not ellipse would still pass. Link the
ellipse too and check that its height follows its width.
effect.cryptomatteAt rendered the effect's input on every call to learn its pixel grid (up
to ~1 s per hover on a 3D precomp). The aux channels and that grid are now kept in a
CryptoProbe on the Session, keyed by the project revision, layer, effect, frame, scale and
render prefs (nested switches, draft shadows). On footage with nothing animated before the
Cryptomatte effect the frame is the source frame, so a still or a held frame keeps one
probe for every comp time. The effect's own Selection doesn't count: the probe never reads
it, so an animated Selection keeps the probe too.

Session::crypto_probe_ready says whether a query answers from the kept probe and changes
nothing. Session::crypto_probe_job builds it on another thread for the viewer; asking for a
job is starting it. Each started job has a generation: it stores its result only while it
is the newest, a newer job or an edit cancels it, and a panic in the build becomes the
probe's error. A cancelled build checks its generation between its stages (reading the IDs,
rendering the effect's input), so it stops early and lets go of what it read. `probeOnly`
makes cryptomatteAt / pickCryptomatte answer {pending} instead of building on the caller.

A pick re-keys the probe (it only edits the Selection), and undo, redo and history jumps
over picks keep it too (each pick is labelled "Pick Cryptomatte"). Any other edit, footage
changes, Open / New and Session::drop_crypto_probe (the eyedropper disarmed) let it go. The
slot's type is crate-private (the Session field stays public, doc(hidden), so other crates
can still build a Session); crypto_probe_stats and crypto_probe_jobs (jobs run, cancelled
ones included) are doc(hidden), for tests.
Accelerator::comp_frame_yuv420 (default None) renders a top-level comp frame and
converts it over the background to the H.264 export's 8-bit BT.709 limited-range
4:2:0, every byte within one level of the CPU conversion (the GPU kernel follows
it operation for operation, including NaN and half-away-from-zero ties, but a
driver may contract its arithmetic). A 4K frame then reads back 12 MB of planes
instead of 133 MB of RGBA f32.

The H.264 export asks the accelerator for planes when a frame needs nothing after
the render (progressive, project depth, no crop/resize, rendered at the output
size) and otherwise renders and converts on the CPU as before. Deferred
readbacks (a browser worker) keep the pass-based path. Sizes the kernel can't
take (width not a multiple of 8, odd height) are declined before any GPU work.

Like Cx::frames, h264_frames caps GPU frames in flight only while the
accelerator is up (map_frames_while with the same liveness check): once the GPU
stops for good, the remaining frames render on the CPU side by side instead of
one at a time.

rgba_to_yuv420 moves to effectcraft_raster::yuv so the export and the GPU tests
share one reference. The GPU tests skip without an adapter (tests::gpu()), as
the crate's others do.
- Default disk cache size 20 GB (was 100 GB); settings version 3 migrates the stored old
  default and keeps a size the user chose.
- The cache keeps max(2 GB, 5% of the disk) free: sysinfo::disk_space (df -Pk / PowerShell
  DriveInfo) is read in the background from memory_tick and caps the limit
  (DiskCache::set_disk_space); cache.diskStats reports limitBytes.
- Eviction goes down to 90% of the limit, so the LRU sort runs once per tenth of the limit
  written instead of on every write at the limit.
- Failed writes are counted (cache.diskStats failed) and the first is logged.

Also fixes #2063.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A disk reported as 0 bytes (an automount map) counts as unknown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
File > Scripts lists a bundled script even when an installed ScriptUI
panel has the same name, but running it by name opened the panel. A
script now runs before a panel of the same name, and it enters Recent
Script Files.
Conflict in crates/track/src/face.rs: upstream moved the feature-blob search
into FaceTracker::feature_blobs. Re-applied the branch there: the per-pixel
point-in-polygon loop is replaced by non_skin_inside (one crossing pass per
window row) and the roll rotation is hoisted with face_mapper.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Conflict in crates/track/src/camtrack/bundle.rs (imports): upstream replaced
the linalg glob import with an explicit list; kept that list and added the
branch's SolveError import (use super::{Distortion, SolveError}). Input
validation, size budget and Result-returning adjust/cost are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Conflict in crates/track/src/camtrack/bundle.rs: kept both the branch's
PtSys/preconditioner/iterative_schur block and upstream's new
add_focal_prior helper. Adapted to upstream's explicit linalg imports
(conjugate_gradient, and I3/log_rot/mt/norm in the tests) and moved the
explicit point elimination into reduce_points so adjust_impl stays under the
cognitive-complexity lint.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Conflicts resolved in crates/ui-egui/src/panels:
- effects_presets.rs: main split the options menu into contents_menu();
  the search field's text-input role and the hamburger's menu-button role
  now live in show() / contents_menu() on top of main's structure.
- render_queue.rs: main split the panel into helpers (current_render_strip,
  footer, row_menu, log_dropdown, output_to, delete_selected). The progress
  bar role, the row focus ring, combo-box roles with keyboard focus return,
  the output template text-input and file button roles and the row
  keyboard navigation are re-applied on those helpers. The keyboard block
  moved into list_keys(), and status/render-time/started cells into small
  helpers, to stay under the new cognitive-complexity and argument limits.
- menus/shortcuts.rs: the Enter-owning panel check moved into
  panel_owns_enter() to keep handle_shortcuts_impl under the new limit.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Resolve the conflict in crates/gpu/src/fx_stylize.rs: main moved ops::warp to
the ops::WarpOptions parameter struct; the branch's per-sample transform loop
(motion-blur samples, now passing the sample by reference) uses the new
signature with the branch's reference argument kept.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Resolve crates/av1enc/src/decide.rs: main replaced the long try_tx/code_tx
argument lists with the TransformBlock struct. try_tx and code_tx keep the
branch's scratch-buffer design (TxScratch taken out of the decider) and take
a TransformBlock; reconstruct now takes its three output buffers as a
ReconBuffers struct to satisfy the denied clippy::too_many_arguments.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Resolve crates/vp9enc/src/lf.rs: main moved sb_plane into FrameFilter::sb_plane
(mi grid and limits as struct fields, no allow(too_many_arguments)). The
branch's 8-lane group filtering (apply mask per lane, filter_group) is
re-applied inside that method; the now-unneeded too_many_arguments allow on
filter_group is dropped (7 arguments).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
occlusion computes its three channels as one vector instead of writing a
vector component through a loop index, which FXC cannot address and tried
to unroll every enclosing loop for (X3550/X3511). classic3d now also
builds like the effect kernels: if a compiler rejects it, 3D runs render
on the CPU instead of the whole compositor failing to start. The ignored
FXC diagnostic test now checks classic3d instead of the stale pointwise
failure.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Services::full_path now returns None when the working directory can't be
read, instead of the relative path. file.runScript still runs the script
but does not add it to the list, so a later working directory can't make
the entry open a different file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Finish reordering from the stored egui drag owner and primary-button
release even when wheel scrolling culls the originating row. Cancel
disabled, interrupted or stale gestures before they can arrange layers.

Cover scrolling in both directions, selected groups, Undo/Redo, later
drags and cancellation boundaries, including opening Settings mid-drag.
Make the existing no-op control cross the drag threshold.

Fixes #2492.
* Honor CARGO_BUILD_TARGET in FreeBSD packaging for aarch64 release builds

The shared release workflow cross-compiles FreeBSD aarch64 in the x86_64 VM and sets
CARGO_BUILD_TARGET. package.sh ignored it: it built for the host, read binaries from
target/release, and took the arch from uname -m (amd64 on that VM), so the aarch64 build
would have produced an x86_64 archive with x86_64 binaries.

When CARGO_BUILD_TARGET is set, pass --target, read binaries from target/<triple>/release,
name the archive from the target arch, and skip the --version smoke test (the binary cannot
run on the x86_64 host). Unset keeps the previous behavior.

* Reject unsupported CARGO_BUILD_TARGET and gate the smoke test on the host triple

Only x86_64-unknown-freebsd and aarch64-unknown-freebsd are accepted when CARGO_BUILD_TARGET
is set; anything else exits 2 instead of being labelled freebsd-x86_64.

The --version smoke test now runs when CARGO_BUILD_TARGET is unset or equals this host's
full FreeBSD triple, so a native explicit target is still checked.
# Conflicts:
#	Cargo.toml
#	crates/ui-egui/src/app/frame.rs
The dev profile keeps line tables only, which keeps builds fast and test binaries small; panics and backtraces still name file and line. Debugger sessions still need local variables and column breakpoints, so add 'dev-full': the same settings plus full debug info. The fast profile remains the default.
Resolve viewport-edge insertion through the full Timeline outline, skipping
moving layers even below the viewport. Cancel a gesture if any captured
layer disappears before its ID can be interpreted as a layer index.

Cover intermediate drops, selected groups, deletion of the owner or another
group member, Undo/Redo, and later independent drags.
Resolve the Interpret Footage form against main's split into
interpret_footage_form and project_settings_form, keeping the Cineon
Settings fields and the pointUnits readback. Restore the Arc, rayon
prelude and cineon_frame imports that main's explicit use lists dropped.
Every undoable edit keeps the previous project for undo, so the comp it touches
is shared and Arc::make_mut deep-copies it: every layer and every keyframe. An
edit therefore costs time in proportion to the comp's size, and a script that
sets many keyframes in a big comp goes quadratic (a 1,600-layer build with
~100k sampled keys took ~45 minutes).

Comp::layers is now a Layers collection backed by Vec<Arc<Layer>>: copying the
stack copies pointers, and only the layer an edit changes is copied. The API
mirrors Vec<Layer>; serialized form is unchanged (a list of layers).
Comp::layer_mut and the iter_mut().filter/find call sites use find_mut /
iter_mut_where so they copy only the layers they actually change.

- prop.addKey in a 400-layer comp: 1.36 ms -> 0.16 ms per key (flat in comp size)
- the 1,600-layer script build: ~45 min -> 47 s, frames pixel-identical
- regression tests: an edit leaves the other layers shared with undo; layers
  round-trip as a plain list

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VkpGcKfWyCudfSsQNiWAaG
Export: convert H.264 frames to YUV 4:2:0 on the GPU
Face tracker: find polygon crossings once per window row
Camera tracking: validate solver input and bound workspaces
av1enc: reuse scratch buffers in the transform path, the coefficient writer and the level search
vp9enc: loop filter in 8-lane groups
Shapes: Wiggle Paths has Spatial Phase, which slides its wiggle along the path
Skip whole CSS at-rule blocks in the SVG parser by brace depth
Fix Align Video to Data panic on multi-byte char in timestamp
Disk cache: 20 GB default and a free-space reserve, so it no longer fills the disk (#2551)
Glow, Radial Blur, Venetian Blinds, Radial Wipe, Fractal Noise and the Transform effect render as in After Effects
Cryptomatte pick: keep a hover probe, built off the UI thread (#473)
Scripts: Recent Script Files lists the last ten scripts and Cmd+Alt+Shift+D reruns the newest
Fix #341: on macOS, a quit goes through `terminate:`
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GPU: Classic 3D builds with FXC, so the GPU compositor starts on DirectX 12 (#613)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edit > Duplicate replaces the layer selection with the copies, but an
open in-canvas text edit stayed on the original layer: typing kept
editing a layer that was no longer selected. When the duplicate leaves
the edited layer deselected, end the edit the way #2096's Deselect All
does (text_edit::end_edit), in the command every caller — menu, shortcut,
MCP — dispatches through.

Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
Edit > Clear follows panel focus: with the Project panel focused it
redirects to project.delete, which removes the selection at once when
nothing uses it. With a text edit open, that deletes the composition
being typed into — the user is working in it — and undo restores the
item but not the active composition. During a text edit the redirect
stands down (Clear does nothing) and the Edit menu's Clear entry is
disabled, mirroring how Deselect All stands down in #2096.

Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
layer.transform's fit / fitWidth / fitHeight ops took the layer's size
from render::source_size, which returns (0, 0) for layers without a
source item — shapes and text — so the ops matched nothing and silently
did nothing, for the shortcut, the menu item and the command alike.

When the source size is empty and a fit op runs, the size falls back to
the layer's painted content bounds at the current time, and the anchor
goes to the bounds' centre: content bounds live in layer space, where a
shape is centred on the origin and text sits above its baseline, so
(w/2, h/2) would fill the frame but land off-centre. Reset and the
other ops keep the source size, so their defaults are unchanged.

The regression test asserts what the viewer shows — the painted bounds
mapped to comp space span [0, 0, 640, 360] after fit, and fitWidth
spans the width centred vertically — for a rect shape and a text layer.

Co-Authored-By: GLM-5.3-Flash <noreply@z.ai>
Asked for on the PR: #1347 allows `file.createProxy` a name that is not a path,
because the engine reads it as an Output Module template, and the `WRITES` entry
added here refused exactly that. Dropped, so the classification is #1347's alone.

Nothing is opened by it. A proxy named outside the roots is still refused —
`cannot read` now rather than `cannot write`, which is #1347's to settle — so the
test asserts the refusal and no longer its wording. A relative name is now
accepted and resolves beside the saved project, inside the write root, which the
test pins so this does not quietly come back.

The lookup this PR is for is untouched: `file.createProxy` stays in
`QUEUES_AN_OUTPUT`, so a proxy queued from a project with no file is still
refused rather than landing in the home folder.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A shape's painted bounds can be tiny enough (a 1e-305 px rectangle) that
the fit scale overflows to infinity. Non-finite bounds are skipped and a
non-finite scale returns an error before anything is written, instead of
storing infinity in the document.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Introduced by the commit before this one. Dropping the `WRITES` entry changed
that refusal's wording from `cannot write` to `cannot read`, so the assertion
moved to the part both share — `outside the automation roots` — but
`/elsewhere/proxy.mov` is not absolute on Windows, where it is refused for that
instead and the assertion fails. The gate runs ubuntu only, so it stayed green.

A sibling of the write root under the temp directory is absolute on every
platform, so the refusal is the one the test is about.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Resolves the docs/scripting-parity.md conflict by keeping the script shortcuts row and main's new Recent Script Files row.
The CJK render test skipped wherever no Japanese face was installed, which
is every CI runner, and on a Mac it drew glyphs with or without the fix, so
it caught nothing. Replace it with a fallback-order test: fixture faces for
`Hiragino Sans` and `Hiragino Sans GB` show that a zh-hans Han lookup tries
the Chinese family first, that ja tries the Japanese one, and that a layout
gets Hiragino Sans GB. Synthetic fonts in an isolated child process keep it
independent of the host's fonts; with the Simplified list reverted it fails
on `[7]` against `[8, 7]`.

Delete `TEST_CJK_LOCALE`: the locale-mutating tests moved to an isolated
child process, so nothing used the lock and the test-only static failed
clippy's `-D warnings` gate.

Trim the documentation edits to the single docs/gaps.md gap-11 row: revert
the header and changelog edits in ROADMAP.md, docs/roadmap.md,
docs/localization-parity.md and docs/target-app-parity.md, and the gaps.md
header and changelog row, which conflict with other PRs.

Tests: cargo clippy -p effectcraft-text -p effectcraft-render --all-targets
-- -D warnings, cargo clippy --workspace --all-targets -- -D warnings,
cargo test -p effectcraft-text (79 lib), cargo test -p effectcraft-render
(264 passed, 1 ignored perf test), cargo fmt --all -- --check.
# Conflicts:
#	crates/project/src/tests.rs
Polish menus (every fixed menu entry, 630 rows) and panels (every row of
the Chinese panel catalogs, 695), the undo/redo, Reveal and Assign Shortcut
labels, the macOS Services item, Settings ▸ General ▸ Language and
Match System for pl-* locales. Tests cover the locale mapping, the
settings registry and the macOS application menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DukHNvUY45rjRnvtz91x33
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DukHNvUY45rjRnvtz91x33
wiggle_transform looks up its Transform group itself and returns when
the group is missing, so it takes seven parameters and no longer needs
the clippy::too_many_arguments allow.
Per review: as in After Effects, the region of interest lies inside the comp, so each converted
ROI edge is clamped to [0, w] / [0, h] and the far edge never passes the near one. A hostile ROI
reaching far outside the frame can no longer pad it into a huge crop, the wide-frame case keeps
its full width, and the i64 widening is no longer needed (frame_size is back to main's code).

Tests: hostile quads give edges >= 0 and a frame between 1 x 1 and the frame size, and an ROI
covering more than the frame is the whole frame.
Resolve the export tests' module list (banding and h264_gpu_yuv both stay). #1631's
yuv420_frame checked Render Settings Color Depth itself; that depth is now the project's
depth for the whole render, so the GPU YUV path is taken only for 8 bpc renders. Above
8 bpc frames take the RGBA path, which dithers, so H.264 exports don't band either.
Test: renders_above_8_bpc_skip_the_planes.
Dither can round a colour byte up while alpha rounds down (colour = alpha = 100.25/255 gives
alpha 100, colour 101), breaking the premultiplied invariant. Clamp each dithered colour byte
to the written alpha byte.

Test: dithered_premultiplied_colour_never_exceeds_alpha.
The tests now sit with the crate's other test modules under crates/script/src/tests/ and are declared in tests.rs. docs/scripting-parity.md points at the new path.
The mixdown evaluated levels and effect parameters once per block, and export mixes WAV/AIFF
in one-second blocks, so a level fade became a staircase that changed once a second. Animated
Audio Levels are now evaluated on a 1 ms grid of comp samples and interpolated in dB between,
and animated per-sample effects (Stereo Mixer, Backwards) run in 1 ms pieces with their
parameters at each piece. The grid is aligned to comp sample indices, so the result still does
not depend on the block size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
effectcraft_project::cineon_log_to_lin does the log-to-linear conversion
for Interpret Footage > Cineon Settings and the Cineon Converter's Log to
Linear. Both raise the result to 1 / Gamma, so Gamma 1 gives linear light
and the same Gamma gives the same values in both. The converter's Linear
to Log, its Log to Log reference and the GPU shader use the same Gamma
scale, with a smallest Gamma of 0.01 / 1.7 (CINEON_MIN_GAMMA).

A test runs the converter at Gamma 1.7 (the default), 1 and 2.4 and
checks it against Cineon Settings at each code value.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
The parent-space code moved to cmd/layer/parent.rs (#2491); this branch's changes to it are
carried over unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
Checkbox call sites follow main (Id::unique, the split effect_controls and timeline modules);
the integration test moves into the shared tests/it binary.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
The Effect Controls viewer hook moved to effect_controls/viewer.rs and the frame loop to
app/frame.rs (#2491); the popup eyedropper is carried over as its own function there.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
The shape-tool double-click, Cmd+W handling and their tests move to the split header, menus/invoke
and engine test modules (#2491); the docking check in the Cmd+W test uses the craft_ui dock API.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
lib.rs and menus.rs were split (#2491): the dialog_state accessors, the undimmed canvas while a
dialog previews, and Escape-as-Cancel for the Gradient Editor and Preview move to app/window.rs,
app/frame.rs and menus/shortcuts.rs. Layer Styles' rollback keeps main's text-editor handling inside
the shared restore(). The dialog preview integration test joins the tests/it binary.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
Bumps the actions group with 1 update: [storytold/craft-common-actions/.github/workflows/windows-arm64.yml](https://github.com/storytold/craft-common-actions).


Updates `storytold/craft-common-actions/.github/workflows/windows-arm64.yml` from 8bb4e5ab95443171104ad88113fe1ef9b0e5ce3f to fa5ef18780cf5e7d5d3b8de068de56cffc83cac5
- [Commits](8bb4e5ab95...fa5ef18780)

---
updated-dependencies:
- dependency-name: storytold/craft-common-actions/.github/workflows/windows-arm64.yml
  dependency-version: fa5ef18780cf5e7d5d3b8de068de56cffc83cac5
  dependency-type: direct:production
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
- add_environment takes the evaluated light (&EnvLight) instead of nine
  loose parameters — strict clippy (#2496) denies too_many_arguments.
- The Environment light's shadow map moves into env_shadow_map so
  shadow_maps stays under the cognitive-complexity limit.
- tests/it/ui_env_light_repro.rs deleted: it duplicated ui_env_light_gpu
  and the engine tests, and polled with sleeps of up to 12 s.

Signed-off-by: Minher0 <minhero@outlook.fr>
Footage.markers is added to the split footage.rs (#2491); the host integration test joins the
shared tests/it binary.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
CC Composite stays After Effects-pure: its Layer parameter is gone, so
parameter indices match AE again (scripts and expressions that address
parameters by index). The model reference lives in its own effect,
ec.3d.instance3d (3D Channel, CPU, one layer parameter), which replaces
the layer's pixels with the referenced layer's render — alpha included.

A referenced model's solo render is now hashed into the owner's cache key
instead of declining the entry (Bflat, review of #2410): the layer, its
animated values and transform, the mesh item, the comp camera and lights,
draft 3D, the renderer setting and the in/out gate. Covered by a render
test: the copy is cached, served on the second render, and re-rendered
when the camera pans or the model leaves its in/out range.
output_guard joins the split engine lib.rs; the overwrite tests move to tests/render_overwrite.rs
(#2491) and the regression workflow's test filter follows.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
main follows the browser's language preferences itself now (set_system_locales /
first_supported), so this branch keeps only what it adds: the Russian, French and German catalogs
selectable by Match System and a locale, and their Undo and Redo menu labels.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
Conflict in crates/ui-egui/src/panels/viewer_text.rs: main moved the
editor's key handling into key_event(). Took main's version and moved
this branch's Duplicate forwarding arm into key_event(), using its ctx
parameter.
Conflicts were main's new code next to this branch's additions; both kept:
- crates/effects/src/lib.rs: main's Buf::pad_sides stays as main wrote it
  (Transform's transform_grow bounds that padding itself); Buf::pad keeps
  this branch's effective-pad clamp.
- crates/raster/src/lib.rs: main's quantize_px/round_half_up and this
  branch's MAX_PADDED_SIDE (still equal to limits::MAX_SIZE on main).
Add bezier(t, tMin, tMax, v1, v2, [x1, y1, x2, y2]) and the short bezier(t, v1, v2, curve) form: a CSS cubic-bezier easing curve solved with Newton-Raphson and a bisection fallback.

Scalars and arrays work like linear/ease; x1/x2 clamp to 0..1, y1/y2 may overshoot, t clamps to tMin..tMax, and a curve that is not four finite numbers falls back to linear instead of throwing.

Tests cover known curves (ease is [1/3, 0, 2/3, 1]), clamps, arrays, the short form, invalid curves and overshoot, plus two fuzz seeds.

Closes #1884.
Add the helper to the expression editor's completions (syntax highlighting and auto-complete) and to the Expression Language menu's Interpolation group, so accepting it inserts the call parentheses like linear/ease.
- accept only four-element arrays for bezier(): extra values make the curve
  malformed, so it falls back to linear like the other invalid forms (#1884);
- stop Newton on the estimated y error (f * dy/dx) instead of a residual in x
  alone: near the endpoints dy/dx is in the thousands, so a 1e-9 residual in x
  could still move the returned value;
- tests: a five-element curve is linear, and three steep curves match a
  bisection reference within 1e-6.
A missing or unusable curve no longer consumes the last value. The long form reads argument 5 and the short form argument 3, with the other arities falling back to linear interpolation. Adds the missing-curve and four-element-array cases reported in review.
A startup key (last, empty or demo) chooses the project the page opens on load. empty restores the last visit's project and otherwise starts blank, never with the demo; demo always opens the demo project; last, or no key, is the current behaviour. ?empty and ?demo still win. The config file is fetched once per page load and the service worker never caches it.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
The Preview panel draws Shortcut, Range, Play From, Frame Rate, Resolution, Overlays, Layer
Controls, Loop, Cache Before Playback, Pre-roll, Post-roll, Full Screen and the caching hint from a
local closure, a check table and a roll tuple, so the call-shape scan cannot see them and no catalog
had rows for most of them. They ask the catalog now, with Simplified and Traditional rows.

Verified on Linux: fmt, cargo test -p effectcraft-ui-egui, clippy --all-targets -D warnings.
Two English surfaces the translation work so far could not reach.

The dock's tab titles: `PanelKind::title()` is drawn by `dock_ui.rs`'s `tab_titles`, which listed only
the tabs that name a comp, a layer or a script and let every other one fall back to `title()`'s English
- so every panel tab (`Timeline`, `Preview`, `Properties`, `Effects & Presets`, ...) stayed English. The
Window menu already carries a (`window.panel`, title) row in every catalog, so the dock reads that pair
and falls back to the panel catalog for `Markers` (the one panel only it names); `History` and
`ScriptUI Panel` have no row anywhere and keep their English names. `app.auto`'s labels and the dock's
serialized state keep the English title, which the control channel needs.

The scan's blind shapes: `converted_panels_have_no_unwrapped_literals` only sees a literal that follows
a call token, so a label passed to a helper, the third argument of `Painter::text`, a `(key, label, ...)`
table, a `let` binding and a `match` arm were all invisible. The two that can be checked without
guessing are added (`helper_and_painter_labels_are_translated`), with the same single-character
exemption the call scan makes, and the 25 sites they find now ask the catalog (`Apply`, `Analyze`,
`Cancel`, `Create Camera`, `Add Point`, `Remove Point`, `Clear`, `Render`, `Stop`, `Save Current as
Template...`, `New Composition From Footage`, the three search fields, `(no composition)`,
`per second`, `Waveform`, `steps`), with 15 rows added to the Simplified and Traditional catalogs in
the conventions those tables already use (Render 算圖, Apply 套用, Add Point 添加點).

The `uic!` labels and the Keyboard Shortcuts dialog are a different shape and are still to decide
(#1451).

Verified on Linux: cargo fmt --all --check, cargo test -p effectcraft-ui-egui (every suite passes),
cargo clippy -p effectcraft-ui-egui --all-targets -- -D warnings clean. Reverting the dock's catalog
lookup fails `the_dock_draws_every_panel_it_can_in_the_interface_language`; unwrapping a helper or a
painter label fails `helper_and_painter_labels_are_translated` ("panels/project.rs:434: the interface
text \"Search project\" that widgets::search_field draws is not in the catalog").

Also answers #1451's question about languages whose rows have no Simplified row to be a subset
of: six labels the Composition Settings dialog draws from a `(label, value)` table, a tab array and
a `match` arm (`Square Pixels`, `Basic`, `Advanced`, `3D Renderer`, `Black`, `Composition Settings`,
plus `White` from the same `color_name`) had no row anywhere, so every interface drew them in
English; they have Simplified and Traditional rows now, in the existing menu wording. The seventh,
`Lock Aspect Ratio`, is only an automation name - the drawn label is `Lock Aspect Ratio to {}`,
which already had rows, and `comp_settings.rs` built it with `format!` where `dialogs.rs` already
used `tr_args`, so that one is wired now too.

Review fixes (CodeRabbit): the scan now runs over the whole file with line comments blanked, so a
`.text(` call split across lines is still one argument list - which found five more labels the
per-line loop could not see (`No effects. Drag one from Effects & Presets.`, the colour-ring hint in
`fx_editors.rs`, the Learn blurb, the Script Console hint, the Templates blurb, plus
`Building waveform…` from the review), all wired with rows. And the New Composition title is back to
the English source: `modal_fit` keys the modal id and the drag offset on the title (its own comment
says the id stays on the untranslated source) and translates the heading itself, so `tr(..)` there
would have moved the dialog when the interface language changed.

And the argument list is bounded by the call's closing parenthesis: `args_of` used to flush a
trailing argument at end-of-string, so a last argument without a trailing comma carried the `)` and
the source after it, which made the check skip it as "not a literal". Reverting that one line is a
two-line test (a literal last argument in `header.rs` is caught now and was silent before).

The label scan also understands the two string forms a label can be written in: a plain string ends
at the first unescaped quote, a raw string (`r"…"`, `r#"…"#`) at a quote followed by as many `#` as
opened it, and `literal_text` decodes the argument before the exemptions run - so an escaped or raw
label is checked like any other instead of being skipped.

Two more scan fixes from review: `without_comments` tracks the escape state rather than the previous
character, so a string ending in a backslash (`"a\\"`) closes where it should; and the scanned range
ends at the test *module* - the first `#[cfg(test)]` followed by a `mod` - rather than at the first
`#[cfg(test)]`, because `dialogs.rs` keeps a test-only helper above its test module and production
code between them was not scanned.

And call tokens are looked for in a code-only mask: comments blanked, the contents of plain and raw
strings blanked too, with the quotes kept and every byte offset preserved (a blanked character
becomes as many spaces as its UTF-8 length, and a debug assertion pins the mask length). The argument
list is read from the original source at that offset, so a sample string that quotes a call cannot be
mistaken for one. Building the mask from chars instead of bytes shifted every offset after the first
non-ASCII character, which the debug assertion now catches.

The dock's display path has its own test now: `tab_titles` is called with `general.language` set to
Chinese and the labels a window shows are asserted (`Timeline` 时间轴 ...), plus `History` keeping its
English tab and the English interface keeping all of them. Reverting the dock to `PanelKind::title()`
fails it with "Timeline is not drawn in Chinese", which the catalog-only test could not see.
The toolbar's tool labels come from Tool::label() through header.rs; they were the last English
surface on the chrome. 31 rows, Simplified and Traditional, and the header asks the catalog.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
The referenced model's solo render goes through the comp camera: its
depth of field (focus, aperture, iris, highlights) blurs it, none of
which is in the projection matrix — the evaluated Dof is hashed in now
(CodeRabbit, review of #2410).

A model with an animation clip selected is posed at its layer time
(Animation Speed and Looping are static and hashed): the layer time is
folded into the key, so every time is its own entry. A model without a
clip keeps one key across times, as before.

An Environment light's image lights Advanced 3D models, and its source
can be any layer's render (or any environment layer) — too open-ended
to hash: while one is on, the read is not cached. Each is covered by a
render test.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
Cancel and Preview-off merge the checkpoint, the preview's result and the project now (new
preview_merge module), so an edit made while the dialog was open stays. OK first brings the
preview up to the draft it finds, and Composition Settings OK applies the Resolution after a kept
preview. The Gradient Editor's OK syncs and sets the draft when it differs from the last preview.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
- The comp-sized shape is made by shape.newShape (new newLayer option) so it takes the Tools bar's
  Fill and Stroke, and a polygon or star fits the shorter comp side.
- Cmd+W on the Render Queue moves the focus to a panel that is still open.
- The Graph Editor's key menu is the Timeline's (one shared function): all its commands, labels in
  the interface language, edits disabled on a locked layer.
- keys.delete and keys.set leave a locked layer's keys alone.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCR4xUtUfBJwkhwaFuD
JAPANESE_CATEGORIES had 20 of the 21 the Simplified table carries, so the Generate group in the
Effect menu drew its English name. AE calls it 描画.

Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
An explicit demo startup no longer saves the session, so the demo cannot replace the last visit's project. The config request is aborted after 5 s, stops reading once the body passes the size limit, and is skipped for ?empty and ?demo. The packaged hosting notes link to the published docs.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCR4xUtUfBJwkhwaFuD
cineon_log_to_lin takes the exponent scale from its caller. Interpret
Footage > Cineon Settings passes 1 (Gamma 1 gives linear light) and the
Cineon Converter passes 1.7, as before, so the converter's output and
its default Gamma 1.7 are unchanged. The converter's Linear to Log, Log
to Log reference and GPU shader return to their earlier code.

A test checks the converter at its default Gamma and at 2.4, for all
three conversion types, against the earlier formula.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
Carry property, Font and Media Replacement overrides into the collapsed nested EvalCtx so nested layers render beyond the precomp frame instead of falling back to a clipped, flattened image. Key the layer cache on instance overrides to keep distinct collapsed instances isolated.
LayerInfo, sourceRectAtTime and Layer.source now resolve the effective footage item/size through the collapsed instance EvalCtx, matching the rendered pixels. Adds a regression test with different original and replacement footage sizes.
Clicking away from a Type-tool layer without typing deleted it, so a click that was meant to place the
text point lost the layer. AE keeps it in the timeline as <空白のテキストレイヤー>, and the layer came from the
user's click rather than being a mistake to clean up.

edit.deselectAll already routes through text.endEdit, so one change covers both paths. The tests that
asserted the old cleanup now assert the layer survives.

Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDvxCS8xcR28VArjzn1x7U
Review of the first commit found three places that still asserted the old cleanup: the UI integration
test (which would have failed CI), docs/agents.md, and the comments on TextEdit::created, layer.newText
and the engine test itself.

Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
The strict-clippy split of inverse_dct (#2496) passes n at run time; inlining keeps
inverse_dct_n's compile-time sizes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It runs for every frame identity and disk-cache key; walk() formatted a path per node.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Generate row added here drew 描画, which Paint already used, so the Effect menu showed the same
category name twice. 描画 is Adobe's name for Generate, so Paint becomes ペイント, and a test now
rejects two categories sharing a Japanese name.

Found by the review bot on this PR.

Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
A few kilobytes of internal entities expanded to millions of nodes (each top-level
reference stays under roxmltree's per-reference cap), so a 1 MB file exhausted memory.
SVG keeps DTDs, which Illustrator's exports use for entities, but caps the expanded
document at 4 million nodes. Final Cut Pro XML and FCPXML, which FilmCraft parses
without a node limit, are refused when they declare an entity; real exports never do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SVG, FCP XML: bound XML entity expansion (#2129)
Camera tracking: avoid per-view triangulation allocations
i18n: two labels that already had a catalog row were formatted in English
Fix HexTile centerline cell selection
Keep FrameRate::from_f64 nonzero for tiny positive fps
Bound SVG arc segment count for huge radii
Require hex digits in glTF uri_decode percent escapes
Reject '+' signs in Lottie text stroke colour hex pairs
macOS: declare the UI languages so the open and save panels are translated
Scripts: socket tests wait for the buffers to stay full on macOS
EffectCraft — start the window surface from eframe's default (frame latency 2 → 1)
Lottie: import a layer's time stretch below 1 % as 100 %, with a warning
vp9enc: forward transform accumulates all outputs together
i18n(ka): shorter Georgian for 14 labels that didn't fit
Lottie: size an imported precomp from its precomp layer's w/h when the asset has none
opusenc: eight partial sums in the 48 kHz decimator
Web: keep Duplicate from opening browser bookmarks
CLI: log warnings to stderr by default
fix(render): use pixel units for text animator tracking amount
Docking: reject raw panel opens through command aliases
i18n: Russian, German and French follow Match System and translate the menus (#1449)
vp9enc: fixed-size SAD kernels and a revisit cache in the motion search
opusenc: radix-2/3/4 FFT kernels for the MDCT
Camera tracking: skip unused SVD output work in null-vector queries
Camera tracking: keep bundle-adjustment Jacobians on the stack
Mask tracker: keep only the cropped previous frame
Share the Warp Stabilizer luma pyramid with trajectory tracking
Reuse the validated Warp Stabilizer plan on first render
Workspaces: wait for migrated window positions before saving
ScriptUI: fix script-drawn colours showing too bright
Lottie: import every Dash/Gap pair of a stroke, not only the first dash and the last gap
Menus: a pointer coming back into the window over another title switches menus (#1290)
Classic 3D Shadow Diffusion blurs instead of stamping copies (#2444)
engine: a template's footage entry can't name a file outside the folder (#2123)
MCP: list_commands and list_effects take compact: true (#2020)
Fixes: macOS quit crash, soft shadow jitter, Interpret Footage params, preset upgrade, composition proxies, font menu names
Use frozen Object Matte in sessions without the freezing model
av1enc: build the three half-sample reference planes in one pass
Keep rgb_to_hsl saturation finite for over-range colours
Return identity from Mat3::square_to_quad for a degenerate quad
Fix "-0.000" in seconds time display for tiny negative times
Shape Size gets the Constrain Proportions chain, off by default (#2489)
Draw a line to the end point of degenerate SVG arcs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
path.freeTransform ignored the drag's merge key, so each frame was its own undo step.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
When the ALSA development package is missing, `cargo xtask ci`'s audio
stage failed at link time with a bare linker error. The check now says
which package installs ALSA's contents on each supported distro, the
missing one is named in the error, and the stage's own tests pin both
the present-ALSA pass and the per-distro message.
The probe ran pkg-config straight from PATH while the build's alsa-sys
goes through the pkg-config crate, which reads PKG_CONFIG first: the
variable may name a working pkg-config that is not on PATH, and then
cargo xtask ci would report ALSA as missing over a build that finds
it. The probe now selects the same executable the same way; no test
stands over it because the probe needs a real pkg-config environment
(the hint is split out for exactly that machine-dependence).
The probe's spawn failure (a PKG_CONFIG that names nothing runnable, or
pkg-config absent from PATH) reported the ALSA-missing hint, sending
the reader off to install alsa-lib they may well have: a failed launch
now reports itself, naming the executable and pointing at
installing pkg-config or correcting PKG_CONFIG, while a probe that
runs and exits non-zero still gets the ALSA guidance.
CodeRabbit and FredApps both flagged that the preflight probed only the plain
PKG_CONFIG variable, while the pkg-config crate alsa-sys pins resolves the
target-scoped variants first and falls back pkg-config -> pkgconf; on a host
with only pkgconf the ci stage failed a build that would succeed. The probe
now goes through the same crate (0.3.34, already pinned via alsa-sys), so the
semantics match by construction, and a probe that ran but said no stays
distinct from one that could not run.

Also from review: audio_backend_hint is gated to Linux (bflatastic: dead code
on Windows/macOS under -D warnings), the fake-tool regression runs the real
xtask audio-backend task in a child process with a controlled environment
(pkgconf-only host, a probe that says no, one that cannot run, a HOST_PKG_CONFIG
override), the rerun-if-env lines stay out of the ci log, and CONTRIBUTING.md
asks for a usable alsa.pc and runtime library rather than development headers.
- Exported HEVC/AV1 nclx says BT.709 and limited range where the pipeline
  means it, matching what the players we tested assume; the round-trip
  tests pin the box values.
- The ffmpeg oracle helpers no longer read the flags column of
  `ffmpeg -decoders` for "is a decoder" — every row of that listing is a
  decoder, and the trailing D there is "supports direct rendering method
  1", which a build's hevc decoder may legitimately lack. Decoders are
  matched by the codec-name column alone.
- Fixtures a given ffmpeg build cannot actually encode (a listed encoder
  that fails at run time: hardware backends without their device, the
  noopenh264 stub) skip with a note instead of failing the run.
CodeRabbit's leftover nitpick: h264_mp4 hard-coded -preset medium -crf 18 for
whatever encoder the build ships, but libopenh264 (and the hardware backends)
reject both — so a build whose picked encoder is not libx264 failed the fixture
and silently skipped every check gated on it. libx264 keeps its constant-quality
pair; everything else gets -b:v. Pinned by a small test over the option sets.
# Conflicts:
#	ROADMAP.md
Groups, paths, fills, strokes and operators in a shape layer's Contents show the
Video eye in the A/V column (a group's Transform doesn't). Clicking it runs
prop.setGroupEnabled, which the shape renderer already honours; layer-style
eyes keep layer.style.toggle. Row building and search results share eye_of.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vp9enc: vectorised phase-plane filters and a predict fast path
opusenc: tabulate the a2nlsf cosine grid, halve its bisection, memoise pitch contour correlations
Camera tracking: factor narrow camera systems within their band
Tracker: Lucas-Kanade resolves its bilinear taps once per window
vp9enc: probability delta table, linear bool-encoder carry, clamp oversized frames
av1enc: fix the bitrate overshoot of the bitrate mode rate control
automation: a confined agent can't start a script's code (#2131 F-11, "Running code")
Automation: confine every command that reads or writes a file it is given (#1243)
effects: Turbulent Displace Evolution moves the pattern at a constant rate (#943)
i18n: the CJK fallback sits on the Latin baseline (#1309)
Auto-orient: keep the last heading when an expression-driven Position stops
model: cap glTF buffer-less accessors per file, check element offsets (#2139)
export: tag BT.709 limited-range nclx; the ffmpeg oracles pick decoders by name and skip unbuildable fixtures
GPU: contain frame-local validation failures (refs #1319)
av1enc: find the first frame's quantizer in fewer passes
opusenc: vectorisable candidate filter for the greedy PVQ pulse search
CLI: run --commands reads {"command", "params"} steps from a JSON or JSON Lines file, or standard input (#2251)
Point tracker: refine in a 3x3 neighbourhood when the motion was small
Expressions: Alt-pickwhip references effect parameters by index (refs #1072)
effects: speed up Match Grain's CPU analysis (~3x on 1080p/4K)
i18n(ja): the effect names, 297 of 307, as AE's Effect menu draws them
i18n: complete the Brazilian Portuguese (pt-br) panel catalog
expr: bound runtime parser and native recursion depth (#2124, #2125)
Export: Targa (.tga) sequence output, 24-bit RGB or 32-bit RGB + Alpha
Add a Nix flake (NixOS, GNOME and Hyprland)
docs: add user docs in docs/user
Main split the panels' value editors into one function per value kind.
The expression colour now comes from Tokens::for_value in each of them:
Effect Controls through PropCx::value_tokens, the Timeline's scalar,
vector and enum editors, and the Properties transform row.
Particles: preserve emitted particles when Birth Rate changes (#1373)
Preview: preserve opening audio while preparing video playback (#947)
Fix timeline layer drops after scrolling (#2492)
Roto Brush: apply correction strokes in drawing order (#1646)
Mask feather, Repeater opacity and Trim Paths on rectangles render as in After Effects
Clipboard: export keyframes as After Effects-compatible text
av1enc: specialise the inverse transform per size, size the forward tmp buffer
Marker names are drawn in a small outlined box, in theme colours
model: OBJ/MTL references from another machine find the file next to the model, and a missing one is logged
Export: cap CPU frames in flight with EFFECTCRAFT_CPU_INFLIGHT (#1615)
Markers: adding one during playback no longer stops the audio (#1083, #1085)
Scripts: subfolders of the Scripts and ScriptUI Panels folders become submenus
The empty Composition viewer's button only ran File > Import, so the picked
file landed in the Project panel and no composition was made. file.import
now takes newComp: when the import lands (at once or when its background job
applies), what it made becomes one composition via New Comp from Selection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Particles: CC Particle Systems II and CC Mr. Mercury keep emitted particles when Birth Rate changes (#1373)
Animation presets: anchor saved effect keyframes at the playhead
Reuse Warp Stabilizer and Camera Tracker analysis keys
#1510: Behaviors animation presets (22 original, expression-driven)
Timeline: every shape Contents item has an eye switch, as in After Effects (#959)
The fill holds every frame as float RGBA, its hole mask and the filled copy until it
finishes, so a long or large range ran out of memory partway through (22-33 % on the
reports). Planning the fill now compares that lower bound with the system's available
memory and refuses with what it needs and how to shorten the range.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Content-Aware Fill: refuse up front when the frames can't fit in memory (#1789)
Alt+] (Trim Layer Out Point to Current Time) used to set the Out edge exactly at the playhead; the layer interval is half-open, so the playhead's frame was already gone - nothing showed in the current frame. The edge now lands one frame after the playhead, as in After Effects, so its frame is the layer's last visible one. A layer still keeps at least one frame; Alt+[ (In point) is unchanged.
A double-click on a creation tool in the Tools bar makes what it draws, as in After Effects: a text layer from the Type tools; from the shape tools a shape that fills the comp - into the selected shape layer's Contents, as a mask on the selected layer (or when Tool Creates Mask is on), or as a new shape layer when neither applies. The shape geometry defaults now match AE: the Rounded Rectangle tool's Roundness is a fixed 20 instead of growing with the drawn size, and the Polygon tool draws five points.
Two corners give a sliver instead of a comp-filling mask when the layer is
rotated by an angle other than a multiple of 90 degrees (CodeRabbit on
#1654): map the comp box's four corners into layer space and use their
bounds.
Settings ▸ Project ▸ Project Files ▸ Save Compact Project Files (off by
default) saves .ecproj files as compact JSON: smaller, faster to save and
open. Off, they stay indented, one value per line, so they read and diff
cleanly in version control. XML copies are not affected; auto-saves were
already compact. The project.ecproj inside exported template and Motion
Graphics template packages is written compact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The viewer lock in a tab group's header drew the same closed padlock in
both states, told apart only by brightness. Unlocked now draws an open
padlock, locked the closed one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Conflicts:
- viewer.rs: the undecodable-footage notice moved into phase_scene on main; it now
  collects its layers in footage_problems, which keeps the proxy and collapsed-precomp
  rules of #1701.
- project.rs: main's row layout, with the problem marker going by item_problem.
- movie_bytes.rs: the H.264 pin is FilmCraft 09aaf9b's bytes plus main's 19-byte
  BT.709 colr box (104_322 + 19 = 104_341).
render/footage.rs imports ItemKind now that main names its imports.
The button row followed each tab's content, so it jumped when switching
between Basic, Advanced and 3D Renderer. The tabs not on show are laid out
unseen each frame and the dialog pads to the tallest, so Cancel and OK stay
put with any font, language or scale.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: pressing play on frames that still had to render left the playhead
standing still for seconds. The cache bar showed a gap of a few frames right
after the playhead while frames further ahead turned green, and playback
started only once the whole prefetch window was cached. The earlier #595
change (prefetch order, eviction) started the right frames first, but they
still finished last. Cache Frames When Idle left the same late-filling gaps.

Cause: prefetch frames render side by side on one rayon pool, and each
frame's own parallel loops run on that pool too. An idle thread steals a
piece of a frame that is rendering; when that piece waits on a nested
parallel loop, the thread picks up a queued frame job instead and renders
queued frames until the queue is empty. Only then does it return to the
piece it stole, so the frame that piece belongs to cannot finish while
frames keep being queued. The frames started first are the ones just ahead
of the playhead.

Fix: the frames of a playing preview and of Cache Frames When Idle render on
lanes: one single-thread pool per frame thread, one frame at a time each. A
lane's thread only ever works on its own frame, so no frame waits under
another. The shared pool keeps the viewer's frame, which still uses every
thread, and the few frames prefetched around it while paused. Queued frames
change hands when a preview starts or stops, or when the viewer shows one.
Idle caching takes every lane instead of half the pool's threads.

Tests: frames_started_first_are_done_first (a playing preview, idle
caching) and queued_frames_render_when_a_preview_starts_or_stops.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Composition and Layer viewers took every scroll as a zoom, so a trackpad
could not pan. A trackpad's two-finger scroll now pans; a pinch,
Ctrl/Cmd+scroll, a mouse wheel and Alt + a trackpad scroll zoom around the
pointer.

Where the platform names the device (egui's MouseWheelSource, macOS), that
decides. Elsewhere browsers and some high-resolution wheels report a mouse
wheel in points, as a trackpad does, so the unit alone doesn't decide. A
scroll in points pans only once it has shown it is a trackpad: a touch phase,
movement on both axes at once, or a pinch. That holds until a wheel scrolls
in lines. Until then the vertical part zooms and the horizontal part pans.

ui.scroll gains unit (point|line), source (wheel|trackpad) and pinch so
agents can drive both gestures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
# Conflicts:
#	crates/av1enc/src/decide.rs
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The Timeline's comp tabs had a × on the shown comp only, so closing another
comp meant showing it first. A comp tab the Timeline does not show now shows
its × while the pointer is over it, and clicking it closes that comp without
showing it. Every comp tab keeps the ×'s room, so a tab keeps its width when
it is shown or hovered. Automation: `panel.tab.Timeline.<comp>.close` for
each comp's ×; `panel.tab.Timeline.close` stays for the shown comp's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
file.import without the flag (scripts, agents, files opened from the command line,
the CLI's templates) turned `lockin_line1.png` into a one-frame image sequence.
ImportOptions.sequence defaults to false in After Effects, so picked and dropped files
now import as stills unless `sequence: true`; folders' numbered runs still import as
sequences, and the Import dialog's Sequence checkbox is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Gesture::Vertices took the raw drag delta; it now goes through one_axis, as a
Shift-dragged layer's move does, before snapping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Import: a numbered still named without sequence imports as itself, as in After Effects (#2018)
Viewer: Shift keeps a dragged mask or shape vertex to one axis (#1964)
An SVG or PDF declaring up to 30000×30000 was drawn into one 14 GB image whose failed
allocation aborted the app. Decoding now refuses sizes past what Continuously Rasterize
draws (16384 a side, shared as MAX_VECTOR_SIDE) with an error naming the size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Media: vector footage past 16384×16384 is an error, not an abort (#2136)
A project can name /dev/zero, a FIFO or a folder as footage; reading those whole
never ended (or ran out of memory). The media layer's whole-file reads (probe,
pool, model side files) and the engine's FsServices::read_file now open the file,
refuse a non-regular one, and reserve its length fallibly, so a sparse file that
claims more than memory is an error too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Footage reads refuse anything that isn't a regular file (#2134)
An EXR's display window sizes a plane per channel, so 60000² over a 1×1 data window
was a multi-gigabyte allocation per channel; past 16384² (the vector footage limit)
the file is refused. An audio header's sample rate scales the samples read, so
0xFFFFFFFF Hz made one conform chunk about 47 GB; rates outside 1–768 kHz are an
error, and the audio reads as silence as for any unreadable source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Media: header-sized EXR windows and audio sample rates are bounded (#2141)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The maintainer asked for the counts, not a new CLI surface: the numbers come from
the catalog tests, and the row-count report test is enough to reproduce them
(cargo test -p effectcraft-ui-egui --lib catalog_row_counts_report -- --nocapture).
Since #2604 a picked numbered still imports as itself unless the import
asks for a sequence, as in After Effects. The compositing and tracking QA
scenarios import a PNG run, so they pass `sequence: true`; docs/agents.md
describes the new default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UI: open padlock on an unlocked viewer lock; inactive comp tabs show their × on hover
Viewer trackpad gestures: two-finger scroll pans, pinch zooms
UI: Composition Settings keeps Cancel and OK in place between tabs
#595: RAM preview no longer stalls on a gap right after the playhead
Timeline: parent and matte selected layers together (#1260)
UI: colour expression-driven numeric values consistently (refs #1072)
build: add an optional dev-full profile with full debug info
Timeline: trimming the Out point to the playhead keeps its frame visible
UI: double-clicking a creation tool in the Tools bar makes an instance (#1072)
fix(engine): preserve alpha channel when saving still PNG frames
Fix Animation > Browse Presets command
av1enc: truncate instead of floor, log2 lookup table, skip division in the dead zone
Fix #2201: one undo reverts a whole mask free-transform drag
Native Edit menu: use text-field Undo and Redo labels while typing
Ease Presets: restore Linear interpolation and diamond keyframes
i18n(ja): the Generate effect category is 描画
fix(text): keep an empty Type-tool layer, as AE does
feat(web): choose the project the page opens with a `startup` config key
Avoid quadratic Schur assembly for long camera tracks
Project files: opt-in compact .ecproj JSON (Save Compact Project Files)
ci: name the missing ALSA package instead of dying at link time
Text lays out as in After Effects: box text baseline, right-to-left text, Justify All, animator tracking, per-character 3D rotation
# Conflicts:
#	crates/media/src/pool.rs
Text strokes were always drawn with round joins. The document gets a
Line Join (`lineJoin: miter|round|bevel` in layer.setText / newText),
Miter for new text as in After Effects, Round for documents saved before
it so their look is kept. The 2D and Advanced 3D (extruded) text strokes
use it, expressions' style copies carry it, and the Character panel has
Miter / Round / Bevel buttons under the stroke width.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Effects that grow their buffer (CC Radial Fast Blur, CC Lens, Magnify,
the distorts and blurs) could pad a 20 x 20 layer by 4096 px a side, a
~8200 x 8200 f32 buffer plus its clone. Buf::pad (CPU and GPU) now cuts
the pad so the grown buffer stays within 16 Mpx or four times its own
area, whichever is larger: small layers are bounded at 256 MiB, big
layers still grow as before in practice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tests: QA scenarios ask for image sequences explicitly
Dragging an effect's (or mask's, shape item's, text animator's) row in
the Timeline drew a marquee. The row now drags: a line shows where it
lands among its siblings and the drop runs prop.moveGroup (one undo
step); Escape cancels, as the layer reorder drag does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	crates/expr/src/prelude.js
#2410's dependency walk for a soloed 3D model was written before
reads_other_times took the effect count; it asks about all effects (None),
as the other walks do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FilmCraft reorders AAC configurations 3 to 6, 11 and 12 to WAVE/SMPTE order
and leaves configuration 7 and program config elements as coded, so the
channel count alone can't say which order arrives: ffmpeg writes 7.1 as
configuration 7 (C L R first) and 6.1 as a PCE it fills in its own order
(L R C LFE Cs Ls Rs), while FilmCraft's encoder writes a centre-first PCE.
The order is now read from an MP4/MOV's AudioSpecificConfig when the source
opens; other containers keep a channel-count guess.

The ffmpeg oracle test gains configuration 12 (ffmpeg's 7.1 relabelled) and
skips PCE files ffmpeg 4.2 marks as stereo, which FilmCraft reads as two
channels.
The encoder's first-frame user_data_unregistered SEI now lists its settings
("... - options: rc=vbr bitrate=10000 ... scenecut=1", 211 bytes where
"filmcraft-h264enc 0.1.1 - clean-room H.264 encoder" took 50): +161 bytes.
The same build gives (104322, 11051847956767423392), the pin Linux CI took on
a base before the 19-byte nclx colr box, when that box is left out, so the
bytes match across platforms.
project: output_size never asks for sides past the comp limit
Scripts: text animator properties resolve by their After Effects names (refs #1063)
Parented cameras and lights keep their static and keyed Orientation (refs #1010)
#1417: WAV markers show on the audio layer
Expressions: cubic-Bezier easing helper (bezier(...)) (#1884)
Fix #1747: keep collapsed precomp instance overrides unclipped
layer.create: implement the instance3d op - a 2D copy of a 3D model layer (#2100)
i18n: the 31 tool names are translated (zh-hans, zh-hant)
# Conflicts:
#	crates/ui-egui/src/i18n/ui.rs
i18n: the Preview panel's labels reach the catalog
fix: bound Classic 3D depth-of-field blur and reject non-finite focus distances (#2092)
docs: clarify engine and UI-only command access (#2564)
ci(deps): bump storytold/craft-common-actions/.github/workflows/windows-arm64.yml from 8bb4e5ab95443171104ad88113fe1ef9b0e5ce3f to fa5ef18780cf5e7d5d3b8de068de56cffc83cac5 in the actions group
Audio: keyframed Audio Levels and Stereo Mixer levels ramp within a mix block
export: dither 16/32 bpc renders on the way to 8 bits, and let Render Settings Color Depth set the render's depth (#2022)
Viewer: Alt/Option-drop of a Project item replaces the layer under the pointer
# Conflicts:
#	crates/project/src/render_queue.rs
project: keep valid ROI crops on wide frames while still refusing overflow (#1577)
engine: Replace Footage keeps a still a still (#2073)
tests: count variable font named instances as styles (#2072)
Text: Chinese Han text uses the installed Chinese face (#453)
Transform: Fit to Comp sizes shape and text layers by their painted bounds (#2198)
Edit: duplicating during a text edit ends the edit (#2226)
Edit: an open text edit stands down the Project-panel Clear redirect (#2227)
Dragging a mask or shape vertex, a tangent or a Pen point against the
edge of the Composition panel now pans the view toward that edge, faster
the further out the pointer is, and the point keeps moving with it, so
zoomed-in rotoscoping no longer stops at the panel edge (as in After
Effects).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The README quoted 306 effects in three places while the registry (and the
generated docs/effects.md) had 307, now 308. A catalog test checks every
effect count the README gives against the registry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	crates/effects/src/lib.rs
The gains of an AAC source now come from the configuration FilmCraft decodes
it with, read from MP4/MOV (esds), Matroska (CodecPrivate) and LATM in MPEG
TS/PS (its first StreamMuxConfig). Channel configurations 3 to 6, 11 and 12
arrive reordered to WAVE order, 4 as L R C Cs, 7 in element order. A program
config element goes by its element positions, except the six structures in
which ffmpeg's encoder fills its own channel order across them (an LFE in a
side SCE, a back centre in a side pair), recognised by its "Lavc" comment.
ADTS can only signal configurations 1 to 7 that FilmCraft decodes, which the
channel count tells apart. A decode of fewer channels than the layout keeps
its first ones (7 of configuration 7's 8 in MPEG-TS LATM).
Tones encoded with FilmCraft's AAC encoder, with the configuration rewritten
to 11, 12 or ffmpeg's 6.1 PCE where the coded elements fit it, so nothing
depends on ffmpeg or skips. Expected gains come from each channel's speaker.
ffmpeg 4.2 writes 2 in the sample entry of an AAC PCE layout and FilmCraft
decodes as many channels as that names, so the 6.1 check now reads a copy
naming all seven instead of skipping. Configuration 12 moves to the
synthetic layout tests.
Refraction samples are clamped to the layer rectangle instead of the buffer, so a sample past the
layer's edge in a buffer with transparent padding (a half-resolution preview's, a text or shape
layer's) repeats the layer's edge pixels rather than reaching the pale colour that stands in for
transparent ones. Transparent pixels inside the layer still show it. Same bounds on the GPU path.

Re-pins the four half-resolution CC Bubbles golden hashes (aarch64 macOS); the full-resolution
ones are unchanged. Adds a CPU test and a GPU-vs-CPU parity test on padded buffers.
`Readbacks::new` spawned a thread that slept 100 ms and checked for expired readbacks forever,
whether or not anything was pending: ten wakeups a second for the life of every desktop session
with a GPU compositor (12–13 idle wakeups/s measured, against 0.4–1.8 for the sibling apps).

The worker now waits on a condition: indefinitely while nothing is pending, otherwise until the
earliest deadline or until what is pending changes (start, complete, cancel, retire poke it).
It owns the state behind an `Arc<Inner>` rather than the `Readbacks`, so dropping the context
closes it instead of leaking the thread. Timeout, exactly-once callbacks, panic isolation and the
browser's timer path are unchanged.

Measured on an M2 Max (release build, window on screen, 60 s samples, A/B interleaved): idle
wakeups 12.15/s → 2.63/s not frontmost, 12.36/s → 2.72/s frontmost.

Tests: an idle worker does not wake; a due deadline wakes it when due; dropping the context stops
it; the existing panic/strand test moves deadlines through a hook that notifies the worker.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Text: stroke Line Join — Miter, Round or Bevel (#2077)
Effects: padded buffers stay within a pixel budget (#2261)
Timeline: drag effects, masks and shape items to reorder them (#2210)
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>
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>
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>
Viewer: a mask vertex held at the panel edge pans the view (#999)
README: 308 effects, and a test keeps the count current (#2565)
`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>
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.
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.
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>
# Conflicts:
#	crates/export/tests/it/movie_bytes.rs
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 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>
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>
Import: an MP4 lasts as long as its video edit list shows (#2592)
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>
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).
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>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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
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.
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.
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.
Otherwise the eye moves back while the point it looks at stays put, and the
view direction changes.
Scripts: FootageItem.replace() swaps in one file, as in After Effects
Render Queue: elapsed and remaining time tick between frames (#2593)
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.
With the shutter open the textured squares fell back to the particle
colour. Both branches now draw through the same sprite closure, and the
sharp frame goes through plan.post like the trails.
A Texture Layer left set with another Particle Type forced the CPU for
nothing. gpu_supported now asks the same predicate the CPU path uses.
Text and shape layers hand over a buffer that spans only their content;
the clamped sampling then stretched its edge pixels across the squares.
Keyframes: mute keyframes without deleting them (#1894)
Expressions: a layer is a callable property group
CC Bubbles: bubbles at the layer's edges refract its edge pixels, not a pale fill (#2024)
UI: Rescan Fonts in the font menu; assert the Linux system scan (#1089)
fix(i18n): follow browser language preferences (#1700)
Render: remove the blanket layer cover; per-effect padding already covers what needs it
A drag that started on a script control stopped at the window edge:
moves outside were dropped and a release outside never reached the
control. While a button is held, the control it was pressed on now gets
the moves (with screen and control-relative coordinates) and the
release, wherever the pointer is, as in After Effects. Every MouseEvent
also carries buttons (held: 1 left, 2 right, 4 middle), so a handler can
tell a drag from a hover; scriptui.mouse accepts it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix #2573: New Composition From Footage creates a composition
Shapes: add the Wiggle Transform path operation, which wiggles paths and each Repeater copy
Scripts: the Startup and Shutdown folders run at launch and quit
Interpret Footage: Cineon Settings convert DPX and Cineon log scans
# Conflicts:
#	docs/gaps.md
Scripts: File ▸ Scripts entries take keyboard shortcuts
# Conflicts:
#	crates/ui-egui/src/panels/viewer/frame.rs
i18n: the labels the call-shape scan cannot see
# Conflicts:
#	ROADMAP.md
#	docs/gaps.md
i18n(ja): panels and dialogs (27% → 39%)
Light Environment lights: default environment, Casts Shadows, Advanced 3D switch
# Conflicts:
#	crates/ui-egui/src/panels/dialogs.rs
#	crates/ui-egui/src/panels/dialogs_3d.rs
#1436: settings dialogs preview live and leave the window undimmed (Gradient Editor, Solid, Camera, Light, Composition) (#1439)
# Conflicts:
#	ROADMAP.md
#	crates/ui-egui/src/i18n.rs
#	docs/localization-parity.md
i18n: add Polish (pl) interface language
perf: share unchanged layers between undo states (scripts in big comps went quadratic)
#2054's own merge of main re-added zh-hans/zh-hant rows that main already
had, so catalogs_are_unique_and_keep_the_english_shape failed on main.
The catalog is the batch-tested one, with ('ka', GEORGIAN_PANELS) back in
ROWS (the batch's merge helper had folded it into the first 'ka' entry).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
i18n: catalogs list each source once again
Add Per-Corner Roundness to Rectangle Path: a checkbox (off by default) and four Pixels properties (top-left, top-right, bottom-right, bottom-left). New rectangles initialise the four values from their Roundness, so ticking the box keeps the shape; the radii are animatable and expression-driven.\n\nrect_radii() builds the path clockwise from the top-left with kappa quarter-circle arcs. Overlapping radii scale down like CSS border-radius; negative and non-finite radii are square; rect() delegates to it, so uniform roundness clamps to half the shorter side as before.\n\nLottie rc has one r: four equal static radii export as that value; mixed or animated radii export roundness 0 with a warning. Import puts r into the four per-corner values so ticking the box on an imported rectangle keeps the shape.\n\nTests: path geometry and clamping, rendered per-corner pixels, Lottie equal/mixed/checkbox-off export.
The equal-radii and zero fallback branches wrote a bare number where the
uniform branch (and every other Value field) writes `{a, k}`; external
players may reject that form even though our importer reads it. Write
`{"a": 0, "k": value}` in both branches and update the export assertions.
Clicking an effect, Layer Style, mask or shape item row in the Timeline
always replaced the selection, so one animation preset couldn't hold
several effects and styles. Shift or Cmd/Ctrl now adds the group to the
selection, or takes a selected one out, as property rows and Effect
Controls headers already do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A docked ScriptUI panel only ever saw its preferredSize: the host drew
it in a scroll area at max(dock, preferred) and the script's window never
learned the dock. scriptui.resize now gives a window a size: the script's
window takes it, lays itself out again and runs onResizing / onResize.
The dock host sends it whenever the panel's dock size changes, so
window.size is the dock's, as in After Effects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ScriptUI: a drag keeps its control past the window edge; MouseEvent.buttons (#2584)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The test opened its project before the window existed, so its 'GPU' frames could be the ones the CPU composited while the compositor was built. It now waits for the compositor (ui_gpu::wait_for_compositor, made pub(crate)) and then opens the project, as the PR does for ui_gpu.rs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Timeline: Shift-click adds effects and other groups to the selection (#2552)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dragging an integer setting applied every step, so UI Scale resized the
interface under the pointer mid-drag. A dragged value now shows in its
field and applies when the button is released (a typed one already
waited for Enter).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ScriptUI: a docked panel gets its dock's size and onResize (#2585)
Docs: ROADMAP progress log and ScriptUI parity for 30 October
Settings: a dragged UI Scale applies on release (#2582)
"Warn User When Executing Files" said the cursor, not the user, was warned.
The other 27 rows give the same English term one Japanese spelling in the
menu table and another in the UI table (track/tracking, Region of Interest,
Time Remapping, Separate Dimensions, Stabilize Motion, Hold, Brightness,
Vector Layer, Small Screen, Straight, Expression and a few more), or read
awkwardly ("({}) Selected Objects", "Interpret Unlabeled Alpha As"). Each
now follows the spelling the rest of the file already uses. Only Japanese
values change; no key or English text does.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5NRZNWfaSLBWVzV8hivyS
- `roxmltree`: 0.21.1 → 0.21.2
- `zip`: 9.1.0 → 9.1.3

Lockfile only. `symphonia` deliberately stays 0.5.5: effectcraft names it only to enable AIFF in filmcraft-codecs' symphonia, which is 0.5.5 at the pinned filmcraft rev. Moving to 0.6 waits for that pin to move.
Describe the dimension, pixel-preservation and extreme-input scenarios and
their synthetic-frame helper to address the review's documentation warning.
Test logic and the crop/resize implementation are unchanged.

AI assistance: OpenAI Codex (GPT-6).
Export: apply crop once when calculating resize dimensions
Shapes: per-corner roundness for Rectangle Path (#1896)
Fix faceted path overlays at high viewer zoom
CC Bubbles: bubbles refract the layer's edge pixels on padded buffers too (#2024)
Add Blender scene export for the 3D Camera Tracker
Media: update the FilmCraft backend for AVI import
Media: enable AVI import across supported entry points
EffectCraft — no polling while idle: a deadline worker that sleeps, and a footage watcher that wakes the UI only on change
EffectCraft — open the window before the GPU compositor is built
# Conflicts:
#	crates/ui-egui/src/panels/timeline.rs
# Conflicts:
#	crates/ui-egui/src/panels/timeline.rs
#	crates/ui-egui/src/state.rs
About 94 rows write Latin text and numbers directly against Japanese
("360度", "EffectCraftについて", "メモリとCPU"); 37 rows put a space on
one side ("メモリと CPU", "3D レンダラー", "{} 回使用"), and no row mixes the
two. The spaced rows now follow the majority. "getartcraft.com の EffectCraft"
keeps its spaces so the domain stays readable. Only Japanese values change;
no key or English text does.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5NRZNWfaSLBWVzV8hivyS
On a project that was never saved, renderQueue.add without output queued
an item whose default output was the home folder, outside every
automation root (rendering it was refused, but the queue held a path the
agent may not write). While file access is confined, such an add is now
refused with a hint to give output inside a write root; a saved
project's default output (beside it) is checked when the queue renders,
as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `roxmltree`: 0.21.1 → 0.21.2
- `zip`: 9.1.0 → 9.1.3

Lockfile only. `symphonia` deliberately stays 0.5.5: effectcraft names it only to enable AIFF in filmcraft-codecs' symphonia, which is 0.5.5 at the pinned filmcraft rev. Moving to 0.6 waits for that pin to move.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AGENTS.md (and CLAUDE.md where present) gets a CI workflows section: workflows only call
craft-common-actions at @main, with no other jobs; feature and fix PRs don't touch CI and
link or open a craft-common-actions issue (a PR only for a small fix that is safe for
every app and does not increase CI time); legacy jobs
aren't added to and move only as their own task, staying until a shared workflow runs them.

windows-arm64.yml moves from its SHA pin to @main. The pinned revision is on
craft-common-actions main and the workflow (and the actions it calls) is identical
there, so nothing changes in behaviour.
i18n(ja): fix one mistranslation and unify terms the menu and the panels spell differently
Markers: keep names and duration handles visible at viewport edges
Fix #1743: protect render outputs from replacing project sources
#712: color pickers get an eyedropper everywhere (#1328)
Windows: document the GNU toolchain's import-library requirements (#1886)
deps: bump tar, zip, roxmltree, rustls-webpki and symphonia to latest (#2633)
fix(effects): let CC Particle World's Textured Square use a Texture Layer
Character panel: All Fills Over All Strokes and All Strokes Over All Fills in the stroke order menu
MCP: get_property and get_layer list an enum property's options (#2085)
Scale Composition: comp.scale resizes a comp and scales its layers, keys, cameras and guides by one factor
Layer Styles: changed Blending Options outlive the last style and only pixel-changing styles block a clip export (#1389)
Files named only by number (0001.png …) gave every sequence the same
name, [0001-0250].png, so five imported folders all read alike. Such a
sequence now takes its folder's name in front, plate [0001-0250].png;
named frames keep After Effects' shot_[0001-0250].png.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reveal in Explorer sent the folder as a file:/// URL, which egui opens
with the web browser on Windows. The frontend now opens file:// folders
in File Explorer on Windows (the engine already checks they are plain
folders); other platforms and other URLs are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	crates/ui-egui/src/app/frame.rs
Polystar: synchronize inner controls after expressions and key edits
Automation: a confined renderQueue.add without output needs a saved project (#2479)
Tests: the Timeline's text Animate and Add menus add animators and properties (#2513)
Import: a sequence of bare frame numbers is named after its folder (#2019)
Windows: Reveal in Explorer opens File Explorer, not the browser (#1961)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stroke width was a field labelled W, which read as the width of the
text, not its stroke. It now shows the stroke-width icon (three bars,
thin to thick), sits beside the stroke order menu as before, and has a
presets menu (0.25 to 100 px) next to its value, as in After Effects;
fractional widths show their decimals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
craft-update's App gains icon_png here: the updater, the self-update helper and
craft-update's startup prompt all get it through crate::UPDATE_APP. The prompt draws it
once craft-libs includes storytold/craft-libs#434; AppImage menu entries get it now.

Clippy fixes for Rust 1.99 in untouched code (same logic):
- crates/gpu/src/tests/wgsl_grammar.rs
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
# Conflicts:
#	crates/ui-egui/src/app/frame.rs
Graph Editor: share keyframe menus and close the Render Queue with Cmd+W
Fix/1190 checkbox drag
Text: duplicate ends the open text edit and selects the copy (#2226)
automation: a confined agent can't queue a render output outside the roots (#2479)
Audio: link stereo level edits while preserving channel balance
Puppet: Shift-select several pins in the Timeline and edit them together
i18n(ja): drop the space between Latin text and Japanese in 37 rows
Tests: Simple Choker keeps growing the matte down to -100 (#2102)
Character panel: the stroke width shows its icon and a presets menu (#2075)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Gradient Overlay's gradient showed "Edit in the Timeline" text in the Layer Style dialog.
It is now an Edit Gradient… button that runs prop.editGradient on that property; OK or
Cancel in the Gradient Editor returns to the Layer Style dialog, as in After Effects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
header.rs, render_queue.rs: main's drag-toggle checkboxes report `.changed()`; the
Snapping, Notify and per-item Render check boxes keep this PR's AccessKit names and use
`.changed()`, the per-item one inside main's `add_enabled_ui`. The Render button keeps its
name with main's translated label.
effects_presets.rs: kept this PR's row_focus/nav_click and main's rows_before_presets;
main's reveal of Animation Presets with this PR's folder_row (nav, level), and the nav's
offset follows the scroll it moves; the search field keeps its name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Noise, paint, the shutter, and the comp scene were still reaching the footprint key, so a finishing adjustment re-uploaded a full frame every frame. The key now uses EffectScope::Footprint and ignores that stack, while a moving mask still re-keys it.
An adjustment layer replaced the comp below with its effects' result whatever its blend
mode. As in After Effects, a mode other than Normal now blends the adjusted comp onto the
comp below in that mode, inside the footprint, track matte and opacity (an adjustment
layer with no effects in Screen screens the comp with itself). CPU and GPU paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Create Split Layers Above Original Layer became on by default in #2083, but a settings
file saved before then still holds `false` (every later save keeps it), so Split Layer
kept stacking the new layer below. Settings version 4 moves a stored `false` to the new
default once, like version 3 did for the old disk cache size; a choice made since stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Layer Style dialog: Edit Gradient… opens the Gradient Editor (#2057)
Viewer: motion path follows X/Y Position when dimensions are separated (#2242)
Translate remaining Japanese panel labels
Update prompt shows the app icon instead of a letter
- ui_accessibility_audit: main's new Character panel controls (stroke width presets, line
  join, stroke order) raise Character:default to 46 findings (22 unnamed); the ratchet entry
  follows, since that panel is not yet audited. The control filter is written as clippy's
  nonminimal_bool asks.
- ui_timeline_shapes (main's #2513 test): this PR names the Properties panel's Position row,
  so "Position" is no longer unique; the test takes the Animate menu's new entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	crates/ui-egui/src/lib.rs
Main routes viewer scrolling through viewer_scroll, so a raw-event scrub fought the wheel zoom. With general.viewerWheelScrubbing on, an unmodified wheel steps frames and the rest of that notch does not zoom or pan. The preference stays off, so the wheel still zooms.
Adjustment layers blend in their blend mode (#2618)
Settings: older settings files split layers above the original (#2545)
Timeline and Effect Controls value editors sent every write with the property's scrub merge
key, so two separate picks from a drop-down (or two check box toggles) on the same property
folded into one undo step. Drop-down, check box, layer and mask picks now record their own
step; scrubs and typed numbers still merge as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A slip on a layer with no source handles succeeded with `delta: 0` and nothing else, which
read like a no-op bug. The result now carries `requested` (seconds) and, per layer, `clamped`
when the source could not move that far.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UPDATE_APP gains our accent (#e0368f, our colour in ArtCraft Launcher). The prompt draws it
on the UPDATE chip, the buttons and the What's new underline, and says "by ArtCraft"
(craft-update's default developer); #2646 gave it our icon.

craft-update is pinned to storytold/craft-libs main at 1065653e7 (#434).
ui: a file dropped on the Timeline joins that composition (#2588)
ui: a drop that holds the target composition lands the rest of the selection (#2589)
i18n(ja): the panel catalog's last 65 rows
File ▸ Export ▸ Adobe Premiere Pro Project… pre-rendered every layer Premiere can't show to
ProRes 4444 with the window waiting: minutes on a large comp, which Windows reports as not
responding. The export now runs as a job (Window ▸ Progress, cancellable, with per-layer
progress) when started from its dialog (`background: true`); without it, the command still
waits and returns the same result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	crates/ui-egui/src/lib.rs
Discrete property choices are one undo step each (#2152)
layer.slip reports the requested shift and whether it was clamped (#2371)
Make font substitute test work when original fonts are installed
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
Premiere Pro export pre-renders in the background (#2269)
Accessibility (gap 25): an AccessKit audit test; names, roles and keyboard operation for Effects & Presets, Settings and Render Queue
Since #2204, ci.yml runs `cargo xtask ci` through craft-common-actions' shared gate on every
pull request and push to main; gaps.md still listed "No CI gate on pull requests" as gap 4 and
in the Stability row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs: gaps.md says the CI gate runs on pull requests (gap 4 closed)
#1339 was written before ui-egui's integration tests became modules of one binary
(`tests/it/main.rs`), so its six test files landed at the top of `tests/`, each its own crate
and link. They move into `tests/it/` as modules; the a11y module's doc names the new path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ui: the composition the drop lands in is left out even when unselected (#2589)
Viewer: mouse-wheel scrubbing over the Composition monitor (#1218)
ui-egui: the accessibility and keyboard tests join the one test binary
Cache/GPU: adjustment footprint ignores the effect stack; upload starvation is reported (#687)
Fix pretend-version testing for update prompts (#2663)
Merge pull request #2668 from nanox333/codex/gradient-stroke-width
Some checks failed
ci.yml / Merge pull request #2668 from nanox333/codex/gradient-stroke-width (pull_request) Failing after 0s
nix.yml / Merge pull request #2668 from nanox333/codex/gradient-stroke-width (pull_request) Failing after 0s
packaging-lint.yml / Merge pull request #2668 from nanox333/codex/gradient-stroke-width (pull_request) Failing after 0s
sonarcloud.yml / Merge pull request #2668 from nanox333/codex/gradient-stroke-width (pull_request) Failing after 0s
ci.yml / Merge pull request #2668 from nanox333/codex/gradient-stroke-width (push) Failing after 0s
nix.yml / Merge pull request #2668 from nanox333/codex/gradient-stroke-width (push) Failing after 0s
packaging-lint.yml / Merge pull request #2668 from nanox333/codex/gradient-stroke-width (push) Failing after 0s
sonarcloud.yml / Merge pull request #2668 from nanox333/codex/gradient-stroke-width (push) Failing after 0s
0555b5263a
Shape toolbar: keep gradient stroke widths
alf merged commit 0555b5263a into main 2026-10-30 07:59:54 +00:00
alf deleted branch chore/sync-upstream-20261030 2026-10-30 07:59:55 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
iris/effectcraft!14
No description provided.