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>
With several layers selected, typing a value into one dimension (say Z
Position) sent the edited layer's whole value with selected: true, and
prop.set gave every selected layer that same value: they all took the first
layer's X and Y and stacked on top of it. Scrubbing was fine (it sends an
offset).
prop.set takes dims: with selected and a typed value, the other selected
layers get only those components and keep the rest of their own value (Position
with Separate Dimensions included). The three panels send the components the
edit changed (both for linked Scale). Without dims, agents still set the whole
value on every selected layer.
Fixes#1434
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9kwMcw2pYQ5fSjKa3ZPnh
Adds Georgian (ქართული, code ka) to Settings > General > Language, with
Match System for ka / ka-GE / ka_GE.UTF-8 locales:
- i18n.rs: the menu catalog, the same 628 (command, English label) keys as
the other languages, plus the Undo/Redo entries, the workspace and 3D
view shortcut submenus, the Reveal labels on Windows and Linux and the
macOS Services items.
- i18n/ui.rs: GEORGIAN, every row of the Chinese panel catalogs (687), and
GEORGIAN_PANELS, 195 rows for text the panels send through tr() that no
catalog has yet (form dialogs, drop messages, screen-reader names,
context menus, Project and Timeline column headers). The tests hold
them to the same rules (unique, same placeholders, used by a converted
panel) and keep them from shadowing a Chinese row; prefs_live.rs joins
the converted files.
- i18n/effects.rs: Georgian names for all 307 effects and 21 categories;
the CC series and brand names stay in English.
- Properties > Text Animation: the Add Animator and Presets buttons grow
with their translated labels (English keeps 116 / 84 px).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMTAbGNRxnMEzRvYnQGphF
`resolve_output` takes the project's folder from `Path::new(path).parent()`.
For a project saved under a bare filename that is the empty path, which the
following `filter` discarded, so a saved project fell through to the
unsaved-project default and relative outputs went to ~/Movies (the user profile
folder on Windows) instead of next to the project.
--project proj.ecproj ~/Movies/clip.mp4
--project ./proj.ecproj ./clip.mp4
Both spellings name a project in the working directory, so both now resolve
there, as the doc comment on `resolve_output` already describes.
Closes#1427
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).
Project::comp_contains recursed through precomp layers with no visited
set or depth limit. A damaged project with a precomp cycle that doesn't
pass the queried comp (A and B nesting each other, C unrelated) recursed
until the stack ran out, which aborts the process ("stack overflow,
aborting"): it can't be caught as a command error, so unsaved work is
lost. layer.addItem checks it before adding a precomp, and the renderer
calls it for every nested comp and its audio. A long chain used the
stack the same way, and comps shared by many paths were walked once per
path (exponential for nested diamonds).
It is now an explicit stack with a visited set: same answers (a comp
contains itself; a target reached through a cycle is found), each comp
visited once, no recursion. The renderer already skips a precomp that
contains the comp being rendered, so a cyclic project now renders
instead of crashing; nothing is repaired on load.
Tests: project comp_contains_ends_on_cycles_long_chains_and_shared_
precomps (cycle with an unrelated target, self-nesting, a target through
a cycle, 40 levels of diamonds, a 20 000-comp chain); engine
a_project_with_a_precomp_cycle_opens_adds_and_renders_without_running_
out_of_stack (file.open, layer.addItem both ways, render). On main the
engine test aborts with a stack overflow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Warn User When Executing Files on, file.executeFile asks the
frontend first, and the frontend's answer re-runs it with
`confirmed: true`. That flag was taken from the parameters whoever
called, so a script could pass it itself (app.run('file.executeFile',
{path, confirmed: true}), also inside engine.batch) and the file opened
without the question, as long as the user had allowed file and network
access.
`confirmed` now counts only when the command isn't running for a script
(Session::running_for_script, which #590 uses for the same reason and
which stays set through nested batches). A script's request always gets
the question; the user's answer from the frontend's form still opens
the file; with the warning off nothing changes. The permission gate
itself is unchanged.
Tests: engine a_script_cannot_confirm_executing_a_file_for_itself
(execute_for_script directly and through engine.batch; the frontend's
confirmation; warning off) and the same through real JavaScript in the
script crate (app.run, app.run('engine.batch'), File.execute()). Both
fail on main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In After Effects solo is not time-dependent: while any AV layer is soloed,
non-soloed layers are excluded at every time. The renderer, nested/collapsed
3D paths, engine viewer helpers and viewer hit tests only counted solo layers
active at the current time, so outside the soloed layer's in-out range the
whole comp reappeared. Drop the time condition (audio already ignored it).
Update the #66 viewer regression test to the AE behaviour and add a render
test for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every layer gets the Video switch (eye) in the Timeline (nulls, cameras and lights
were excluded by an is_av() check). A selected layer whose eye is off no longer
draws its box, handles, anchor or paths in the viewer. Alt-click on an eye and
Layer > Switches > Hide Other Video now take nulls along.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round nonzero duration frame counts away from zero while retaining
absolute-time rounding, default argument selection and numeric coercion.
Cover fractional durations, absolute controls, defaults and coercion.
Sort expected paths in the existing footage test for temporary-directory portability.
Co-authored-by: Codex <codex@openai.com>
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>
A path's vertices and its two tangent arrays deserialize independently,
and nothing on load makes them agree. mask.new pads them, but a path
read from a project file may carry fewer tangents than vertices.
mask.addVertex checked its index against the vertices only and then
inserted into both tangent arrays, and mask.setVertex indexed the
tangent arrays after the same check, so either command panicked on
such a path (Vec insert/index out of bounds).
ShapePath::fit_tangents gives both arrays one entry per vertex (missing
tangents become corners, extras are dropped), and edit_path_counted
calls it on the value it edits, so every mask/shape-path vertex command
works on a consistent path. Other keyframes of an animated path were
already padded by apply_count_op when the vertex count changes.
Tests: two regression tests save a project, cut every mask path's
tangent arrays to one entry (static value and keyframes), reopen it
through Project::from_json and run setVertex and addVertex. Both panic
at mask.rs:209 without the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Looped *video* footage had a regression (#1045); a grouped PNG sequence
going through the same path — interpret a fresh placeholder to Loop 15,
drag it onto the timeline — did not. The behaviour is already correct on
main (addItem sizes a sequence layer from looped_duration, the pool's
frame_index wraps per cycle), so this is coverage, not a change:
interpret_footage_loop_extends_sequence_layers asserts a 15-loop of a
0.4 s sequence lands a 6 s layer.
cargo test -p effectcraft-engine --lib interpret_footage: 4 passed.
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.
File > Dependencies > Find Missing Fonts listed a text layer whenever
its font matched no family name, while text layout finds a font by its
family, PostScript or full name (effectcraft_text::resolve). For a
layer set to the PostScript name Inter-Bold, EffectCraft draws the text
in Inter Bold, and text.fontFeatures reports the layer's font as not
missing. Find Missing Fonts listed that layer and a layer set to the
full name Inter SemiBold.
find_missing now lists a layer when effectcraft_text::resolve, which
text layout uses for each run, reports the layer's font as missing.
fonts_named_by_their_postscript_or_full_name_are_not_missing adds text
layers in Noto Serif, Inter-Bold, Inter SemiBold and a font that no face
has, and checks that Find Missing Fonts lists only the last one.
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>
- Cryptomatte (#1183): planes shorter than the image read as empty instead of panicking.
- layer.addMarker (#1180): a comp marker no longer replaces one at the same time.
- Desktop (#1005): ignore macOS's -psn_ argument instead of refusing to launch.
- Doc comments put back on their own items (#1153, #1037).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The six PSD PRs merged one by one leave #1131's test calling samples()/to_rgb(), which #1139
replaced; vangel76's integration draft #1149 has the adapted version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>