Commit Graph
1014 Commits
Author SHA1 Message Date
tylerjenningsw 4ae87f1e83 Packaging: the legacy-folder fallback covers every release before this installer change (#466)
v0.7.0 has no App Paths Path value either, so the comments no longer
name a release.
2026-10-10 10:52:20 -04:00
tylerjenningsw 59c9169892 Packaging: the installer's Repair puts back a damaged effectcraft-cli.exe (#466)
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.
2026-10-10 10:52:04 -04:00
tylerjenningsw a26867fe4b Packaging: upgrading a custom-folder install from an earlier release keeps its folder (#466)
Cause: the folder search read only the App Paths "Path" value, which this
branch introduced. Releases before this installer change 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.
2026-10-10 10:52:04 -04:00
tylerjenningsw 561d6fd24f CI: the MSI test reads Apps entries one value at a time (#466)
Get-ItemProperty throws on a malformed Uninstall entry, which aborted the
upgrade check on a real Windows 10 machine. Failures now print their message.
2026-10-10 10:52:04 -04:00
tylerjenningsw 66c4b7693e Contributors: tylerjenningsw's display name is tylerjenningsw 2026-10-10 10:52:04 -04:00
tylerjenningsw 23a5e4a4a9 Contributors: credit tylerjenningsw as Tyler Jennings 2026-10-10 10:52:04 -04:00
tylerjenningsw 13c8f284ec Docs: releasing.md describes the MSI install folder and its test (#466) 2026-10-10 10:52:04 -04:00
tylerjenningsw 677ef1c571 README Downloads: the Windows installer lets you pick the folder (#466) 2026-10-10 10:52:04 -04:00
tylerjenningsw c9f62e1c7b CI: the MSI is install-tested with a custom folder, upgrade, repair and uninstall (#466)
test-msi.ps1 builds the MSI around stub binaries, validates it (ICE) and,
silently: installs into a folder with spaces and a non-ASCII name, upgrades
without naming a folder (it must stay there, one entry in Apps), repairs a
deleted file, uninstalls (files and App Paths gone), and installs to the
Program Files default. Run by a windows-latest job in packaging-lint.yml.
2026-10-10 10:52:04 -04:00
tylerjenningsw de8ed2398a CI: packaging lint checks every Windows .wxs file (#466) 2026-10-10 10:52:04 -04:00
tylerjenningsw 9e6f5191ff Packaging: the Windows installer asks where to install EffectCraft (#466)
A fresh install opens on a destination page (Program Files by default;
Change... browses, with Up and New folder), then a Ready page that shows
the folder before anything is written. An upgrade goes straight to the
Ready page with the installed copy's folder and can't move it; opening the
MSI again offers Repair or Remove. Cancel, finish, files-in-use and error
dialogs are authored too. Silent installs (/qn) show none of it.

The dialogs (installer-ui.wxs) are original, built from Windows Installer's
own controls and events, not the WixUI library, whose MS-RL licence is not
on the allowed list. package.ps1 builds both .wxs files.
2026-10-10 10:52:04 -04:00
tylerjenningsw 7f9a490eac Packaging: Windows upgrades and repairs install into the existing folder (#466)
The MSI records its folder as the App Paths "Path" value of effectcraft.exe
and reads it back with a registry search, so an upgrade, a repair or a
silent reinstall goes where the installed copy already is. A folder passed
on the command line (INSTALLFOLDER=...) still wins, and INSTALLFOLDER is
Secure so a folder chosen in the UI reaches the elevated install. Releases
before this change have no Path value; a later commit finds their folder
from the installed effectcraft.exe.
2026-10-10 10:52:04 -04:00
Brandon ThomasandClaude Opus 5.5 813c7c4650 Bump version to 0.7.0 (from 0.6.0) for release; refresh contributors
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.7.0
2026-10-09 21:02:08 -04:00
BFlat 30aaddf7c2 Merge pull request #459 from storytold/fix/394-408-arabic-flac
Arabic, Hebrew, Indic and Thai names; FLAC preview opens once and off the UI thread (#394, #408)
2026-10-10 01:04:24 +07:00
bflatasticandClaude Opus 5.5 09a5dc357a Audio: a FLAC opens once and off the UI thread; conforming is bounded (#408)
The freeze and quit during a FLAC preview did not reproduce on current
main (Windows 11): a 4-minute stereo and a 10-minute 24-bit 6-channel
96 kHz FLAC played through a RAM preview with a real-time audio pull,
after trimming and zooming, with no stall, no growing queue and memory
bounded by the RAM preview budget; 150 cpal output start/stop cycles
ran clean. Three defects in that path were found and fixed:

- FilmCraft decodes FLAC, MP3, Ogg and AIFF files whole when they are
  opened (8 s and 1.4 GB for the 10-minute 6-channel file). MediaPool
  opened a file outside its lock, so every caller asking before the
  first open finished decoded the whole file too: a preview's frame
  workers, audio feeder, conform and waveform threads at once (8 reads
  at once: 8 decodes). Each path now opens once; the others wait for it.
- AudioPlayback::start mixed its first 50 ms on the calling (UI)
  thread, so the first read of such a file froze the UI for the whole
  decode (audio-only previews start the sound right away). All mixing
  is on the feeder thread now; the device plays silence until the first
  block arrives, and the clock (frames actually played) waits for it.
- Conforming (Settings > Disk > Conformed Audio Folder, on by default)
  held the whole conformed track in memory before writing it, sized by
  the footage duration from the file's header: a header claiming a
  month of audio aborted the app on a failed 995 GB allocation. The
  conformed file is now written a chunk at a time, and footage longer
  than 24 hours is read from its decoder instead of conformed.

Tests: media concurrent_first_reads_open_a_file_once (8 threads: 8
opens before, 1 now; PoolStats::opened), pool
a_header_claiming_a_month_of_audio_is_not_conformed (aborted before),
ui-egui audio starting_audio_does_not_wait_for_slow_footage (start took
the footage's 2 s before), conformed_flac_audio_reads_back_like_the_
decoder, and the robustness test flac_audio_of_any_shape_reads_in_
device_blocks (2 minutes of 24-bit 6-channel 96 kHz, mono 8 kHz,
11.025 kHz, 192 kHz and 8-channel FLAC read in 1024-frame blocks at
48 kHz through and past the end: level, silence after the end, opened
once). FLAC fixtures come from ffmpeg (skipped without it).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 00:53:21 +07:00
bflatasticandClaude Opus 5.5 98a6a3a041 UI: Arabic, Hebrew, Indic and Thai names can be typed and are drawn (#394)
Cause: two things kept Arabic out of a layer name on Windows.
- Windows switches the keyboard layout with Alt+Shift. Releasing Shift
  before Alt left Alt held alone, and the in-window menu bar (#279)
  counted the release as an Alt tap: it took the keyboard focus, the
  rename field closed, and the letters typed next were swallowed by the
  menu bar. The Project panel's rename field lost them the same way.
- The UI fonts are Inter plus the CJK system fallbacks, so Arabic,
  Hebrew, Indic and Thai names that did arrive (pasted, or from a
  project) showed as missing-glyph boxes.

Fix:
- An Alt tap only counts when Alt went down with no other modifier
  held, so Alt+Shift in either release order is no tap; a plain Alt tap
  still focuses the menu bar.
- The UI registers installed system faces for Arabic, Hebrew, Devanagari,
  Bengali, Gurmukhi, Gujarati, Tamil, Telugu, Kannada, Malayalam and Thai
  after the CJK ones, the same way (native only; the web build has no
  system fonts). A face covering several scripts is registered once
  (Segoe UI: Arabic and Hebrew; Nirmala UI: every Indic script).
- The text engine's fallback tries each script's plain platform families
  (Segoe UI, Nirmala UI, Leelawadee UI, Geeza Pro, Kohinoor, Noto…)
  before any covering face, so neither the UI nor a text layer gets a
  calligraphic face that merely sorts first.
- The Timeline's rename field registers `timeline.layer.<id>.nameEdit`.

egui 0.36 shapes each run of one face, so Arabic letters join and a word
reads right to left; it has no bidirectional layout, so the words of a
multi-word Arabic name (and mixed Arabic/Latin) still follow each other
left to right.

Tests: ui_inline_edit a_layout_switch_keeps_the_name_field_and_arabic_
names_are_kept (Alt+Shift both ways in the layer rename, Arabic typed and
stored; Hebrew/Devanagari/Thai in the Project panel; a plain Alt tap
still focuses File; failed before: the field closed), theme
installed_script_fallbacks_draw_arabic_hebrew_indic_and_thai_names
(failed before: replacement glyph for Arabic) and
arabic_words_are_joined_and_read_right_to_left, text
other_scripts_fall_back_to_a_plain_system_family and the script
classification.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 00:53:21 +07:00
BFlat 921f0dbdbe Merge pull request #454 from storytold/fix/397-particle-world-camera
Effects: CC Particle World follows the comp camera (#397)
2026-10-10 00:52:58 +07:00
bflatasticandClaude Opus 5.5 923d978119 Effects: CC Particle World follows the comp camera (#397)
Cause: CC Particle World projected its particles through a fixed camera
(distance 2 layer widths, looking at the layer centre) and never asked the
host for the comp camera, so moving or orbiting a camera layer left the
particles flat on the layer. It also had no Extras > Effect Camera. Separately,
the layer cache key ignored the comp camera and lights, so the effects that
read them (Card Dance / Shatter / Card Wipe Comp Camera, First Comp Light,
Caustics) kept a stale frame when the camera, a light or the layer moved.

Fix: as the Cycore manual describes, Particle World sees its particles through
the comp's active camera layer when there is one (EffectHost::comp_scene now
says whether its camera is a camera layer's, CompScene::camera_layer), through
card3d's comp-camera projection (factored out of Card Dance as comp_proj, with
Proj::view for depth, size scale and screen velocity). Otherwise it uses the
new Extras > Effect Camera: Distance (2), Rotation X / Y / Z (0) and FOV (50)
orbit the world origin; FOV moves the camera so the origin's plane keeps the
scale Distance gives it, so the defaults reproduce the old fixed camera bit for
bit (golden hashes unchanged) and old projects render as before. The Effect
Camera is left out of the simulation cache key, so animating it doesn't
restart the simulation. The plan is shared, so the GPU compositor follows too.
The layer cache folds the comp scene into the key of layers running an effect
in effects::READS_COMP_SCENE (render::comp_scene is shared with the host).

Tests: sim particle_world_follows_the_comp_camera (panning a camera layer moves
the particles, zooming spreads them, the Effect Camera is ignored then, the
default view keeps the Effect Camera, cameras facing away / zero / NaN draw
nothing) and particle_world_effect_camera (missing params = defaults, rotation,
distance and FOV change the view, extremes stay finite, the sim key ignores the
camera); render particle_world_follows_the_comp_camera (a real camera layer
panned 40 px moves the particles left, and the cached render after the move
equals an uncached one: it failed with the old cache key); gpu
particle_world_through_the_comp_camera (CPU vs GPU at 8 / 32 bpc, full and
half resolution). particle_golden / sim_golden unchanged. effects.md
regenerated; parity.md and gaps.md describe the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 00:35:25 +07:00
BFlat e9032c5ce2 Merge pull request #452 from storytold/fix/363-property-pick-whip
Timeline: every property has a pick whip in the Parent & Link column (#363)
2026-10-10 00:34:54 +07:00
bflatasticandClaude Opus 5.5 67b9b20a07 Timeline: every property has a pick whip in the Parent & Link column (#363)
What: each property row that can take an expression (it has a stopwatch) now
has a property pick whip in the Parent & Link column, as in After Effects
CC 2018 and later (automation id `timeline.prop.<uid>.propertyPickWhip`).
Dragged onto another property's name or one of its values, it runs
`prop.pickWhip`: the reference is added as the property's expression, or
replaces the one it has, and the expression is enabled. A drop on one value
picks that dimension (`position[1]`).

The parent, expression and property pick whips share one widget and one
drop path (rubber band, autoscroll, dimension pick); only the expression
pick whip inserts at the cursor of an expression being edited.

Pick whips (expression and property) now also drop on properties in
Effect Controls: its property rows register as targets while a pick whip
is dragged, with their per-dimension value fields.

Why: the last part of #363. Adobe's docs say the Property Link pick whip
adds the expression without enabling expressions first, and a pick whip
outside an edit replaces the whole expression text. No modifier keys for
the property pick whip are documented, so none are implemented.

Tests: `the_property_pick_whip_links_a_property_to_another` (ui_inline_edit)
drops it on a property's name, on one of Position's values, on a property
with a disabled expression (replaced and enabled), on an Effect Controls
slider and one value of a point, and on itself (no change). It fails
before the change; with Effect Controls' targets removed it fails at the
slider. Looked at the result with the headless snapshot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 00:28:22 +07:00
BFlat 4b7bd135ec Merge pull request #449 from storytold/fix/410-411-412-exr-ocio
EXR beauty layer, Interpret Footage Preserve RGB, OCIO v2 transforms for Blender configs (#412, #411, #410)
2026-10-10 00:27:39 +07:00
bflatasticandClaude Opus 5.5 492fa6de0a Effects: OCIO custom configs read Blender's v2 transforms and warn (#410)
Cause: the config reader didn't know ColorSpaceTransform or
ExponentWithLinearTransform and passed colours through them silently.
Blender's Linear Rec.709 -> sRGB therefore stayed in its E-white XYZ
reference space (the blue cast) without the sRGB curve. Display
colorspaces, view transforms, v2 views, looks, AllocationTransform,
BuiltinTransform and .spi1d / .spimtx files were missing too, and
nothing told the user.

Fix (from the OCIO v2 documentation and the CLF "moncurve", not OCIO's
source): ExponentWithLinear, ColorSpace, Look, DisplayView, Allocation
and Builtin transforms (identity, ACES AP0/AP1 matrices, ACEScct /
ACEScc, the sRGB / P3 / Rec.1886 / gamma / PQ displays). RangeTransform
now clamps and takes one-sided ranges. Display colorspaces meet the
scene reference through the default view transform. Added v2 views,
view looks, the looks section and .spi1d / .spi3d / .spimtx files. A
conversion is built once as a flat list of steps (Config::path /
display_path) for both the CPU effect and the GPU colour program (new
moncurve and clamp ops and the ACEScc curve; a program over the
kernel's 255 ops renders on the CPU). Named-transform resolution is
limited to 16 levels and 4096 steps, and YAML nesting to 64 levels, so
hostile or looping configs stop. Transforms that are still unsupported
(Grading Primary / Tone, ACES output, HLG…), unreadable configs and
unknown spaces are now reported: a warning under the effect in Effect
Controls, and the new effect.warning query.

Tests: ocio_config::blender_like_config_converts_linear_rec709_to_srgb
(CST, display colorspaces, v1 / v2 views, allocation, looks, builtin,
range, loops), moncurve_matches_srgb_and_inverts, builtin_styles,
yaml_nesting_is_bounded, ocio::spi_files_parse,
tests_effects::ocio_effect_warns_about_unsupported_config_transforms,
gpu tests_fx_lut::ocio_custom_config_v2 (GPU = CPU).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 23:57:12 +07:00
bflatasticandClaude Opus 5.5 d6de68b20d Import: Interpret Footage ▸ Preserve RGB keeps the file's values (#411)
Cause: float footage (OpenEXR, float TIFF) was always sRGB-encoded on
decode, and Interpret Footage could not turn that off, so an OCIO effect
on the layer converted values that were already encoded.

Fix: After Effects' Interpret Footage ▸ Color ▸ Preserve RGB
(Footage::preserve_rgb; file.interpretFootage {"preserveRgb": true}; a
checkbox in the Interpret Footage form; Remember / Apply Interpretation
carry it). The decoder keeps float values linear, and the working-space
conversion from the colour profile and Interpret As Linear Light don't
apply, so the file's values enter the composition unconverted. The media
cache key includes the flag, so both interpretations decode separately.

Tests: pool::preserve_rgb_reads_float_exr_values_unconverted,
tests_color::preserve_rgb_footage_is_not_converted (managed, linearized
and not), tests_proxy::guess_alpha_invert_and_linear_light (command,
render, Apply Interpretation).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 23:57:12 +07:00
bflatasticandClaude Opus 5.5 698ade94ac Import: layered EXRs show the beauty pass, never Cryptomatte (#412)
Cause: a multi-layer OpenEXR without an unnamed RGB layer showed its
first colour layer unless one was named combined / beauty / rgba. A
Blender compositor File Output names the beauty pass "Image", and its
Cryptomatte layers (lowercase r, g, b, a) sort first, so the picture was
CryptoMaterial00.

Fix: the picture is the unnamed RGB layer, else the beauty pass by the
last part of its name (Combined, Beauty, Image, RGBA, RGB, Color), else
the first colour layer that isn't a data pass (Cryptomatte, depth, mist,
normals, vectors, positions, IDs). A layer without R, G and B shows its
own first channel as grey. After Effects shows a file's main RGBA and
leaves the other layers to EXtractoR, which is unchanged.

Tests: exr_channels::layered_exr_shows_the_beauty_pass_not_cryptomatte
(the reported Blender layout, ViewLayer.Combined, no beauty name,
data-only files).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 23:57:12 +07:00
BFlat 176cddcdad Merge pull request #444 from storytold/fix/415-416-417-text-fidelity
Text and viewer fidelity: glyph seams, overlay fringes, text raster scale, viewer minification (#415, #416, #390, #417)
2026-10-09 23:56:18 +07:00
bflatasticandClaude Opus 5.5 9d85d4f116 Viewer: Auto resolution follows the magnification; minified frames are averaged (#417)
Cause: Auto dropped to Half below 75 % magnification, so text between
50 % and 75 % was rendered with fewer pixels than the screen shows and
looked soft. Below 100 % the frame texture was minified with a plain
bilinear sampler and no mip levels (Full at 46 % skipped most frame
pixels), so edges stair-stepped.

Fix:
- Auto renders the pixels the magnification needs, as in After Effects:
  the coarsest whole factor that still gives every screen pixel a
  rendered one (Full above 50 %, Half down to 33.3 %, Third down to
  25 %, Quarter below).
- GPU frames: the display texture carries a full mip chain, each level
  the 2 x 2 mean of the one above (display.wgsl `mip`, same bind group
  layout as `display`), sampled trilinearly with Viewer Zoom Quality
  More Accurate (Faster keeps the nearest pixel of the full-size frame).
- CPU frames (egui-wgpu textures have no mips): uploaded averaged down
  by the whole factor that leaves about one texel per screen pixel, the
  same box average as the texture-limit fit (#201), re-uploaded when the
  magnification calls for another factor; drawn with texture coordinates
  that keep each texel over its pixels when the size isn't a multiple.
  Show Channel / exposure textures follow.

Tests: gpu tests::display_frames_are_mipmapped (level count and level 1
= 2 x 2 means of level 0), viewer_tools
auto_resolution_follows_the_magnification (60 % was Half) and
minified_frames_keep_their_texels_in_place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 23:17:33 +07:00
bflatasticandClaude Opus 5.5 94e4afe8de Text: text layers are always rasterised at their on-screen scale (#390)
Text is vector in After Effects and stays sharp at any Scale. Here a
text layer was drawn at its font size and the bitmap resampled by Scale
unless Continuously Rasterize was on, so text grown with Scale went soft.
This reworks #200 (reverted in 29c4d3a for kinetic_typography).

Why kinetic_typography failed: its time remap key at 1.9 s sits on
frame 23 (1.917 s), so main-comp 0.3 s shows the precomp at 1.6026 s,
but the test compared it with the precomp rendered at 1.6 s. The moving,
motion-blurred title already differed by 0.89 of the 1.0 mean-difference
budget on main (0.987 in all); with continuous rasterisation the "Sub"
line, whose expression scale is 84.00 % at 1.6 s and 84.105 % at
1.6026 s, falls on either side of the 2^-1/4 quarter-octave step and was
drawn at 0.84 at one time and at 1.0 at the other, pushing the total to
1.075.

Fix: raster_scale treats text layers as always continuously rasterised
(shape layers and vector footage still follow the switch), and rounds
the quarter-octave step up past rounding noise only: an unscaled layer
at a fractional position measured its scale as 1 + 1e-15 and was
rasterised at 2^1/4, softening unscaled text. The QA test now compares
the remapped precomp with the precomp at the time the remap really
shows (read back from the property): difference 0.63, all of it the
title's motion blur, the Sub line identical.

Tests: tests_collapse::text_scaled_up_stays_sharp_without_the_switch
(from #200: 10 px text at 800 % matches 80 px text),
tests_collapse::unscaled_text_rasterises_at_the_output_scale (fails
without the noise tolerance), automation qa kinetic_typography.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 23:17:32 +07:00
bflatasticandClaude Opus 5.5 36ef45077a Styles: overlays keep anti-aliased edges clean (#416)
Cause: the interior styles were blended over the layer body with the
content's coverage folded into their opacity. On an edge pixel at 50 %
coverage a Gradient or Color Overlay went over the half-transparent body
as a half-transparent layer, so the result kept a quarter of the layer's
own fill and its alpha rose to 0.75: a light rim (the white fill
showing through) on styled text.

Fix: blend_clipped composites the interior styles (Gradient Overlay,
Color Overlay, Satin, Inner Glow, Inner Shadow and the inside of
Stroke) onto the layer within its shape: the body is divided by the
content coverage, blended with the style at its own opacity, and
multiplied back, so the alpha is kept and an edge pixel mixes the style
with the layer's colour inside the shape only, as a clipped layer does.
Fill Opacity and Blend Interior Styles as Group behave as before.

Tests: tests_styles::overlays_leave_antialiased_edges_clean (a masked
square with half-covered edge columns; Color Overlay, Gradient Overlay
and an inside Stroke now match a plain layer of the overlay colour;
Color Overlay's edge was 0.41 red against 0.23 before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 23:17:32 +07:00
bflatasticandClaude Opus 5.5 88e2e6470a Text: touching glyphs join without a seam (#415)
Cause: each glyph's fill was rasterised and composited on its own, so
where two glyphs share an edge (the crossbars of "ff") two half-covered
pixels composited to 0.75 coverage instead of 1: a thin light seam,
where After Effects draws the line as one outline.

Fix: consecutive fill passes of the same opaque colour, without Blur,
under Normal Inter-Character Blending are rasterised as one coverage
mask (the glyphs' outlines in layer space, wound the same way so
overlaps add up under NonZero). Characters of other colours, translucent
or blurred ones, strokes and the other blending modes are drawn one by
one as before.

Tests: text::tests::touching_glyphs_join_without_a_seam (two glyphs
sharing an edge mid-pixel: 0.75 before, 1.0 now; opposite windings and
overlaps stay filled; other colours and Multiply still composite one by
one).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 23:17:32 +07:00
BFlat 2a460986ee Merge pull request #445 from storytold/fix/398-tr-lookup
i18n: interface translations are looked up by hash (#398 follow-up)
2026-10-09 23:16:12 +07:00
bflatasticandClaude Opus 5.5 ea5dfae95f i18n: interface translations are looked up by hash, not by scanning the catalog (#398)
tr() scanned the whole catalog (about 380 rows) for every string a panel
draws, every frame, in Chinese. Each catalog is now indexed once, on
first use, in a HashMap keyed by the English source; English and
languages without rows still skip the lookup. The catalogs already have
no duplicate sources (catalogs_are_unique_and_keep_the_english_shape),
so the index finds the same rows.

Test: the i18n tests (pass-through, tr_args, catalog shape).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 23:15:54 +07:00
BFlat 4fad3e7af4 Merge pull request #436 from storytold/fix/393-370-timeline-ux
Timeline and viewer: resize cursors, time display toggle, pick whip, expression field, Tab, property menu (#393, #370, #362, #363, #371, #361, #407)
2026-10-09 23:10:56 +07:00
bflatasticandClaude Opus 5.5 79bf5a6b59 Timeline: property names have the property menu (#407)
Right-clicking a property's name in the Timeline did nothing, so
Position's dimensions could be separated only from the Graph Editor.

As in After Effects, it now opens the property's menu: Reset, Edit
Value…, Separate Dimensions (Position and, once separated, X / Y / Z
Position, checked), Add / Remove Expression and Add Property to
Essential Graphics. Only Position separates, as in After Effects
(Scale stays one property).

`prop.reset` without `default` used to leave the value as it was: it
now gives the property its own default (an effect parameter's, or a
Transform property's, shared with Layer ▸ Transform ▸ Reset), which
also makes the Properties panel's Reset reset the values. `prop.set
{layer, prop}` without `value` opens the value dialog, like
`keys.set`.

Tests: timeline_property_names_have_the_property_menu
(ui_blank_context_menus), reset_gives_a_property_its_default (engine).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:52:56 +07:00
bflatasticandClaude Opus 5.5 9c9e954ee2 Timeline: Tab moves to the next value field (#361)
Tab in a value being typed (hot text) dropped the keyboard: egui moved
the focus to the next widget, and the inline editors asked for the
focus again every frame, which also reset what keys a field keeps for
itself.

As in After Effects, Tab now commits the value and types in the next
value field of the same panel (the next dimension, then the next
property's), Shift+Tab the previous one. The hot-text fields of each
panel register in drawing order; the dock tags each panel's Ui so the
order stays within it. `keep_focus` asks for the focus only when the
field doesn't have it.

Tests: tab_commits_a_value_and_moves_to_the_next (ui_inline_edit:
Timeline and Effect Controls).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:52:56 +07:00
bflatasticandClaude Opus 5.5 0171eb1e2e Timeline: the expression field grows while typing and drags taller (#371)
The inline expression field took its height from the saved
expression, so lines typed into it ran over the rows below until the
expression was committed.

Its height now comes from the text being typed (up to 8 lines, as
before), and its bottom edge drags it to any height in whole lines, as
in After Effects; the dragged height is kept per property in the
Timeline state (`expr_lines`).

Tests: the_expression_field_grows_while_editing_and_drags_taller
(ui_inline_edit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:52:56 +07:00
bflatasticandClaude Opus 5.5 6fadae2818 Timeline: the pick whip picks the dimension it is dropped on (#363)
Dropping the expression pick whip on one of a property's values (the X
of Position) referenced the whole property. As in After Effects, a
drop on one value now picks that dimension: `transform.position[0]`;
a drop on the name still picks the whole property.

`prop.pickWhip` takes `target.dimension`.

The rest of #363 is unchanged: inserting at the cursor landed with
#284, and After Effects' property pick whip in the Parent & Link
column is left for later.

Tests: pick_whip_picks_one_dimension_of_the_target (engine),
the_expression_pick_whip_picks_the_dimension_it_is_dropped_on
(ui_inline_edit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:52:56 +07:00
bflatasticandClaude Opus 5.5 639342a5e7 Timeline: the pick whip scrolls the layers at their edges (#362)
A pick whip dragged to the top or bottom of the Timeline's layers left
them where they were, so a property out of view couldn't be picked.

While a pick whip (expression or parent) is held within a row of the
top or bottom edge of the layers, or past it, the layers scroll that
way, faster the deeper, as in After Effects.

Tests: the_pick_whip_scrolls_the_timeline_at_its_edges
(ui_inline_edit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:52:56 +07:00
bflatasticandClaude Opus 5.5 0b2ef8e584 Timeline: Ctrl/Cmd-click on the time display toggles Timecode / Frames (#370)
The Time Display Style could only be changed in Project Settings. As in
After Effects, Ctrl-click (Cmd-click on macOS) on the current-time
display of the Timeline or the Composition panel now toggles it between
Timecode and Frames (Feet + Frames goes to Timecode). It runs
`file.projectSettings {timeDisplay}`, so it is one undo step.

Under the Timeline's display the other style shows now (frames under
timecode, timecode under frames), as in After Effects; it always showed
frames.

Tests: ctrl_click_on_the_time_display_toggles_timecode_and_frames
(ui_inline_edit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:52:56 +07:00
bflatasticandClaude Opus 5.5 fcf3bb5dca Viewer: resize cursors follow each handle and the layer's rotation (#393)
Every bounding-box handle in the Composition viewer showed the
north-west / south-east resize cursor, so two corners and all four
edges showed the wrong direction.

Each handle now takes the cursor along the direction it scales, worked
out from the on-screen box: a corner points between its two edges (the
box's diagonal whatever its proportions), an edge handle across its
edge. The cursors turn with the layer's rotation, as in After Effects.

Tests: handle_cursors_follow_each_handle_and_the_layer_rotation
(ui_viewer: a wide box at 0°, 90° and 45°).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:52:56 +07:00
BFlat 609ef9cacb Merge pull request #429 from storytold/fix/413-414-export-cache
Export: Render Queue jobs keep a layer cache and render on one compositor (#413, #414)
2026-10-09 22:51:10 +07:00
Brandon ThomasandClaude Opus 5.5 e3873ae92f CI: pushes to main no longer cancel the FreeBSD / Windows ARM64 run in progress
Merges land faster than the hour-long FreeBSD job, so every main run was
cancelled and main went unverified. Only PR pushes cancel now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:37:20 -04:00
bflatasticandClaude Opus 5.5 8c4a3cbfe0 Export: a Render Queue job renders every frame on the project's renderer (#414)
Cause: pipeline::Cx rendered with Backend::Auto, which picks the CPU or the
GPU compositor for each top-level frame from measured frame times
(render::AutoPick: warm-up on both sides, a re-probe every 48 frames). The
two compositors differ slightly, so frames came out differently depending
on which side the timings favoured in that run, and two GPU exports of the
same frames differed.

Fix: the job renders every frame on the project's renderer, as After
Effects renders a job with one renderer: Backend::Gpu with Mercury GPU
Acceleration (frames the GPU declines still render on the CPU, as they do
in every GPU render), Backend::Cpu with Mercury Software Only. The viewer
and previews keep Auto. Docs (architecture, parity, web) say so.

Tests: render_settings::a_job_renders_on_one_compositor exports ten frames
with a stand-in GPU compositor that keeps Auto history and draws solid
green: with Mercury GPU Acceleration every frame is green and two runs are
identical (with Auto the warm-up sent frames to the CPU: it fails), with
Software Only the stand-in is never asked. The web worker GPU job test's
comment no longer describes Auto.

Diagnosis by @pat-the-wizard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:26:17 +07:00
bflatasticandClaude Opus 5.5 1ccf7cb910 Export: Render Queue jobs keep a layer cache, so static layers render once (#413)
Cause: pipeline::Cx::render_at built the job's Renderer without a layer
cache, so every layer of every frame rendered from scratch (source, masks,
effects, styles), and the GPU compositor, whose uploads are keyed by the
cached buffers, uploaded every layer again on every frame.

Fix: each job (Cx) owns a LayerCache, the cache type the viewer uses, with
its default budget (1 GiB, the default Settings > Memory & CPU layer cache),
shared by the job's parallel frames and dropped with the job. With deferred
GPU readbacks it holds the accelerator's miss gate, as the job workers'
caches do. Cx::renderer builds the job's renderer (options, accelerator,
cache) for render_at and the test.

Tests: pipeline::tests::layers_render_once_per_job exports ten frames of a
solid animated only in Transform: the cache misses of the first frame are
the only ones (it fails without the cache: no layer is cached), and every
frame equals the frame rendered without the cache.

Diagnosis by @pat-the-wizard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:26:16 +07:00
BFlat f08d090cd8 Merge pull request #433 from storytold/fix/402-no-row-separators
UI: no parameter row separators, as in After Effects (#402 follow-up)
2026-10-09 22:14:05 +07:00
bflatasticandClaude Opus 5.5 82f0cd9668 UI: Effect Controls and the Timeline have no parameter row separators, as in After Effects (#402)
#402 made Effect Controls' rows compact and aligned, as After Effects
draws them, and also added thin separator lines between parameter rows
in Effect Controls and the Timeline, with a Settings > Appearance > Show
Parameter Row Separators preference. After Effects has neither, so the
lines, the preference and the Tokens field go; the compact rows, the
alignment and the automation ids stay.

Saved settings with the old key load as before (unknown keys are
ignored).

Test: ui_parameter_spacing keeps the row geometry, hit target and
nesting tests; the separator tests go with the feature.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:13:31 +07:00
Brandon Thomas 9144eef269 Merge remote-tracking branch 'origin/main' into integrate5
# Conflicts:
#	docs/preferences.md
2026-10-09 10:33:41 -04:00
Brandon ThomasandClaude Opus 5.5 f1a31fb9eb i18n: drop the Ukrainian row for the removed Ease Presets panel (#401 follow-up)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 10:33:17 -04:00
BFlat be676f90ec Merge pull request #401 from dmatviichuk/i18n/ukrainian
i18n: add Ukrainian (uk) menu translation
2026-10-09 21:32:04 +07:00
Brandon Thomas 4047ea94ea Merge pull request #398 from FeiLiuEM/ui-i18n
i18n: translate the interface below the menu bar (panels, dialogs, tooltips)

# Conflicts:
#	crates/ui-egui/src/lib.rs
#	docs/preferences.md
2026-10-09 10:22:32 -04:00
bflatasticandClaude Opus 5.5 07e163db64 i18n: Ukrainian drops the Ease Presets panel entry, which became a ScriptUI panel (#395, #404)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:22:15 +07:00