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>
keys.quickOffset and keys.move stored the drag's new total before the
edit ran. When the edit refused the step (keys.quickOffset with a key
landing past the end of time), the project stayed as it was but the
total kept the refused delta, and with incremental steps (merge without
fromStart) the next step moved by it too.
The drag state is now stored only after the edit applies.
Test: a_refused_step_does_not_count_towards_the_drag (+1 s, a refused
step, -1 s: the keys end where they started; fails before: the last key
ends near the end of time).
keys.move and the nudges snapped each key's destination to the nearest
frame, so moving keys a Quick Offset had staggered onto subframes put
them back on frames: four keys at 0, 1/3, 2/3 and 1 frame, moved one
frame, came out at 1, 1, 2 and 2. Layer bars kept theirs, because
layer.timing snaps the move, not the times.
The move is now rounded to whole frames and every key translated by it.
layer_shift turns a comp-time move into a layer-time move through the
layer's start and stretch (reversed too) without a round trip through
comp time; keys.quickOffset uses it as well.
Test: moving_staggered_keys_keeps_their_subframe_spacing (Quick Offset,
then keys.move and keys.nudgeForward; fails before: 1/3 frame lands on
a frame).
prop.reset cleared every keyframe before setting the default value. As in
After Effects (and as effect.reset already does for effect parameters), an
animated property now keeps its keyframes and gets the default as a key at
the current time; a static property just takes the default. The expression
is still removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Validate saved stroke frames against the current layer limits before clamping a span edit. Keep project and undo state intact when trimming has left strokes outside the layer.
Co-Authored-By: OpenAI Codex <noreply@openai.com>
After Effects 25.4's Quick Offset staggers selected layers, or each
layer's selected keyframes, in the order they were selected: the first
stays, the last moves by the drag and the ones between by equal steps,
landing on subframes.
layer.quickOffset {delta, layers?} moves each layer's start, in and out
point by total * i / (n - 1), worked out once in ticks with integer
arithmetic, so 1 frame over 4 layers gives 0, 1/3, 2/3 and 1 frame
exactly. keys.quickOffset {delta} does the same per layer for the
selected keys, the layers in the order their first key was selected;
every selected key of a layer, across its properties, moves by that
layer's comp-time offset through its start and stretch (reversed too),
keeping values, interpolation, easing and labels. Locked layers are
skipped and at least two layers are needed. With merge and fromStart,
delta is the whole drag and each step starts again from before it, so
the drag is one undo step and a key passed over survives. A delta or a
destination out of the range of times is an error, not a clamped move.
Docs: agents.md; architecture.md notes Quick Offset as the exception to
times snapping to frames.
keys.move (and nudging) rebuilt the key selection in layer and property
order, so the order the keys were selected in was lost after a move. A
Quick Offset that follows staggers layers in that order (#1012).
The move now goes through move_key_refs, which maps each selected key to
its new time and returns the selection in its original order, with the
same collisions (a key landing on another replaces it), roving retiming
and locked-layer handling as before. key_gesture is the drag-step logic
(start again from the keys and project as they were before the drag)
split out of shift_keys so other key drags can share it.
Test: moving_keys_keeps_the_selection_order (fails before: the keys come
back sorted by layer).
The 2^28 pixel limit applied per image; one composite or layer decode kept
the ZIP output, a Vec<f32> per channel and the result alive together, and the
bake size came from the file's quad. A decode now charges every buffer
against a byte budget (4 GiB, 1 GiB on 32-bit), checks its whole expected peak
before allocating, writes channels straight into the result and returns an
error when over budget. Bakes are at most 4x the document across and 2^24
pixels.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Psd::parse drops the layers, records Psd::warnings and keeps the header and
merged image; the importer then imports the flattened image as one layer and
reports 'Layers could not be read'. Header damage and a missing merged image
stay hard errors.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Psd::tree flattens groups nested deeper than MAX_GROUP_DEPTH into their depth-64 parent, keeping every layer, so the importer recursion and the tree's Drop/Clone/PartialEq are bounded. The import result reports how many groups were flattened.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
CameraLayer gets the transform and camera-options shortcuts (position, xRotation, zoom...) that AVLayer already had, and Convert Expression to Keyframes run without a property converts the selected ones, as app.executeCommand does it.
Problem: the built-in animation preset library had no Backgrounds category
(gap 7: 31 presets against After Effects' 621).
Fix: 23 original Backgrounds presets in crates/engine/presets/backgrounds.json,
built from our own generator, noise, colour and stylize effects: Soft Gradient
Drift, Aurora Veil, Slow Fog, Cloud Bank, Dark Smoke, Ember Haze, Magma Flow,
Ice Flow, Pastel Plasma, Cell Drift, Crystal Facets, Hex Mosaic, Mirrored Silk,
Water Caustics, Blueprint Grid, Checker Scroll, Scrolling Stripes, Pulse Rings,
Bokeh Drift, Spotlight Floor, Dawn Sky, Sweeping Light, Grainy Wash.
The preset format gains `expr` (an expression per property, so backgrounds keep
moving for the whole layer) and point parameters written as fractions of the
layer size. Effects & Presets lists them under * Animation Presets > Backgrounds.
Tests: catalogue uniqueness and categories, apply/undo/redo of every preset
(engine), render of every preset with the expression engine: finite,
non-flat, deterministic, moving (host), panel listing and apply (ui-egui).
Sidecar, ATTRIBUTION.md, gaps.md row 7 and effects-parity.md updated.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Guides can store a percent of the comp so they follow comp-size changes
(View > Add Percentage Guide, Convert Guides to Percentage / Pixels); an
effect instance has an optional colour label (effect.setLabel, Effect Controls
header bar and Label menu). Both are serde-defaulted, so old projects load.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
keys.deselectAll (Shift+F2, Cmd+Alt+Shift+A) clears the selected
keyframes, properties and groups and leaves the layers selected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
comp.workAreaToSelection (Cmd+Alt+B) spans the selected layers, or the
whole composition with none selected. comp.activateRecent (Shift+Esc)
goes back to the composition that was active most recently within the
active one's nesting hierarchy; EditorState keeps recent_comps for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
time.workAreaStart / time.workAreaEnd (Shift+Home / End) and
time.previousInOut / time.nextInOut (Cmd+Alt+Shift+Left / Right): the In
and Out points of the selected layers, of every layer with none selected.
time.go takes nextInOut and prevInOut.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The media layer keys decoded frames by path, and nothing dropped them when a
new project (or a new import) named a file that another app had overwritten
since, so a same-size overwrite showed the old pixels. Importing footage and
replacing the project now make the media layer and the layer cache let go of
the files involved.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Split out of 7782ab01 (shapes: Reverse Path Direction …; work area keys) for upstream, where it
belongs with the commands it binds.
(cherry picked from commit 7782ab015a01613c1685cb6dda48d48c3b1db67b)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01F2dXDtF1ivCLP4tQwFveQ4
(cherry picked from commit b58cfb7434e147535e66582718b8856bd3ce1230)
(cherry picked from commit ff4876054f3cbf9388954602e8fe7288d136c255)
(cherry picked from commit 79d083a8c2a0bfa4663445257149a0d0ce491987)
Shortcut presets accept Physical+<key> (egui US-position names) so layouts that report the
same logical key for two physical keys (AZERTY's - and )) can bind them separately; physical
bindings win for that press, and a focused panel's binding wins over an application command on
the same chord. ui.key accepts the same notation. Adds time.workAreaStart / time.workAreaEnd.
(cherry picked from commit 209543e9e0ba32f809f9ff333abb8b8ec4125cea)
(cherry picked from commit f8301a9e3e0b60e200e8a233455398741658481e)
(cherry picked from commit 4d002fabf992934e1bb73a0c32bc9f3fd3ae5447)
(cherry picked from commit 5d466c5955ee934d5347c194ec7d15fb5f58bba4)
File > Import > EffectCraft Project (and .ecproj/.ecprojx given to File > Import)
merges the project into a folder named after the file, with every id shifted past
the open project's, in one undo step. Relative footage resolves next to the file;
the result lists comps, root comps, primaryComp and missing media.
offset_ids now shifts tracker Motion Targets and saturates instead of overflowing;
fix_next_id saturates too, so a damaged file can't overflow on open or import.
Menu entry translated in all ten menu catalogues.
Rebased onto current upstream main for the split of #858 (test module list in
engine lib.rs; es/pt-br/ru/fr/de/cs menu translations).
(cherry picked from commit 19c653fccc95133fb35e978a7f7f20a18c0e8c89)
(cherry picked from commit 4e751ad2)
(cherry picked from commit ceed706cc3b5f3755769738a1ddb3d965a94ab6b)
Shift+Spacebar set Skip to 1, so it played every other frame out of the box. It now plays every
frame, like Spacebar. Skip still works when it is set.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Alt+1 after Alt+Shift+1 shows the red channel in grayscale instead of going back
to RGB (and the other way round). The same key again is still RGB.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
layer.sampling takes `toggle`: layers that all have the sampling already get the
other one. The Bilinear and Bicubic menu entries, which carry the keys, pass it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F2 Deselect All, Cmd+Shift+T Effect Controls, Cmd+Alt+Left / Right Go to Start /
End, Cmd+Shift+/ Add to Render Queue.
Effect Controls is a menu entry with bound parameters, which could hold one key:
a menu line can now give further default keys after a comma
(F3, Cmd+Shift+T); the menus show the first. MenuEntry gains alt_shortcuts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>