Setting the start past the current end, or shortening the composition, stored an inverted interval. Trimming that interval then saved a negative duration.
Every pointer move replaces the comp's content identity, so the frame on
screen takes a new cache key at every step while its render is still in
flight; the viewer only ever showed an exact key, so the picture froze on
the last completed frame until the drag ended. While a drag or a property
scrub is running, show the newest frame of the same comp, frame, scale,
view and render options that is ready — a render behind, as After Effects
does — and go back to exact frames when it stops.
macOS's /var is a symlink to /private/var: the CLI resolves its working
directory through current_dir(), which is canonical, so the recorded
output path was /private/var/... while the test expected /var/....
Canonicalise the test's temp directory so the comparison holds on macOS.
The drag preview was a 1 px axis-aligned rectangle for every shape tool,
which read as no preview at all against most comps. Draw the tool's own
path instead (rectangle, rounded rectangle, ellipse, polygon, star), from
the engine's own geometry, filled with the Tools bar's Fill and outlined
in its Stroke, with a caption saying where the shape lands — a mask on
the selected layer, into the selected shape layer, or a new shape layer.
The ghost and the released shape go through one normalisation, so the
drag shows exactly what it commits: Alt (Option) draws from the press
point and Shift squares the box, both live. The drag also starts at the
press point now, not where the 6 px drag threshold was crossed, so the
shape is drawn from where the button went down, as in After Effects.
GIFs went through the still-image path, so an animated GIF came in as a
single frame with zero duration. A GIF with two or more frames is now
probed as a movie: its frame rate is chosen so every per-frame delay is
a whole number of frames (uneven timing is kept exactly), delays of 0
or 1 hundredths count as 10 like in browsers, and the pool decodes the
frame shown at the requested time. A single-frame GIF stays a still.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
decode_stream_within now bounds the output of each stage of a /Filter
chain, not only FlateDecode. RunLength, LZW, ASCIIHex and ASCII85 stop
when their output would pass the limit, a full LZW table takes no more
entries, PNG predictor rows are sized with checked arithmetic and only
allocated when a whole row fits in the data, and CCITT checks
/Rows x row bytes against the limit before allocating.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
file.uninstallScript and script_code joined the name from the request into
Scripts/<name> and handed it to the settings store, so "../.." reached files
outside the settings folder (uninstall deleted them; run-by-name ran them).
Accept only bare file names (and, on Windows, none of its reserved names),
require a .jsx/.js name, and skip refused names in the script list.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
inflate used the unbounded miniz_oxide functions, and File::parse decodes
every object stream eagerly. Use the limit variants (1 GiB per stream, 256 MiB
for object streams, a quarter of that on 32-bit targets) and bound the
truncated-stream salvage buffer too.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
The second pass over the interface below the menu bar: the strings that are technical terms or
proper nouns - `Mercury GPU Acceleration (…)`, `8/16/32 bits per channel`, `OCIO (built-in ACES
config)`, `sRGB IEC61966-2.1 (default)`, `Import Photoshop File`, `Current Settings`, the viewer's
channel and Fast Previews items. Each wording was checked against the installed After Effects
26.5.0 Simplified Chinese, the way the menu bar was in #353. Terms Adobe keeps in English
(`Display P3`, `HDTV (Rec. 709)`, `RGB`, `Alpha`, `H.264`, `ProRes`) need no row: a missing row
falls back to the English source, which is the decision.
The text reaches the catalog through the funnels that draw it, so one change each covers every
call site:
- `forms.rs`: field labels, notes, choice items and info bodies, plus `dialogs::modal`'s title -
the modal id stays keyed on the untranslated source, so changing the language cannot move it;
- `settings.rs`: page titles, sections, notes, buttons, setting labels and choice items (the page
table itself is engine data and cannot reach the catalog);
- `comp_settings.rs` (3D renderer), `key_dialogs.rs::combo` (Current Settings and the roving
items), `properties.rs` + `timeline.rs` (the text animator menu), `dialogs.rs` (About links);
- `render_queue.rs`: the Opus bitrate goes through `tr_args`, so the value is substituted after
the lookup - worth calling out, because building the string with `format!` and translating the
result looks up the assembled text and silently stays English (the same trap as the project
settings' GPU label, which this commit also fixes).
30 rows join the Simplified catalog. The same keys join the Traditional one with the English
source as their value: the tests require the two lists to match, and the Traditional wording is
the translator's call.
Tests: `CONVERTED` gains the three engine files whose strings the interface draws
(`prefs.rs`, `viewer.rs`, `commands/viewer_cmds.rs`); each holds no drawn literals, so the literal
scan stays green and the new rows have a source to be "used" from. `cargo test -p
effectcraft-ui-egui --lib i18n`: 16 passed.
LayerCollection.addSolid dropped its fifth argument, so layer.newSolid
fell back to the composition's pixel aspect. The wrapper now passes
pixelAspect through; layer.newSolid already validates it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Text a helper draws for a panel was handed to that helper as a bare literal, so the catalog lookup
never happened and parts of the interface stayed English with a Chinese language chosen:
- the Composition panel's toolbar tooltips - Choose grid and guide options, Toggle Transparency
Grid, Toggle Mask and Shape Path Visibility, Region of Interest, Show Channel and Color
Management Settings, Reset Exposure, Take Snapshot, Show Snapshot (hold);
- the items of its four popups (grid and guides, resolution, magnification, channel) - Grid,
Guides, Rulers, Proportional Grid, 3D Reference Axes, Full, Half, Third, Quarter, Custom...,
Fit, Fit up to 100%, Colorize;
- the solid dialog's "Affect all layers that use this solid" checkbox and its disabled hint
"Only this layer uses this solid.".
`tog` and `popup` now look the label up themselves, so every current and future call site is
covered by one change each; the automation ids and labels stay English, as the rest of the panels
keep them for scripts and tests. `resolution_label` composes its words from the catalog, so the
closed resolution dropdown reads Chinese too, and the viewer's item now reuses the panel's existing
"Title/action safe" row instead of a second spelling of the same concept.
21 rows join the Simplified catalog. The same keys join the Traditional one with the English source
as their value: the tests require the two lists to match, and the Traditional wording is the
translator's call rather than this change's.
Tests: the row-completeness check now also accepts a row that a helper draws (its English source
appears as a literal there), and a new check refuses a bare `Checkbox::new` / `RadioButton::new`
label - the shape that hid the solid dialog, and the one the literal scan cannot see because the
label is the call's second argument.
This also corrects two Simplified rows #398 left wrong (`Channel` read 色版 in a Simplified
catalog, `Motion Blur: {}` read 动态模糊 while the menu catalog's own Motion Blur is 运动模糊).
Cause: clipboard events carry no modifiers, and the Cut and Copy/Paste
mapping read i.modifiers, egui's state after all of the frame's events.
A Shift+Delete with Shift released in the same frame became Ctrl+X (no
deletion), and a Ctrl+X followed in the same frame by Ctrl up and Shift
down became Shift+Delete: the selected items and the layers using them
deleted without the prompt.
Fix: walk the frame's events in order from the modifiers held when it
began (the state the last pass ended with, recorded each pass), so each
Cut, Copy and Paste uses the modifiers at its place in the stream. When
that state isn't known (a pass was skipped), a Cut is never Shift+Delete.
Only Shift alone counts, not Ctrl/Alt+Shift.
Tests: a_cut_is_read_with_the_modifiers_where_it_is_in_the_frame queues
modifier changes and a Cut in one frame (Shift then Cut and Shift up;
Ctrl, Cut, Ctrl up, Shift down; and the mixed orders); the Windows
Shift+Delete helper now sends Shift, Cut and Shift up as one frame. They
fail on the previous mapping and pass now.
Cause: quantize_px rounded with `(v + 0.5) as u32 as f32`. Converting back
from u32 is several instructions in V8's wasm code on x86 (no unsigned
conversion), so quantising a 1080p layer took about 15 ms in the browser:
every 8/16 bpc layer is quantised after its source and every effect, and
the CPU compositor quantises the comp after every layer.
Fix: round_half_up uses floor in wasm (one instruction there) and keeps the
integer conversion elsewhere (x86-64's baseline has no floor instruction,
where floor is slower). The values are the same: both round non-negative
values half up. About 1.5x faster quantising in the browser (15 -> 10 ms
per 1080p layer in a wasm micro-benchmark), unchanged on the desktop.
Tests: color::tests::rounding_forms_agree: both forms agree on 0..=levels
in sixteenths (exact halves included) and on 100 001 products, at 8 and
16 bpc.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Cause: at Half, Third and Quarter resolution (the viewer's Auto resolution
in most windows) every movie frame was converted from Y'CbCr to f32 RGBA at
full size and then resampled down. In the browser (wasm, one thread) that
was about 8 ms converting a 1080p frame, 6 ms resampling it and 2 ms
zeroing a 33 MB buffer, per layer and frame, for a frame shown at a
quarter or a ninth of its pixels; the footage frame cache held the
full-size frames.
Fix: FootageSource::frame_at_size asks for a frame shown at a given size
(default: the full frame, as before). The renderer asks for it where it
resampled footage (below 3/4 scale; not for proxies, fields or Interpret
Footage's pixel options, which need the full frame). MediaPool makes opaque
4:2:0 movie frames of the footage's own size there: it box-filters them to
half size in Y'CbCr before converting (each 2x2 block of luma, and the
[1 6 1]/8 neighbourhood of its chroma sample, which is the mean of the
upsampled chroma its pixels get), resamples the rest of the way and caches
the result per size. Conversion is linear until colours clip, so this is
the RGB box filter resample starts with, except in blocks where some
colours clip. Other frames convert at full size and resample as before.
In the browser the conversion and resampling take about 4 ms instead of
16. Docs (architecture).
Tests:
- convert::scaled_conversion_matches_resampling_the_converted_frame: half,
third, quarter and odd sizes of an unclipped 4:2:0 frame are within 1e-5
of resampling the converted frame; larger sizes, odd frame sizes and
4:4:4 convert at full size.
- tests_footage_size::reduced_renders_take_footage_resampled_by_the_source:
a source that resamples frames itself renders the same frames as one
that doesn't, at 8 and 32 bpc and every resolution, and is asked only
below 3/4 scale, and not with Invert Alpha.
- footage::h264_frames_at_reduced_sizes (ffmpeg fixture): 1080p H.264
testsrc2 frames, whose saturated colours clip, at 960x540, 640x360,
480x270 and 323x182 are 58-62 dB from the resampled full frames, and
are cached per size.
- convert's ignored 1080p bench also times the conversion at a third.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Cause: the browser build kept the desktop's cache budgets (1 GB layer
cache, 1 GB footage frame cache) inside the page's memory, which wasm32
caps at 4 GiB. FilmCraft's decoders keep up to 384 MB of decoded frames
per open movie beside them, so a comp of eight stacked 1080p H.264 clips
grew the page by about 55 MB/s while playing and ran it out of memory
after about 50 s ("unreachable": a failed allocation).
Fix: in the browser Prefs::cache_budgets keeps the Layer Cache within
512 MB and the Footage Frame Cache within 256 MB (WEB_LAYER_CACHE,
WEB_MEDIA_CACHE); smaller settings and the desktop are unchanged, and
the RAM preview keeps its budget. Docs (web, preferences) say so.
Tests: tests_m145::web_cache_budgets_leave_room_for_the_decoders: with
4 GB settings the web budgets are the caps, the desktop's are the
settings, and smaller settings stay as they are.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
With the Project panel focused, Delete Project Items Without Confirmation
runs project.delete on the selection: items compositions use go without
the prompt, with their layers, in one undo step. In the other panels it
does nothing, and like every shortcut it doesn't fire while typing or
with a dialog open.
On Windows the windowing layer sends Shift+Delete as Cut (the old Cut
key), which was mapped to Ctrl+X: in the Project panel, which has no
clipboard, a Cut with only Shift held is Shift+Delete now. Elsewhere it
still cuts.
Tests (ui_project_delete): Shift+Delete as a key and as Windows' Cut
deletes a comp, a solid in use, footage and a folder without the prompt
and one undo restores them; a multi-selection with a folder holding a
solid in use goes together; an empty selection deletes nothing; the next
Delete still asks after Shift+Delete or an answered prompt; it does
nothing in the Composition panel, while renaming or with a dialog open;
Shift+Backspace, Alt/Ctrl+Delete and Ctrl/Alt+Shift+Delete don't delete.
The deleting tests fail without this change.
The prompt's Delete set a flag that let the next project.delete skip the
prompt. It now runs project.delete directly (delete_items::delete_confirmed),
so the confirmation goes with that request and can't carry over to a later
deletion. Shift+Delete uses the same path.
A frontend command (project.deleteWithoutConfirmation) so the shortcut is
listed and rebindable in Keyboard Shortcuts and runs by id over the
control channel and MCP. The UI runs project.delete for it.
Translate the 396 current panel catalog rows and route Ukrainian Reveal
labels for Finder, Explorer and File Manager. Enforce catalog parity,
placeholder preservation and platform label coverage; update language docs.
Use a fixed-size OCIO row iterator to satisfy the current Clippy gate.
Co-authored-by: Codex <[email protected]>
Convert Position values and spatial tangents into the new parent space.
Preserve supported static Orientation and Scale in constant uniform
parent spaces, and convert spatial Position ease speeds for uniform
transforms. Retain the compensation and Undo/Redo regressions.
Refs #348.
Co-authored-by: Codex <[email protected]>
Cause: Repair used REINSTALLMODE=ecmus. effectcraft-cli.exe has no version
resource, so Windows Installer treats it as unversioned: a changed copy
with a later write time counts as a user edit and is kept, and "c" only
checks files that carry an MSI checksum, which these don't.
Fix: Repair uses "amus", which puts back every file whatever its version
or date.
Tests: test-msi.ps1's stand-in CLI is unversioned like the real one; the
test damages it in place and repairs with the REINSTALLMODE read from
installer-ui.wxs (no longer msiexec /fa after deleting it), then compares
contents. It fails with ecmus (the damaged file stays) and passes now.
Cause: the folder search read only the App Paths "Path" value, which this
branch introduced. 0.6.0 and earlier could already be installed elsewhere
with INSTALLFOLDER=..., but have no Path value, so an upgrade went to
Program Files while the wizard said it would stay in the same folder.
Fix: when there is no Path value, a second search reads the App Paths
default value (the full path of effectcraft.exe, written by every
release). A "file" registry search with no FileSearch inside yields the
folder of that file.
Tests: test-msi.ps1 installs a legacy package (the same .wxs without the
Path value) into a custom folder, then upgrades it without naming a
folder; it fails on the previous .wxs (the upgrade lands in Program
Files) and passes now. A further upgrade checks the Path value route.
ui.rs gets a Japanese table with the same 396 rows as the Simplified and
Traditional Chinese ones, so Japanese now translates the interface below
the menu bar too (it covered the menus only). The shape test keeps the
three tables listing the same source strings; a new test checks that
Japanese reads its own rows and fills values in order. The existing
"no rows yet" check uses Ukrainian instead.
ROADMAP.md and docs/preferences.md say which catalogs have panel rows.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Get-ItemProperty throws on a malformed Uninstall entry, which aborted the
upgrade check on a real Windows 10 machine. Failures now print their message.