Commit Graph
1768 Commits
Author SHA1 Message Date
dotCooCoo 2f437ea551 EPS and PDF: huge arcs, long paths and loop passes count against the import's limits
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.
2026-10-14 10:19:05 -07:00
Hamza a213b7f23a i18n (de): fix two labels that name the wrong thing, settle six terms (#1443)
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.
2026-10-14 21:21:39 +05:00
Nigel Ihza Firdausy ba37283aa4 fix(lottie): serialize Offset Paths miter limits as animatable values 2026-10-14 23:14:18 +07:00
tylerjenningsw 8b536e597b Export: millisecond-rounded footage renders at its rate, each frame once (#1442) 2026-10-14 11:37:36 -04:00
tylerjenningsw 36a905293b Media: tests for the averaged MP4 frame rate (#1442) 2026-10-14 11:33:49 -04:00
tylerjenningsw eb95f732e5 Media: millisecond-rounded MP4 frame times read at the rate they average to (#1442)
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.
2026-10-14 11:33:49 -04:00
zitann2020andDeepSeek V4.1 Flash 66a29505a5 i18n(ja): the Settings dialog in Japanese
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
2026-10-15 00:19:47 +09:00
midasmatterhorn c3b88d0d76 pdf: don't slice glyph names at a non-char boundary in name_to_unicode 2026-10-14 18:01:41 +03:00
wixar172andClaude Opus 5.5 9846c7255d Shapes: a stroke's buffer is padded by the miter limit only where it has miter joins
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>
2026-10-14 17:20:02 +03:00
Marco Arnulfo cc29d90943 Import: item.metadata counts a WebM's last frame when its duration is not a whole millisecond (fixes #1429) 2026-10-14 14:22:15 +02:00
Hamza 449e9edd59 Render Queue: resolve a bare project filename against the working directory
`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
2026-10-14 17:21:58 +05:00
Marco Arnulfo 78b4fe4170 Import: AIFF files import, including EffectCraft's own AIFF renders (fixes #1428) 2026-10-14 13:44:59 +02:00
Marco Arnulfo 69d17d89af Keyframes: a path pasted onto Position becomes a motion path (fixes #1425) 2026-10-14 13:30:45 +02:00
arnav-hiwarkar 68d928aff3 fix(render): preserve preceding effects in GPU Echo (#1120) 2026-10-14 13:42:59 +05:30
Marco Arnulfo 1b44c5c98a Scripting: Stroke and Hue/Saturation parameters use After Effects' match-name numbers (part of #1405) 2026-10-14 09:15:12 +02:00
arnav-hiwarkar bed289e2a1 fix(presets): identify copy folder action by label
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.
2026-10-14 12:09:48 +05:30
arnav-hiwarkar 8041329df8 feat(media-browser): add Copy Source Path 2026-10-14 11:42:13 +05:30
Marco Arnulfo ce437747ed Expressions: accepting a function completion inserts the call parentheses (fixes #1393) 2026-10-14 05:28:14 +02:00
Marco Arnulfo 7df16a5e20 Effects: 4-Color Gradient keeps the layer's alpha with Blending Mode None (refs #1381) 2026-10-14 02:56:17 +02:00
DWAA1660andClaude Opus 5.5 e0bb54e4c6 render: hash the comp scene of selected layers that read it into the owner's cache key (#1252)
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
2026-10-13 18:05:48 -06:00
DWAA1660andClaude Opus 5.5 4b475f5781 model: budget the memory an FBX import keeps, and share instanced meshes (#1234)
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
2026-10-13 17:44:27 -06:00
DWAA1660andClaude Opus 5.5 3d45046132 model: validate glTF accessor spans before allocating, with checked arithmetic (#1235)
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
2026-10-13 17:35:25 -06:00
Kristian MyramandClaude Sonnet 5.5 9f02bd2a72 #1368: dragging an effect lights up the layer it will land on
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 00:59:11 +02:00
DWAA1660andClaude Opus 5.5 79ebd01935 svg: bound <use> expansion by an element budget and skip reference cycles (#1236)
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
2026-10-13 16:59:08 -06:00
Marco Arnulfo dd5afffee3 Scripting: test that Layer.property() resolves AE's top-level shortcuts (#1348) 2026-10-14 00:55:12 +02:00
Kristian MyramandClaude Sonnet 5.5 be3261419b Viewer: Composition Settings… in the composition's right-click menu
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-14 00:46:05 +02:00
DWAA1660andClaude Opus 5.5 da73df4b2e Script sockets: per-socket locks, a write deadline, and close interrupts (#1248)
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>
2026-10-13 16:03:53 -06:00
DWAA1660andClaude Opus 5.5 7a9ca5810a Scripts: a script can't confirm executing a file for itself (#1246)
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>
2026-10-13 14:47:51 -06:00
Kristian MyramandClaude Haiku 5.5 fdc515f5be #1063: find an undefined name's first use from the failing frame on
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>
2026-10-13 21:58:52 +02:00
tylerjenningsw 4dddb34cba Ease Presets: tests for Out alone through undo and redo, and a single key on a curved path (#1329)
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.
2026-10-13 15:04:19 -04:00
tylerjenningsw 9b74dba2a8 Ease Presets: skip roving keyframes and say so, rather than reporting them eased (#1329)
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.
2026-10-13 15:04:19 -04:00
ThemolxandClaude Opus 5.5 f62af4ca73 Guide layers: hidden in nested comps, and in the Timeline context menu, as in After Effects
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>
2026-10-13 20:44:40 +02:00
ThemolxandClaude Opus 5.5 03b7a958e5 Solo: exclude other layers even when the soloed layer is outside its time range
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>
2026-10-13 20:30:26 +02:00
tylerjenningsw af486dc44a Ease Presets: tests for easing single keyframes In, Out and In-Out (#1329)
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.
2026-10-13 14:28:50 -04:00
tylerjenningsw 3a953859f2 Ease Presets: ease the incoming, outgoing or both sides of selected keyframes (#1329)
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.
2026-10-13 14:28:49 -04:00
ThemolxandClaude Opus 5.5 a15a7a4818 Null layers: the Video switch hides them in the Composition viewer, as in After Effects
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>
2026-10-13 20:04:34 +02:00
sanyamsingh001andCodex 36ebe2eada Honor duration rounding in timeToFrames
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>
2026-10-14 03:21:23 +10:00
Lloyd NicholsonandClaude Opus 5.5 d309d6f1d0 Effects & Presets: drag animation presets onto a layer
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>
2026-10-13 19:04:33 +02:00
DWAA1660andClaude Opus 5.5 76a3e7eefe Masks: addVertex and setVertex fill in missing tangents of a loaded path (#1250)
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>
2026-10-13 11:04:02 -06:00
zitann2020andDeepSeek V4.1 Flash 5569aad8b5 i18n(ja): four labels After Effects contradicts
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
2026-10-14 01:57:37 +09:00
Ian-ChegeandClaude Opus 5.5 5270eab095 Masks: Mask Expansion past a curve's radius grows a solid mask instead of a ring
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>
2026-10-13 19:15:30 +03:00
MarekSpodBiedry 9050b1818e Scripting: add the missing app.preferences.saveToDisk() so imported scripts like Duik get past startup (#1263) 2026-10-13 18:02:11 +02:00
zitann2020andDeepSeek V4.1 Flash 2ff7639219 i18n(ja): the resolution labels agree with the panel catalog and After Effects
Co-Authored-By: DeepSeek V4.1 Flash <noreply@deepseek.com>
2026-10-14 00:57:21 +09:00
Vikram Bhaskaran d15c82b163 Liquify: expose Distortion Mesh Offset in Effect Controls (#1194)
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.
2026-10-14 02:46:26 +11:00
Vikram Bhaskaran bde6eb57f3 UI: Spacebar previews the demo opened from the start screen (#1059)
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.
2026-10-14 02:20:07 +11:00
Vikram Bhaskaran f12d75e37a Engine: Interpret Footage loop coverage for sequences (#1068)
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.
2026-10-14 02:15:32 +11:00
Vikram Bhaskaran 4ddf5ffd6f CC Particle World: motion blur smears particles across the shutter (#1195)
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.
2026-10-14 02:01:30 +11:00
dotCooCoo 1cc315708e Find Missing Fonts: fonts found by their PostScript or full name are no longer listed
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.
2026-10-13 07:43:21 -07:00
zitann2020 9ab33d9758 i18n(ja): the first rows of the Japanese effect catalog (#821) 2026-10-13 23:33:38 +09:00
Vikram Bhaskaran 48738093b5 Shapes: Merge Paths keeps paint that belongs to a group above it (#1174)
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.
2026-10-14 00:02:12 +11:00