The EPS interpreter counted one operation against its budget for each
name it ran, whatever the operator did, and paths had no length limit.
Parsing one arc at a radius of 10^300 passed 3 GiB of resident memory
within 43 seconds. A loop of arc at a radius of 2.2 x 10^18 was still
running after 60 seconds, and a loop of pathbbox on a path of 100,001
elements and { {} loop } loop did not finish within 20 seconds. PDF
content had no path limit either: parsing a page that repeats
0 0 1 1 re 4,000,000 times peaked at 2.3 GiB of resident memory.
A path now holds at most 1,048,576 elements (MAX_PATH, the limit of
VectorCraft's EPS interpreter). In EPS, moveto, lineto, curveto,
closepath, arc, charpath and the rect operators check it, and an
operator that would take a path past it stops the program with
"PostScript (path limit)". In PDF, m, l, c, v, y, h and re (each adds
at most five elements) stop the content stream with
"content (path limit)" when the path already holds more than 1,048,571.
Each path element an operator makes, reads or copies (arc, rectangle
arrays, charpath, pathbbox, clip, eoclip, gsave, save) now counts
against the EPS budget. Each pass of repeat, for, loop and forall
counts too, and so does each procedure item that is not a name. A
moveto right after a moveto replaces it, as the PostScript Language
Reference describes, so text placed with moveto and show leaves one
element.
Five tests in tests_import.rs cover the loops, the path limit in both
interpreters and runs of moveto and show.
EffectCraft's RPM requires Fedora package names: `libxkbcommon`, `(vulkan-loader or libglvnd-egl)`,
`alsa-lib`. openSUSE has no `libxkbcommon`, `vulkan-loader` or `libglvnd-egl` package, so zypper
refuses it with `nothing provides 'libxkbcommon'` (exit 4, which the ArtCraft Launcher shows as
"error 4"; storytold/craft-launcher#341).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two German menu labels described a different command:
- `layer.timeReverse` was "Ebenenreihenfolge umkehren", which reads as reversing
the stacking order rather than time -- now "Ebene zeitlich umkehren", matching
`keys.timeReverse`;
- `layer.style.convertToEditable` was "In editierbare Formate umwandeln", and
"Formate" are formats, not layer styles -- now "In bearbeitbare Stile
umwandeln".
Where the same English term had more than one German rendering, the one already
in the majority wins, so this settles rather than re-translates:
- "Expression" is "Ausdruck" in seven entries and "Expressions" in the two
`layer.expressions` ones, which now read "Ausdrücke aktivieren/deaktivieren";
- "Reveal" is "anzeigen" in eleven of twelve entries, so `layer.revealInFinder`
joins `file.revealInFinder` at "Im Finder anzeigen" -- in every other
catalogue both commands already share one string;
- "Track" as a verb is "verfolgen" in the other four entries, so `track.mask`
is "Maske verfolgen" rather than "Maske tracken";
- one spelling of "nach vorne", matching the neighbouring arrange entry.
German also had no platform variants for the file manager. The engine puts the
platform's name into the label before the catalogue sees it, so on Windows the
source is "Reveal in Explorer" and on Linux "Reveal in File Manager"; only
Ukrainian and Czech special-cased those, and a German UI showed the English
string. Added, with a test mirroring `czech_…` and `ukrainian_…`.
Two of the reported six are left alone: "Essential Graphics" is a 3-3 split and
a panel's proper name, and "Null" partly mirrors the English, which itself says
both "Null Object" and "Nulls". Both are translator's choices with no majority
to follow, not corrections.
Phones and messaging apps store frame times rounded to whole milliseconds
(33/34 ms at 30 fps, 16/17 ms at 60 fps). FilmCraft's reader takes the
median duration, so such clips imported at 30.303 or 58.824 fps, and New
Comp from Selection and renders followed that rate. When the median is no
standard rate, the durations near it are averaged and snapped to the
standard rate within 0.5 %; exact and variable rates keep the reader's
estimate.
draw_bounds padded every stroke by width × miter limit / 2 on each side, joins or
not. A straight 1080 px wide wipe used as a track matte became a 4326×6366 layer
buffer (~440 MB as RGBA f32), made anew every frame while its trim animates, then
cached on the CPU and uploaded to the GPU. With 12 frames exported in parallel a
16 GB Mac ran out of memory and froze.
Now the miter limit applies only to miter-joined paths with a join (two or more
segments, or closed); square caps reach √2 half widths, other strokes one; Taper
and Wave keep the widest padding. Rendered pixels are unchanged.
A full-res frame of the affected ad: 1.7 GB → 0.7 GB peak.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`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
Follow up on #1210: remove the fixed menu index from automation and click handling, and move the preset-folder documentation into the Effects & Presets section.
Keep ordinary --empty renders from loading the demo. Preserve explicit
project, template, and demo selection, and cover them with real GIF renders.
Sort the relative-footage test's expected paths consistently with its output.
Co-authored-by: Codex <codex@openai.com>
A layer selected by another layer's effect (Displacement Map, Blend...) had
its structure, values, matrix and time hashed into the owner's key, but not
the comp camera/lights its own scene-reading effects (Caustics, CC Particle
World...) see. The owner's key included the scene only when the owner itself
ran such an effect, and a cached owner never runs the selected layer's
effects, so moving the camera or a light reused stale owner pixels.
Dependencies::layer now hashes comp_scene for every selected (or nested)
layer that reads it, with the same policy as owner keys.
Fixes#1252
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9kwMcw2pYQ5fSjKa3ZPnh
The 1 GiB array limit counted wire bytes, but arrays are kept as f64/i64,
so boolean arrays grew 8x and 32-bit arrays 2x past it, and nothing bounded
the total across arrays. Every Model linked to a Geometry also built its own
copy of the mesh.
- A per-import Budget (2 GiB) is charged 8 bytes per array element before
the array is decompressed or widened, ASCII files are charged for their
parsed values, and each built mesh is charged for its vertex and index
data. Going past it fails the import with a clear error instead of an
allocation abort. parse_within/read_within take a lower limit for tests.
- Models instancing the same geometry with the same materials share one
mesh; a different material mapping still gets its own.
Fixes#1234
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9kwMcw2pYQ5fSjKa3ZPnh
Accessor counts sized the f32/u32 output before the buffer view was looked
at, and offsets, strides and count * components used unchecked arithmetic,
so a few hundred bytes of JSON could request a multi-gigabyte allocation
(aborting the process) or overflow.
- Doc::layout checks count * components, adds the view and accessor offsets,
rejects a byteStride smaller than an element, and checks that the last
element ends inside the buffer, all with checked arithmetic, before
floats() or ints() allocate anything.
- Accessors without a buffer view (all zeros) are capped at 2^24 components.
- idx() and node index lists use usize::try_from instead of 'as usize'; an
image bufferView's end and a GLB chunk's end are computed with checked_add.
Fixes#1235
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9kwMcw2pYQ5fSjKa3ZPnh
Depth-limited <use> expansion still let a few kilobytes of branching
references expand to 2^N elements. The parser now counts every element it
converts, <use> copies included, against a budget of max(500 000, twice the
file's own elements) and fails the import with Error::Xml once it is spent.
Targets currently being expanded are tracked, so mutual references stop at
the first repeat instead of nesting to the depth limit.
Fixes#1236
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N9kwMcw2pYQ5fSjKa3ZPnh
Every socket operation locked one global table and held it through the
blocking write_all/flush, and streams had no write timeout (accepted
ones were explicitly blocking). A peer that stopped reading kept the
script blocked for good, and every other socket call, close included,
waited behind it.
- The table maps handles to Arc<Entry>, each with its own Mutex<Sock>.
The table is locked only to look a socket up, add or remove one,
never during network I/O, so one stalled socket doesn't hold up the
others.
- `write` sets a write timeout from the socket's `timeout` (10 s by
default, as reads) and reports "write timed out … the other side
isn't reading" instead of blocking. Failures to set the read or write
timeout, or to make an accepted stream blocking, are reported instead
of ignored.
- `close` removes the socket and shuts its stream down through a second
handle, so a call blocked on it fails at once.
Tests (socket.rs, native only): a 64 MB write to a loopback peer that
never reads fails with "timed out" after a 1 s timeout; while one socket
is blocked writing, another answers at once, and closing the blocked one
returns at once and ends its write. Both fail on main (the write never
gives up; the second socket waits on the table lock).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
effectcraft-cli read its command line with std::env::args, which panics
on an argument that is not valid Unicode (a Latin-1 file name on Linux,
for example). `effectcraft-cli $'\xff'` or
`effectcraft-cli render-frame --out $'caf\xe9.png'` printed a panic from
std's env.rs and exited 101.
It now reads std::env::args_os and refuses such an argument as a usage
error (exit 2) that names it, before anything runs, like an unknown
option. A lossy copy is not used: every argument is a string here, and
`caf\u{fffd}.png` would read or write another file than the one named.
FilmCraft moved its CLI to args_os too (storytold/filmcraft#850).
Tests: a unit test for the conversion and a process test that runs the
CLI with a non-Unicode `--out` path (both unix-only). Before the fix the
process test fails (exit 101, not 2).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
makehybrid -hfs gave every file in the image a com.apple.FinderInfo, so the app in the
DMG, and a copy installed from it, failed codesign --verify --strict --deep ("resource
fork, Finder information, or similar detritus not allowed"), though Gatekeeper accepts
it. Build the image as VectorCraft (#1085) and PdfCraft (#823) do now: hdiutil create
-srcfolder with a few tries for CI's "Resource busy", and verify the mounted app with
--strict --deep so the regression can't ship again.
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>
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.