Problem: app.scheduleTask(code, delay, true) queued the task and then
skipped it (eval_with_tasks: `if repeat { continue; }`), so repeating
tasks - what ScriptUI panels use for polling and refreshing - never ran.
One-shot tasks scheduled by handlers or in the Script Console never ran
either.
Fix: tasks live per script context (crates/script/src/tasks.rs) with a
due time. Frontends run the due ones every frame through the new
ScriptUi::run_tasks hook (the egui app does; agents and headless hosts
use the `script.runTasks` command). Script-host threads get a Tick
message, inline hosts (Script Console, web) run in their kept context.
A repeating task runs every `delay` ms until app.cancelTask, until it
throws (reported once, then cancelled), or until its owner goes: the
script's last window closing, or - for scripts without windows and the
console - the project being closed or replaced (ScriptUi::project_epoch).
Bounds: at least 50 ms between runs, at most 64 pending tasks per
script, one run per pass (no catch-up bursts), task code nested too
deeply is refused, the engine's loop and recursion limits apply. Ticks
only reach the hosts of the session that scheduled them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: `$.evalFile` always threw, and `typeof BridgeTalk` was
`undefined`, so frameworks such as DuAEF failed during initialisation
when they load their modules or ask which application they run in.
Fix: `$.evalFile(file[, timeout])` evaluates a script file in the global
scope and returns its last value. Relative paths resolve against the
folder of the file being evaluated (the running script's for the
outermost call, else the project's); reads follow the same file gate,
16 MiB size cap and 32-deep nesting limit as `#include` (#759, shared
`scriptfile.rs`), and the bracket-depth check runs before parsing.
`BridgeTalk` names the host honestly (`appName` "effectcraft", its
version, `appSpecifier`, `isRunning` / `getSpecifier` / `getStatus` /
`getTargets` for ourselves only); `send()` throws a clear "not
supported" error instead of messaging other applications.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: After Effects scripts call layers and property groups as a
shorthand for .property(nameOrIndex) (layer("ADBE Effect Parade"),
fxGroup(1), effect("Slider Control")("Slider")); Duik and many other
scripts use it throughout. EffectCraft's objects were Proxies around
plain objects, so each such call failed with "TypeError: not a callable
function".
Fix: Layer, PropertyGroup, MaskPropertyGroup and Property objects are
now Proxies around an arrow function carrying the object's state and
prototype (own name/length removed so the object's getters show). The
apply trap returns self.property(arg), so a call matches .property():
null when absent, Property returns null, and a missing name is the same
script error. instanceof, attribute access and script results are
unchanged; new on these objects stays a TypeError.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After Effects' `app.project.workingGamma` (2.2 or 2.4, ignored with a working space) sets
the curve an unmanaged project uses: at 2.2 the piecewise sRGB curve, at 2.4 a pure 2.4
power curve. EffectCraft always used sRGB, so linear EXRs (Interpret As Linear Light) from
a 2.4 project came out darker in the shadows (0.18 → 0.461 instead of 0.490).
- `ProjectSettings::working_gamma` (default 2.2, serde default for old projects).
- The colour pipe's unmanaged curve is the Working Gamma's (sRGB, or Rec. 709's pure 2.4
curve): Interpret As Linear Light encodes untagged footage with it and Blend Colors Using
1.0 Gamma linearizes with it; it is part of the pipe's cache key. The GPU path takes the
same conversions.
- `file.projectSettings {workingGamma: 2.2|2.4}` (other values are refused, as in AE),
`app.project.workingGamma` in scripts and a Working gamma choice in Project Settings.
Test: render test with linear 0.051 / 0.18 / 0.5 footage (0.290 / 0.490 / 0.749 at 2.4,
0.250 / 0.461 / 0.735 at 2.2, the values measured in AE), linear blending at 2.4, a working
space ignoring the setting, and the cache key.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
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 <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>
Composition Settings ▸ Advanced has both options (they were missing from the model, the
renderer and the dialog):
- Preserve frame rate: nested in another comp, the comp shows only its own frames
(`EvalCtx::nested_time` floors the precomp layer's source time to the nested comp's frame
rate; used for the nested frame, its proxy, aux channels, collapsed transformations and the
viewer's internal wireframes), and the Render Queue renders it at its own rate whatever the
Frame Rate override (`RenderSettings::rate`).
- Preserve resolution: nested in a comp rendered at Half / Third / Quarter, it renders at full
size and the buffer's scale places it.
- `comp.settings {preserveFrameRate, preserveResolution}`, `comp.info` reports them, scripts
read and set `CompItem.preserveNestedFrameRate` / `preserveNestedResolution`, Pre-compose
copies them, and the dialog registers `dialog.comp.preserveFrameRate` /
`dialog.comp.preserveResolution`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The getters were registered as keySpatialInTangent / keySpatialOutTangent,
so scripts using the documented After Effects names failed with
"TypeError: not a callable function" (setSpatialTangentsAtKey and the
other keySpatial* methods already use AE's names).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adding a property already in the Essential Graphics panel adds a mirror
(EgKind::Mirror: own name and place, one value in the comp and in every
instance; editing, reverting or pushing either acts on both). A property
control can also drive further properties of the comp (links follow the
main property both ways and take instance values). essential.addMirror,
essential.linkProperty, essential.unlinkProperty; panel badges and menu;
ExtendScript addToMotionGraphicsTemplate(As), canAddToMotionGraphicsTemplate,
motionGraphicsTemplateName, controller count/name and
exportAsMotionGraphicsTemplate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>