* Scripting: export commands a script runs directly respect Allow Scripts to Write Files
CompItem.saveFrameToPng and exportAsMotionGraphicsTemplate checked the
file-write preference only in their JavaScript wrappers. A script could call
comp.saveFrameAs or essential.exportTemplate itself (app.run, __exec) and the
native handlers wrote the file with the preference off.
Session::refuse_export and write_output_with now refuse when the running
command was issued by a script and the preference is off, so every command
that writes a path it is given is gated the same way. The wrappers check the
gate with a new side-effect-free __file("checkWrite") instead of writing an
empty file at the destination.
* address review: clear stale export test dir before use
* test: allow script writes in script_imports_footage_and_saves
popup_key lowered only ASCII letters, so an item starting with a capital
outside ASCII (French "Écran") kept it and never matched the same word in
lower case. After Effects stores popup items in its UI language, and
#3432 starts matching such names through popup_alias, keyed by
popup_key. Use Unicode lower case; ASCII keys are unchanged.
* .aep import: recognize French names for Levels' Channel and Clip popups
After Effects stores a popup's items in the UI's own language. A project saved
by a French After Effects writes "RVB", "Activé" and "Désactivé pour la
couleur 32 bpc" for Levels' Channel and Clip to Output Black / White.
EffectCraft's own popup options are English only, so these never matched by
name and the parameter fell back to its default, reported as "has no
counterpart".
Add popup_alias, a small table of confirmed After Effects labels in other
languages, used as a fallback in popup_mapping after the exact name match
fails. A synthetic test reproduces the three losses from a real file and
checks they are gone, with the same values as the English original.
* Cover Levels' French Red, Green and Blue popup items too
popup_alias only mapped "rvb" (RGB), so a French Levels with Red, Green or
Blue selected still lost that selection the same way. Add "rouge", "vert"
and "bleu" (from the same real pdnm list, confirmed against it); "Alpha" is
already the same word in both languages. A direct test on popup_alias covers
every item of the Channel popup it claims to know.
---------
Co-authored-by: sunkidda <sunkidda@users.noreply.github.com>
picks_what_the_viewer_shows_across_sizes_scales_zoom_pan_resolution_and_proxies looped over four
independent window-size and display-scale cases, each with its own session. As one test it took
~90 s in the gate and ~590 s under coverage, where it alone set the slowest SonarCloud shard.
Each case is now its own test, running the same checks, so nextest runs them in parallel and
partitions them across shards. The "more than 100 hovers checked" floor becomes more than 25 per
case, which still adds up to more than 100.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In a comp whose Start Timecode is negative, the current-time display shows
the first frames as `-0:00:00:12`, but typing that back (the Go to Time
dialog opens with it since #3419, and the Timeline's time field takes it)
read the leading `-` as "move back", so OK on an unedited dialog jumped to
another frame. There, a `-` before a full timecode is now the time's sign,
as it already is before Feet + Frames; a bare `-n` still steps back, and
comps that start at 0 or later are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Synchronize Time of All Related Items clamped each related comp's time
with its own duration-minus-one-frame rule, so a nested comp whose
duration is off the frame grid could not reach its final partial frame.
Use comp_frame_at, as direct moves and restored times do.
A frame-rate change can leave the duration off the frame grid. The
current-time clamp used duration minus one frame, which put the last
reachable frame before a final partial frame, so a layer fitted to a
work area inside it never got the playhead. Clamp to the last frame
that starts before the end instead; on-grid durations are unchanged.
The work area need not sit on frames (a typed start, a frame-rate
change), so rounding its start to the nearest frame could land before
the new layer's in-point. show_span now rounds up to the first frame in
the span and moves nothing when the span holds no frame.
show_span now checks and moves the playhead of the comp the layer was
made in, through a new Session::set_time_of that set_time uses for the
active comp, so time sync and the text editor boundary stay the same.
With Fit New Layers to Work Area on and the playhead outside the work
area, the Type tool, shape tools and Pen now move the playhead to the
work area start so the new layer and its controls show. Layer timing is
unchanged; Layer ▸ New commands leave the playhead.
Settings ▸ General ▸ Fit New Layers to Work Area (off by default) makes
new text, solid, shape, null and adjustment layers start at the work
area start and end at its end. It applies to Layer ▸ New, the shape
tools and the Pen tool, and wins over Create Layers at Composition
Start Time. Footage and compositions keep their own length.
#2944 (497a056c) normalises each box-blur mean in f64 and rounds once
(it was rounded to f32 first, then scaled), and counts repeated edge pixels
with one multiply instead of adding them one by one. Its PR states results
change only by float rounding, and it added the focused regression test the
golden-test rule asks for (box_row matches a direct sum). The golden hashes
are only enforced on aarch64 macOS, so Linux PR CI passed, and every
completed macOS CI run on main since #2944 has failed (497a056c's own run
was cancelled by a newer push).
Bisected on an M5: the golden tests pass at ed085bd4 and fail at 497a056c
with the same 52 hashes macOS CI reports. All 52 are 32 bpc frames of
effects that blur through box_plane/gauss_plane (Lightning, Advanced
Lightning, Inner/Outer Key, Caustics, Paint Bucket, Vegas); every 8 bpc frame
is unchanged. All 52 frames differ by at most 4.8e-6 per channel, with no new
non-finite values.
Only the pinned hash values change; LIBM_VARIANTS is untouched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clears all 29 open SonarCloud issues.
- `examples/plugin-catalog/scripts/validate.sh`: `[[ ]]` in place of `[ ]` for bash tests (SonarCloud shell:S7688). Every script has a bash shebang; no test uses `-a`/`-o`, escaped parentheses or an unquoted pattern on the right of `=`.
- `crates/expr/src/prelude.js`, `crates/script/src/prelude.js`, `crates/script/src/scriptui.js`: `Number.isFinite` / `Number.isNaN` / `Number.parseInt` (javascript:S7773). Every call already has a number (a `typeof` check or `Number()` before it), and `Number.parseInt === parseInt`, so nothing changes in boa.
- `packaging/windows/test-legacy-fixture.ps1`: dot-sources `[scriptblock]::Create($block)` in place of `Invoke-Expression $block` (powershelldre:S8659). It runs in the same scope, so `$LegacyWxs` is still set after it.
- `sonar-project.properties`: S7773 and S8786 are not raised on `*.jsx`. Those are After Effects ExtendScript (ES3), which also runs in After Effects and has no `Number.isFinite` or `String.prototype.trim`. The ignore covers 7 issues in `Ease Presets.jsx` and `Rename Layers.jsx`.
* Gradients: bound the stops sampled per pixel
Gradient sampling scans the stops linearly and shape painting samples once
per pixel, so a gradient with a huge stop list multiplied every frame's
work. Sampling now looks at the first 256 colour and opacity stops, and the
plain JSON form (coerce_json) refuses gradients with more than 256.
* address review: bound tagged gradients to the stop limit
* Text: merge variable font axes by tag and cap their number
Each tag in a variations object scanned the style's growing axis list, so a
large object cost quadratic time, twice (validation probe and apply). The
object is now merged in one keyed pass, refused when it names more than 64
axes, and a merge that would leave a style with more than 64 is refused
without changing the style.
* cargo fmt
* address review: refuse variation over every run before changing any
* address review: empty-document range cannot overflow in set_attr
* Auto-trace: bound the nesting, simplification and fitting work
Many separate regions made the nesting depth test every pair of contours,
Douglas-Peucker could rescan a span once per point (and recurse as deep as
the loop is long), and the curve fit searched the raw contour from its start
for every simplified vertex.
- Depth: each loop's bounding box goes into a coarse grid; a loop's first
point is tested only against loops whose box holds it. Same depths as
testing every pair.
- Douglas-Peucker: spans wait on an explicit stack, with a budget of 64
distance evaluations per point; spans still waiting when it runs out keep
all their points, so the outline stays exact.
- Fitting: uses the raw indices the simplifier kept instead of searching.
* cargo fmt; clippy manual_is_multiple_of
* address review: keep wide contour boxes out of the nesting grid
* Box blur: count repeated edge pixels instead of visiting them, and bound the radius
* address review: box blur first mean ignores an unused edge term (inf * 0 gave NaN)
* fix(render-queue): keep Output Module crop padding within the largest output frame
* address review: crop padding is taken back only when the side would pass the cap
* PDF fonts and fax: bound charstring work, ToUnicode ranges and reference search
Type 1 and CFF charstrings bounded subroutine depth but not total work:
subroutines that call each other several times multiplied a small font into
unbounded instructions and geometry. Each glyph now has a shared budget of
100,000 instructions. The Type 1 Subrs parser looked up /CharStrings again
for every entry; it is found once. A ToUnicode bfrange expanded a long
destination string for each of up to 65,536 codes; destinations past 256
bytes are skipped and a CMap keeps at most 2^20 mappings. CCITT 2-D decoding
scanned the reference row from its start for every code; it bisects.
* address review: cap bfchar and array bfrange unicode mappings
* address review: seac components share the glyph instruction budget (CFF and Type 1)
* Segment checkpoints: charge memo copies and string bytes; refuse impossible zip sizes; bounded inflate errors
The pickle reader charged memo reads but not memo writes, and counted a
string as one object whatever its length, so memoizing one long string
many times kept unbounded copies; writes are charged too and strings count
one node per 64 bytes. A zip entry stating more than deflate can expand
its data to is refused before inflating, and inflate errors keep only the
status, not the partial output.
* address review: charge every owned field of Global, Storage and Tensor to the pickle clone budget
Reuse the existing diff() helper so a non-finite pixel fails the test
(f32::max ignored NaN). Keep exact equality on llvmpipe, which still
fails without the row-block fix; other adapters use the usual Bpc32
tolerance, because a 2e-6 bound can fail on unmeasured hardware and
does not catch the regression there.
Horizontal GPU box passes restarted the running sum every 256 pixels, while
the CPU keeps one sum per row; use the full row width as the block so f32
rounding matches. The regression test requires exact equality on llvmpipe
(CI); other adapters only get an absolute 2e-6 sanity bound.
The pace counted cached frames among the next ahead x 2, but the prefetch never requests more than the RAM preview holds (offsets 0..fit-1 from the current frame). With room for only a few frames the lead could never fill, so a preview that rendered every frame at once was slowed to a fraction of its frame rate, with Lower Resolution When Playback Can't Keep Up off (the default). The lead is now counted over at most the frames the prefetch keeps after the current one, and with none of those (room for at most two frames) the clock runs in real time as before pacing.
An effect past the limit was still added to the example lines when they had room,
so a report could name up to 120. Past the limit an effect is only counted.
The dialog's Frame field held the engine's zero-based frame, so with frames shown
from 1 the viewer's 00013 opened as 12, and typing 13 went to the frame displayed
00014. It now holds the time in the project's display style (timecode, frames from
Start Numbering Frames At, or feet + frames), which time.set already reads that
way. The now-unused 'Frame' translation rows go.
The step-down was skipped while the achieved frame rate read as keeping up, and that rate comes from the frames shown in the last second they were shown: through a stall it kept the rate from before it, so a preview that played a cached stretch and then stalled never stepped down. A rate whose last frame is more than a second old no longer counts.
The import report keeps only the first 20 example lines per category, and each
unmapped effect instance was one line in the Effects category. With other losses
in that category, an effect used on many layers could be counted without being
named. Each effect with no counterpart now gets one line, with how many instances
there were ('... and 68 more like it'), placed before the other examples. The
category's count is unchanged.
The time display in the Composition panel's bottom bar only reacted to
Ctrl/Cmd+click, which switches timecode and frames. A plain click now opens the
Go to Time dialog (View > Go to Time), as in After Effects. The Ctrl/Cmd+click
test follows: its plain click now opens the dialog, which it closes.
In an 8 bpc comp, a movie used by both a plain and a masked layer has an 8-bit and a float entry in the media pool, decoded separately. Rare and only a speed cost, so it is noted where the 8-bit path starts rather than changed.
It never steps back up during a play, so a looping preview kept playing at a quarter of its resolution after its first loop. Without it the paced first loop caches the viewer's resolution and the next loops play in real time, as in After Effects. The setting stays for whoever prefers a lower first pass.
Only a lane's drain thread waits for a GPU slot. A lane runs one drain at a time (it is idle, and started again, only once its drain found no job), and its other threads only help with that frame's parallel work, as Rayon helpers that never wait. The comments said lanes had one thread; the test now runs on a lane LANE_WIDTH wide and checks that its other threads decline while the drain thread waits.