project_dir rebuilt the folder with `/` and dropped a UNC share's leading
`\`, so a project opened from \server\share\Job looked for its moved
footage under the relative path server/share/Job and never found it. It now
keeps the path's own separator and its leading `/` or `\`.
The relink tests built mixed-separator paths (`…\Job/p.ecproj`) and so failed
on Windows; they now use native paths and check the separator is kept.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Drop the byte-count tooltip, the "—" marker and their strings and UI test,
and the bench wrapper script: they are outside this PR's scope and After
Effects leaves the Size cell blank when the size is unknown. The size sort
and its tests are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review on #1627: the X arrow tip can sit on a bounding-box handle (a 120 px
wide layer at 100%). The arrow is painted on top, but the press and hover
tests checked the handles first, so dragging it scaled the layer. Check the
arrows first in both, and cover the overlap with a pointer-input test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 3D gizmo's X/Y/Z arrows were drawn but not interactive, so pushing a
layer in depth meant typing Z Position by hand. With the Selection tool,
dragging an arrow now moves the layer along that axis only (no snapping),
as one undo step, reusing the viewer's Move gesture.
An axis pointing at the camera (Z in the default front view) projected to
a dot; it is now drawn as a short diagonal stub so it can be grabbed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The After Effects stagger idiom
id = thisProperty.propertyGroup(2).propertyIndex;
failed with "not a callable function": the expression object model had
no propertyGroup() and property groups had no propertyIndex.
- Property.propertyGroup(countUp = 1) and PropertyGroup.propertyGroup()
return the group countUp levels up; from a top-level group they reach
the layer. A count below 1, not a number, or above the layer is an
error naming propertyGroup, never a crash.
- PropertyGroup.propertyIndex: the group's 1-based index among its
siblings (Child requests now carry it).
- One new host request (Ancestor) walks up from the node's full uid
path, so it also works for the property being evaluated.
- Tests for the stagger expression, effect parameters, reaching the
layer, other layers' properties and groups and out-of-range counts;
the fuzzer seeds and tokens include the new members.
Load PDF document objects through the pinned craft-pdf parser and convert them into EffectCraft's existing drawing model with bounded traversal, recovery and retained data. Preserve generation numbers, stream limits, encrypted-document refusal and reported repairs.
Retain app-owned drawing, EPS, fonts and content/image decoding. Assert early excessive-nesting rejection without partial art, and cover tolerant arrays on native and WASM.
Validated with the22-stage local matrix and exact-head FreeBSD, Windows ARM64 and packaging checks.
Co-authored-by: Codex <noreply@openai.com>
The Effect Controls header menu and the Timeline property menu build their
items with the same local closure the layer menu used, so they still drew
English. Both now go through i18n::context_item; Copy, Paste, Edit Value…
and Remove Expression get panel catalog rows.
The face's own style (Figtree's Light) is its default instance, so it
resolves without axis values and lays out as before, on the static path.
docs/agents.md says how named instances are selected.
In the browser editor a .jsx script couldn't open any file, even one the
page holds next to the project: File.open("r") returned false with "File
not found" and File.exists was false. The footage check after File > Open
also marked imported media as missing.
Both asked whether a file exists on the local disk (std::fs), which has no
files on wasm32:
- scripts read files through Session.services but checked existence with
std::fs (File.open's access check, File.exists, Folder.exists); they now
ask services.exists / services.is_dir, after the folder check;
- WebServices kept the default exists (std::fs); it now looks in the file
table.
Desktop and CLI are unchanged (FsServices keeps the std::fs defaults).
Tests: script tests_services (a host with in-memory files), web smoke
step hostFiles (a script reads a stored file, footage check finds stored
media).
Clicking keyframes on Position, Rotation and Scale highlights those
properties, and Copy then added every key of a highlighted property that
had none selected, so deselected keys came back on paste. The keyframe
copy branch also left a stale value clipboard that Paste preferred over
the new keys.
Copy now takes whole animations only when no key is selected or the
Transform group is selected, and every copy branch clears the clipboards.
Also collapses a nested `if` in the STL importer into a match guard,
which Clippy 1.95 flags under -D warnings (no behaviour change).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The font scan read only whether a face has an fvar table, so a variable
family offered one style (its default instance), fontStyle = "Black"
picked that face with synthetic bold, and an instance's PostScript name
(Figtree-Black, what After Effects scripts use) found no face at all.
The scan now reads the named instances (style, axis values, PostScript
name, or the one Adobe Technical Note #5902 derives). They are styles of
the family in text.fonts and the font menus, a style, PostScript name or
full name selects the instance, and layout uses its axis values as the
base variations; axes set on the text still win.
The roi quad is stored raw from the control channel (crop.useRoi passes
serde_json's [f64;4] straight through, unlike the sibling edge fields,
which are clamped to plus/minus 20 000) and nothing repairs it on load.
Crop::edges converted it with round() as i32 and subtracted raw: an x of
-1e12 saturates to i32::MIN and w as i32 - l overflows in debug builds,
wrapping to a wrong-but-bounded crop in release.
Clamp each converted edge to the same plus/minus 20 000 the sibling
fields use and subtract with saturating_sub in edges() and frame_size().
Regression test: hostile_roi_cannot_overflow_crop_math — panics at
crates/project/src/render_queue.rs:774 on main; with the fix the hostile
quad degrades to a 1x1 frame and the sane-ROI behavior is unchanged.
inline_len computed (w * n * bpc).div_ceil(8) * h with Width/Height taken
straight from the content stream and no size validation — unlike decode()
and mask_image, which bound their geometry first. /W 5000000000000000000
overflows the multiply in debug and test builds; in release it wraps to a
small length and the caller slices the wrong bytes, misparsing the image
and shifting the EI resync.
Chain the arithmetic through checked_mul and return None, which the
caller already routes to the EI-scan fallback. Regression test:
inline_image_with_absurd_dimensions_falls_back_to_ei_scan — panics at
crates/pdf/src/image.rs:85 on main, passes with the chain.
mask_image read /BitsPerComponent from the attacker-controlled /SMask
dict with no validation and fed it straight into 1u32 << bpc — the main
image path rejects anything but 1|2|4|8|16 before its own identical
shift, the mask path did not. /BitsPerComponent 32 panics in debug and
test builds and wraps the shift in release, mistranslating every sample
into alpha.
Mirror decode()'s depth check: an unsupported mask depth means no usable
mask, so the image draws with the mask treated as absent. Regression
test: smask_with_absurd_bpc_is_ignored_not_fatal — panics at
crates/pdf/src/image.rs:184 on main, passes with the check.
`pen_press` placed each new vertex where it was clicked (after snapping)
whatever the modifiers, so Shift did nothing and a vertical, horizontal or
45° segment had to be lined up by eye. With Shift held, the new vertex now
moves onto the nearest line at a multiple of 45° through the vertex before
it, as far along that line as the click lies (`constrain_45`). Masks and
shape paths both go through this path.
`exec_on_keys` put the named keys in the key selection, then `exec` made
the keys' comp active. Since comps keep their own selections (#1037),
activating the comp stored the injected keys with the comp that had been
active and brought back the target comp's own key selection, usually
empty, so `keys.interpolation` and `keys.velocity` had nothing to act on:
setInterpolationTypeAtKey and setTemporalEaseAtKey were silently ignored
in any comp other than the active one. The keys' comp is now made active
before the keys are selected, and the comp that was active comes back
afterwards, as `exec` does for its own target (now `leave_comp`).
Entries whose layer buffer was freed were pruned only once the cache went over its budget
(1.5 GiB), so an animated layer, which makes a new buffer every frame, left a dead 4K texture
per frame allocated until the cache filled up.
Retry only explicitly read-only control queries after connection failures. Report ambiguous edit outcomes without resending mutations, reconnect on the next explicit call, and reject request-ID exhaustion without overflow.
Add loopback regressions for lost and malformed replies, read-only retries, playback queries versus edits, and unknown methods.
Fixes#1247
Use checked division and remainder for EPS integer operators so i64::MIN with a divisor truncating to -1 cannot panic. Preserve the existing zero-divisor fallback and normal truncation behavior.
Add public EPS parse regressions covering extreme, fractional and ordinary operands.
PR #1223
The PDF / Illustrator import dialog now offers Composition - Retain Layer
Sizes. Each file layer's footage is cropped to its artwork bounds (fills,
stroke outlines, images, clips) and positioned to keep its place on the
page. Decoding, continuous rasterisation, shape conversion and reload share
one crop (effectcraft_pdf::layer_doc_with_sizes), so they agree.
Also collapses a match guard in the STL reader for current stable clippy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Black & White offset each pixel's luminance by (hue weight - 0.5) times
its HSL saturation. HSL saturation is near 1 for any tinted near-black
pixel, so the code or two of colour noise in compressed footage's
shadows came out as bright and black blocks, and a 32-bpc overbright
such as (1.5, 0.5, 0.5) divided by zero and went black.
The gray is now the pixel's smallest channel plus its chroma (largest -
smallest channel) times the same hue-interpolated slider weight. Neutral
pixels keep their value, a pure hue becomes its slider's percentage
(Reds 40% turns pure red into 40% gray), noise moves the gray by no
more than its own size, and nothing divides. The CPU effect and the GPU
kernel change the same way.
Merge each offloaded property-group result against the snapshot the job started from. Preserve disjoint live edits, group identity and child changes; refuse conflicting results without changing the document or undo/redo history.
Record an apply rejection on its originating job so completion logs and waiting control commands report failure. Keep concurrent jobs' outcomes separate and retain the correct completion position when the bounded log rolls over.
Add actual-worker regressions for conflicts, disjoint edits, concurrent analyses and undo/redo, plus control completion/error regressions.
Fixes#1249
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Problem: effect colour labels (26.5) were drawn only on the Effect Controls header; the Timeline's effect rows looked unlabelled.
Fix: an effect group row in the Timeline paints the label colour as a 4 px bar on its left edge (same look as the Effect Controls header) and registers `timeline.group.<uid>.label` with the label's name for agents. Docs: gaps.md row 21 and ui-parity.md updated.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>