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.
Get-ItemProperty throws on a malformed Uninstall entry, which aborted the
upgrade check on a real Windows 10 machine. Failures now print their message.
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.
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.
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. v0.6.0 and
earlier have no Path value and upgrade in Program Files, where they are.
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
#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>
The Ease Presets panel (#254) is modelled on a third-party panel, so,
as with third-party tools in After Effects, it isn't core any more: it
is `extensions/scriptui-panels/Ease Presets.jsx`, a ScriptUI panel
bundled as an optional extension (Window ▸ Ease Presets.jsx), written
only against the public scripting API. Clicking a preset eases every
pair of neighbouring selected keyframes in one undo step (per
dimension, along the motion path for spatial properties, the keys'
other sides as they play); the fields edit the working curve, which the
panel draws; From Keys, Save, Rename and Delete as before. The twelve
built-in curves are unchanged.
Scripting API fixes it needed (After Effects parity):
- `app.settings` lasts between runs (`script_settings.json` in the
settings folder; `script.settings.get` / `script.settings.save`).
- Per-key methods (`setTemporalEaseAtKey`, `setInterpolationTypeAtKey`,
`removeKey`, the continuous / auto-Bezier / roving setters) no longer
change the user's key selection (they selected the key they edited).
- `keyInTemporalEase` / `keyOutTemporalEase` report linear and
auto-Bezier sides as they play, `keyIn/OutSpatialTangent` the
tangents the motion path plays.
- `selectedProperties` includes the properties of selected keyframes.
- `isInterpolationTypeValid` is false for Linear / Bezier on values that
only hold.
Removed from core: Window ▸ Ease Presets, the `keys.easePreset.*`
commands, `engine::ease_presets` and the `ease_presets.json` store,
`keyframe::EaseCurve` (nothing else used it) and the egui panel. User
presets saved by the v0.6.0 panel move into the panel's app.settings
section the first time settings load (the old file is left as is).
Docs: plugins.md (bundled extensions), parity.md, gaps.md G8, agents.md.
Test: app_settings_last_between_runs,
per_key_methods_keep_the_key_selection_and_report_eases_as_they_play,
spatial_tangents_are_reported_as_the_path_plays (each fails before),
the_ease_presets_panel_eases_every_selected_pair_in_one_undo_step,
a_typed_curve_scales_to_each_dimension_and_follows_motion_paths,
user_presets_save_rename_delete_and_persist,
presets_saved_by_the_old_core_panel_carry_over (script, through the
panel's controls), ease_presets_panel_eases_the_selected_keys_from_its_list
(UI: the panel docks and a click on a preset eases the keys).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EffectCraft's extension points compared with After Effects' (public
user guide and Scripting Guide) are now in docs/plugins.md, which
becomes "Plug-ins and extensions": scripts, ScriptUI panels and effect
plug-ins, with a short guide and an example panel for writing your own.
ScriptUI panels already came from `Scripts/ScriptUI Panels/` in the
settings folder, were listed at the bottom of the Window menu and
docked as tabs. What was missing: After Effects reopens the panels
that were open when it quit. Now the open panels are remembered in
`scriptui_panels.json` in the settings folder (a panel is added when
it opens and dropped when its tab or its script closes it; quitting
leaves the list), and `window.restoreScriptPanels` opens them again;
the desktop and web apps run it at start-up. A panel whose script is
gone or fails is reported and forgotten, and the others still open.
Where a panel was docked isn't kept, as workspace layouts aren't saved
between launches.
Gaps recorded in plugins.md and gaps.md G8: the Startup and Shutdown
script folders, keyboard shortcuts for scripts, ExtendScript's
`#include` / `#target` directives and `.jsxbin`.
Test: open_panels_open_again_at_the_next_launch (fails before: no
restore command), a_panel_dropped_in_the_folder_is_listed_and_opens_as_a_panel
(script), panels_open_at_quit_dock_again_at_launch (UI: the restored
panel docks with its controls; closing the tab forgets it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Tools bar's Snapping options menu (#265) offered per-feature
toggles (Edges, Corners, Centers, Anchor Points, Mask and Shape Path
Points) that After Effects' Snapping menu doesn't have. It now has the
two After Effects has:
- Snap Edges Extended (as before).
- Snap to Features in Collapsed Compositions and Text Layers: the
visible layers inside a precomp layer with Collapse Transformations
on are snap targets too, mapped into the outer comp (points always,
edge lines while the precomp layer is unrotated); a snap highlights
the precomp layer. Nested collapsed precomps are followed eight deep.
Every layer feature snaps again (corners, edge midpoints, centre,
anchor point, mask and shape path points); the snapping engine itself
is unchanged. `view.snappingOptions` takes `edgesExtended` and
`collapsedFeatures` (and `toggle` of either); the old keys are refused.
Test: collapsed_precomp_features_snap (fails before: no such option),
snapping_options_command_sets_and_toggles (the per-feature keys are
refused), snapping_options_pick_the_targets (AE behaviour kept: edges
extended or not, hidden layers, guides only), and the Snapping menu UI
test (only the two options are listed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>