Create each atomic write through its own exclusively created temporary sibling and keep its open file handle until writing finishes. Concurrent writers publish complete files without truncating or following an existing temporary entry; failed writes or renames clean up their own staging file.
Keep the temporary basename bounded independently of the destination name so previously supported long project filenames still save. Retain the concurrent-writer and symlink regressions and add a 251-byte filename overwrite regression that checks exact bytes and staging cleanup.
Fixes#1233
Co-Authored-By: Claude Opus 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
`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
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>
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>
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.
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.
- 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>