A ReferenceError's first use was searched across the whole file, so a mention
earlier in a comment, a string or another function could report the wrong line.
The search now starts at the innermost script frame's line, and covers the whole
file only when there is no frame. Follow-up to storytold/effectcraft#1161.
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Out on an auto-Bezier key leaves its in side playing the same values,
and undo and redo restore each state. On a curved Position path with
unequal segment lengths and durations, In sets the key's speed from the
incoming path length, playback arrives faster than a constant speed, and
the out side plays as before.
A roving key passes speed straight through: the engine keeps both its
sides linear whenever the keys are edited (retime_roving), so an ease set
on one never plays. The panel still counted it as eased, so a selected
roving key showed "Eased 1 keyframe(s)" and stayed linear. The previous
pair-only panel did the same to a pair holding a roving key.
Selected roving keys are now left out of the work and counted. The
status names how many were skipped and that turning off Roving lets them
ease; when nothing else was selected, nothing changes and no undo step
opens. Fixed keys next to a roving key still ease.
Guide layers render only in their own comp's viewer: a nested renderer (precomp, collapsed
precomp in 2D, Classic 3D and Advanced 3D, comps read by effects, aux passes, the GPU walk's
resident precomps) now runs with guides off (Renderer::nested_opts), so a precomp's guide
layers no longer show in the parent comp's viewer or in a "Current Settings" render.
The Timeline layer and Composition viewer right-click menus (shared layer_menu) gain a
Guide Layer item (a plain toggle, no check mark) acting on the selection, and guide layers show a guide mark
before their name in the Timeline.
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>
One middle key between unequal segments in each mode, with its
neighbours, times, values and selection unchanged and one undo restoring
it; first and last keys, keys that aren't neighbours and keys of several
properties; no undo step when nothing can ease; a Hold and an
auto-Bezier side kept as they play; per-dimension and motion-path
speeds on Scale and Position; and the pair's outer sides under In-Out
and Out. All but the unchanged API tests fail on main.
The panel eased only pairs of neighbouring selected keyframes, so a single
selected key (or keys that aren't neighbours) changed nothing.
An Ease selector (In, Out, In-Out; In-Out by default) now chooses the
sides of each selected key to ease. In eases a key's incoming side with
the curve's in side, Out its outgoing side with the curve's out side. A
side is eased when it has a neighbouring key, selected or not; the
neighbour is left as it is. Each side's speeds scale to the segment on
that side, so a key between a +100/s and a -40/s segment gets each its
own. The side not eased keeps playing as it did (easeSide, unchanged).
Sides are measured before any key changes, and no undo step opens when
no selected key has a segment on the chosen side; the status says which
neighbour is missing. A selected pair in In-Out still gets the whole
curve; its outer sides now ease too when they face an unselected key.
From Keys still reads the curve of a selected pair.
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>
Preset rows (text, built-in and user .ecpreset) were click-only, so dragging
one onto a layer did nothing. They now start a DragPayload::Preset drag with
a ghost, and the Timeline, Composition viewer and Effect Controls apply the
preset to the layer it is dropped on, as they already do for effects and as
After Effects does.
Co-Authored-By: Claude Opus 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>
Expansion was a fill plus a stroke 2·|expansion| wide, blended per pixel. Once the
expansion passed the shape's curvature radius the stroke's inner edge inverted: a
20 px circle expanded by 60 left a ring with the layer showing through the middle,
+600 cut only the original dot, and negative values left islands. The outline is
now offset with Offset Paths' geometry (round joins) and filled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Compare device and inode on Linux alongside existing path checks. Cover
template and values aliases with input-preservation and diagnostic checks.
Sort the existing footage test's expected paths for temporary-directory portability.
Co-authored-by: Codex <codex@openai.com>
After Effects' Liquify shows Distortion ▸ Offset (x, y), and the renderer
already applied distortionMeshOffset — sampling the mesh at layer
coordinates minus the offset — but the parameter was declared hidden, so
users could neither see nor edit it, and #1194 read as 'the effect does
not work and the Offset option is missing'.
It is now a visible 2D Point control (still defaulting to 0,0, still
scriptable by its old path). Regression test: the spec exposes it as a
Point, a [20,0] offset moves the displaced-region centroid right by the
offset (the render differs, and the left of the shifted mesh is back to
untouched pixels), docs/effects.md refreshed via UPDATE_DOCS.
The reported flow — launch to the start screen, Open Demo Project, press
Space — does not reproduce on main: the demo comp activates, the start
screen closes, and playback starts. The behaviour already held (the
#279 menu-focus and #204 start-screen work fixed the neighbouring bugs),
so this lands as a regression test:
space_previews_after_opening_the_demo_from_home walks the exact flow
headlessly and asserts playing=true.
cargo test -p effectcraft-ui-egui --test ui_menus: 8 passed.
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.
With the comp's and the layer's motion-blur switches on, the renderer
hands effects the shutter (angle, phase, samples) — CC Particle World
now uses it: eight to sixty-four sprite fields sampled across the shutter
are averaged into the transfer, so fast particles draw the motion trails
After Effects shows instead of one sharp frozen field.
The GPU particle pass defers to the CPU while the shutter is open
(catalog::gpu_supported), keeping the oracle identical on both backends.
Tests: sim::particle_world_motion_blur_trails (blurred differs from
sharp, lights more pixels, keeps alpha energy) and a catalog assertion
for the CPU gate; effects 450 and gpu 212 lib tests pass; fmt and
clippy -D warnings clean.
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.
A Merge Paths item appended to a shape layer's Contents cleared every draw
collected so far, including fills and strokes inside groups above it, so
the layer's shapes vanished from the render. AE's rule is per group: only
paint items in the merge's own group above it are dropped. Draws now
record which contents level created them; the merge drops only its own
group's paint, and the group-bubbled draws keep rendering.
Adds a regression test (Merge Shapes and Add modes) that renders nothing
without the fix.
The pinning regression only exercised All Edges (and Left Edge in the CPU
unit test), so the reported 'does not work with Top/Bottom Edge' had no
coverage to catch or clear it. This holds the pinned first/last row in a
CPU test at Direction 90, and adds CPU/GPU parity cases for Top (4),
Bottom (6), diagonal directions, and single-edge pinning on shape layers.
CPU pinning verified correct on main: the guards in dst_wave_pin/wave_pin
agree; the gap was that neither edge path was tested.
Problem: on Windows two tests failed on main although the app is fine; they assumed
macOS/Linux path formats.
Fix: the CLI test strips the verbatim path prefix that canonicalize adds on Windows (a
child's current_dir() never reports it); the host sink test uses a drive-letter path on
Windows (a path rooted without a drive is joined to the home drive by the engine, the test
expected the current drive). Test code only.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
- Keyboard Shortcuts: Enter is not Delete while the delete confirmation is open (#1113 with #1019).
- Speed graph drag test compares proportions, since the Graph Editor now fits its selection (#1182 with #1109).
- Spanish panel test no longer demands the Chinese catalogs' exact rows (#996 with #1189, #1019).
- Object Matte gets its Simplified Chinese effect-name row (#1199 with #792).
Co-Authored-By: Claude Opus 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>
The nightly FreeBSD run measured 119 ms/frame against a 100 ms limit: the mean included
Tracker::new and any scheduling stalls on the two-core VM. The best single step with a
250 ms bound still catches an order-of-magnitude regression.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>