Repository navigation
Add shape path, mask, render queue and 3D tools; fix broken and data-losing tools - #5
Merged
Merged
Conversation
Clears 8 advisories (5 high) reaching the tree through @modelcontextprotocol/sdk's transitive dependencies - hono, @hono/node-server, ajv, fast-uri, body-parser. SDK moves 1.0 -> 1.30. Semver-compatible only. Build is clean and the tool count is unchanged at 67 before the shape work lands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the shape-layer capability the server was missing. Previously it
could only create parametric Rect/Ellipse/Star primitives - never an
actual editable path, and none of the shape operators.
New tools (67 -> 71):
create_path editable bezier path; creates a shape layer or
adds to an existing one
get_path reads vertices/tangents/closed back, including
every keyframe when the path is animated
set_path_keyframes animates a path
add_shape_operator all 12 operators with friendly property names
Notes on three traps this encodes, all verified against AE 26.5:
- Path keyframes must be written one at a time with setValueAtTime().
The bulk setValuesAtTimes() does not work on Shape-valued properties,
only on numeric ones.
- addProperty() on a PropertyGroup invalidates references already held
to that group's other children. Adding a Fill/Stroke after creating a
path group makes the earlier handle throw "ReferenceError: Object is
invalid" on something as harmless as .name, so the path name is read
into a plain variable before any siblings are added.
- Pucker & Bloat is "ADBE Vector Filter - PB". The plausible-looking
"ADBE Vector Filter - PuckerBloat" is not a valid match name and
hard-crashes After Effects with no exception and no dialog. Operator
names are validated in TypeScript so an invalid one can never reach
AE.
Gradient tools document that color stops cannot be set by scripting:
After Effects exposes ADBE Vector Grad Colors as NO_VALUE.
Also replaces every English display-name property lookup in
add_shape_layer with match names - property("Size") returns null on a
non-English After Effects and the script then dies - and exposes
rectangle roundness, which was already in the tree but unused.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The server exported 81 script generators but only wired 71 of them to MCP tools. These ten were fully implemented and simply unreachable: list_project_items set_active_composition set_proxy remove_proxy get_current_time set_current_time snap_to_marker get_nearest_marker navigate_markers batch_set_expressions No new ExtendScript - this is schema and registration only. Tool count 71 -> 81, orphaned generators 10 -> 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The server had no render queue coverage at all. render_frame grabs a
single still, so there was no way to actually deliver anything.
add_to_render_queue queue a comp, with templates, output path
and time span
list_render_queue every item with status, span and output
modules - poll this for progress
list_render_templates the render settings and output module
presets configured in this AE install, so
renders reuse existing presets by name
set_render_queue_item retarget output, templates, span, or
whether an item is queued
remove_from_render_queue drop one item or clear the queue
control_render start / stop / pause / resume / showWindow
queue_in_ame hand off to Adobe Media Encoder
Two behaviours callers need to know, both documented on the tools:
- RenderQueue.render() is synchronous. After Effects blocks until the
whole queue finishes, so control_render "start" will usually exceed
the bridge's command timeout on any real render. queue_in_ame returns
immediately because AME renders out of process, which makes it the
better choice for long jobs.
- The output module decides the container. Passing a .mov path while
the module is set to H.264 yields a .mp4, so the resolved path is
returned rather than echoing what was asked for.
RQItemStatus is mapped to readable strings; the raw enum crosses the
bridge as an opaque number.
Verified end to end against AE 26.5: queue, edit, inspect, pause,
resume and clear. render() and queue_in_ame were deliberately not
executed - both tie up the machine.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Masks had no coverage at all. They reuse the Shape object that shape
layer paths use, so path building, validation and tangent handling are
shared with shapeGenerators rather than reimplemented - validatePathPoints,
zeroTangents and emitShapeObject are now exported for that.
add_mask add a mask, optionally with a bezier path and
full settings in one call
list_masks every mask with mode, feather, opacity,
expansion and whether its path is animated
get_mask_path read a path back, keyframes included
set_mask_path replace a path
set_mask_keyframes animate a path
set_mask_properties change settings without touching the path
delete_mask remove a mask
Match names verified empirically against AE 26.5, since the scripting
guide documents only ADBE Mask Parade for this group:
ADBE Mask Shape path (Shape-valued)
ADBE Mask Feather feather ([x, y] - a single number is applied to
both axes for convenience)
ADBE Mask Opacity opacity
ADBE Mask Offset expansion - note the name does not match the UI
set_mask_properties temporarily unlocks a locked mask, applies the
changes, then restores the requested lock state. A locked mask silently
rejects edits otherwise, which is an easy way to lose work without an
error.
Mask modes are validated in TypeScript against the MaskMode enum, so an
invalid mode is rejected before any script reaches After Effects.
Verified end to end against AE 26.5: create with a bezier path, list,
read back, animate with 3 keys, edit through a lock, delete.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ht tools
3D coverage was two shallow tools - add_camera_layer took type and zoom,
add_light_layer took type, colour and intensity. Live After Effects
exposes 15 camera and 14 light properties.
set_comp_renderer switch the comp's 3D renderer
get_3d_info renderer, available renderers, and a layer's
current Material/Camera/Light/Geometry values
set_3d_layer 3D switch plus orientation and rotation X/Y/Z
set_material_options all 17 material properties
set_geometry_options extrusion/bevel and plane curvature/segments
set_camera_options all 15 camera properties
set_light_options all 14 light properties
The hard part is that layer.property(matchName) returns a live object
even when the property cannot be set - setValue then throws "the
property or a parent property is hidden" - and .active, .elided and
.enabled all report identical values in the working and failing cases.
There is no flag to test first.
So setters apply properties individually and report applied[] and
failed[] rather than aborting on the first refusal, and each hidden
failure is annotated with availableUnderRenderers naming the renderers
that actually expose it. Extrusion additionally fails fast with an
actionable message, since it is the common case.
Renderers are NOT a hierarchy - each exposes a different subset,
measured on AE 26.5:
Blur Level, Iris, Highlights classic only
Depth of Field, Focus, Aperture classic + advanced
Focus Area Width, Shadow Colour advanced only
Appears in Reflections cinema4d only
A first pass told callers to switch to "advanced" while they were
already on it. Naming the specific renderer per property replaced that.
Note that "ADBE Advanced 3d" is the CLASSIC renderer despite its name,
and is the default for new comps. Two more name traps encoded: cone
feather is "ADBE Light Cone Feather 2", and highlight saturation is
"ADBE Iris Hightlight Saturation" - Adobe's own typo, which must be
spelled wrong to work.
Verified end to end against AE 26.5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…al easing
Found by a full regression sweep over every non-destructive tool.
Data loss in offset_keyframes and scale_keyframe_timing
------------------------------------------------------
Both removed EVERY key first and then re-added only the ones they could
place, silently dropping the rest while still reporting success.
- scale_keyframe_timing with scale missing -> scaleFactor undefined ->
every new time NaN -> `NaN >= 0` false -> all keys removed, none
re-added, response "keyframesScaled: 2". Passing `factor` instead of
`scale` - an easy slip - wiped the animation.
- scale 0 collapsed every key onto one frame.
- offset_keyframes silently deleted any key pushed before time 0.
Both now compute and validate every new time BEFORE removing anything,
and refuse with "Nothing was changed" if a key would be lost - landing
before 0, or (for scale) two keys merging onto the same frame. Numeric
params are validated in TypeScript first. The reported count is now the
number of keys actually written, not the number that were read.
Spatial properties take one temporal ease
-----------------------------------------
set_keyframe_advanced, apply_easy_ease and set_temporal_ease all counted
TwoD_SPATIAL / ThreeD_SPATIAL as 2 or 3 dimensions and built that many
KeyframeEase objects. Spatial properties (Position, Anchor Point) take
exactly one, so all three failed on Position - easy easing Position being
apply_easy_ease's main use - with "Value array does not have 1 elements".
Only non-spatial multi-dimensional properties such as Scale get 2 or 3.
copy_keyframes runaway
----------------------
The loop read sourceProp.numKeys live. Copying onto the same property
grew the bound with every key added: a 2-key copy produced 4,321 keys.
Source keys are now snapshotted before any are written. Also validates
sourceProperty, which previously crashed in TypeScript with
"Cannot read properties of undefined (reading 'split')".
Verified against AE 26.5: all eight bad-input cases are refused with the
keyframes byte-identical afterwards, and valid input still produces the
expected times.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
get_current_time, navigate_markers, collect_files and import_folder all
ended their generated script with a bare object literal:
{
time: comp.time,
frame: Math.round(comp.time * comp.frameRate)
};
In statement position a `{` opens a BLOCK, not an object. `time:` parses
as a label and the comma is a syntax error, so every call failed with
"SyntaxError: Expected: ;" - for every user, on every invocation.
get_current_time and navigate_markers were unregistered until recently,
which hid it. collect_files and import_folder are registered upstream
tools, so they have been failing for everyone.
The literal is now parenthesised, assigned to `result`, and restated as
the final expression, so it parses and is the value the script returns.
(For the mutating tools wrapped in an undo group, that value only reaches
the caller once the wrapper stops ending on app.endUndoGroup() - see
PR ishu86#4.) copy_effects had the same bug and is fixed alongside the other
effects changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
reorder_effects read effect.name AFTER effect.moveTo(). Moving a property invalidates the reference, so every call threw "ReferenceError: Object is invalid" - after the move had already happened. The name is now read first, and both indices are range-checked up front, so an out-of-range move says so instead of AE's "parent property has only 1 child properties". Also looks the group up by match name (ADBE Effect Parade) rather than the English display name. copy_effects ended with a bare object literal, which parses as a block and failed with "SyntaxError: Expected: ;". Same root cause and fix as the marker and asset generators. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Polystar Type is 1 = STAR, 2 = POLYGON. Verified directly: the default
is 1, and Inner Radius is only settable under 1.
The generator had them backwards:
- shape "star" set type 2 (polygon), then set Inner Radius, which is
hidden on a polygon -> "property or a parent property is hidden".
- shape "polygon" set type 1 and silently drew a STAR, with no error.
Also switches add_camera_layer and add_light_layer from English display
names ("Camera Options", "Zoom", "Light Options", "Color", "Intensity")
to match names, as already done for add_shape_layer.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
create_logo_reveal animates an existing footage item. With neither logoItemId nor logoItemName, neither branch declared logoItem and the script died with a bare "ReferenceError: logoItem is undefined". It now says what is missing before any script is generated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The bundled skill documented calls that fail or misbehave. Every claim
below was checked against the tool schemas and generator code, and the
path, effect, blend-mode and example calls were run live in AE 26.5.
Wrong, and failing:
- Nested property paths were written with dots
("Effects.Gaussian Blur.Blurriness"). The server splits on "/", so a
dotted path is looked up as one literal name: "Property not found".
- Shape examples used group "Shape 1". Groups created by these tools
are "Group 1"; operators sit under "Contents/Group 1/Contents/...".
- apply_expression_template("randomize", ...) - randomize is a text
animator type, not an expression template.
- blendMode "Screen" - the value is passed through as an AE constant,
so it must be SCREEN; "Screen" errors with "Value is undefined".
Wrong, and silently ineffective:
- springy was given `frequency`; it takes mass, stiffness and damping.
- The parallax example relied on link_properties' offset, which is
additive and cannot scale motion. Now uses a multiplier expression.
- modify_layer(motionBlur=...) - no such parameter.
- "Duplicate layer" and time remapping - no such tools. The description
no longer triggers on time remapping.
- create_logo_reveal style "particle" is listed but has no
implementation; the skill now says to avoid it.
Also:
- Recommends the camelCase path shortcuts (position, rotation,
sourceText), which resolve to match names and survive non-English
installs. Leaves out coneFeather, castsShadows and pointOfInterest,
whose shortcuts point at the wrong properties.
- Covers the new shape path, operator, mask, 3D and render queue tools,
including the renderer capability matrix, gradient colours being
unscriptable, render() blocking, and the output module choosing the
file type.
- Documents known issues: precompose_layers moves layers then errors,
and delete_composition leaves its solids behind.
- Tells the model to ask before project-wide operations.
- All examples use named parameters matching the schemas.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
create_logo_reveal accepted style "particle" but had no branch for it,
so the logo got no animation and the call still reported success.
ExtendScript has no particle API, but the bundled particle effects are
ordinary effects that can be added by match name. The reveal now:
1. applies CC Scatterize to the logo and animates Scatter and Right
Twist down to 0, so the logo assembles out of particles, fading in
across most of the move;
2. fires a short CC Particle Systems II burst on its own solid
("Logo Particles", ADD blend) as the logo lands.
The values were chosen by rendering frames and looking at them, because
several parameters don't behave as their names suggest:
- Particle Type is a numbered popup whose indices include menu
separators. The default, 1, draws lines. 5 is Faded Sphere, picked
from a rendered contact sheet of all 20 entries.
- Radius X/Y and Velocity are relative units, not pixels. Passing the
logo's pixel size filled the whole frame with particles. On a
1920-wide layer one radius unit is about layer width / 200, so the
spawn ellipse is sized from the logo with that ratio.
- Scatterize can't move pixels outside the logo layer's bounds, so a
large Scatter drew a solid rectangle of noise. It now starts at 150.
Effect parameters are addressed by match name, since effect property
lists are flat and reuse display names. If CC Particle Systems II is
unavailable, the reveal still runs and reports particleBurstAdded:
false.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…iles render_frame reported every render as failed. saveFrameToPng returns before the PNG is on disk - measured on AE 26.5, it appears about 50 ms later - and the tool checked for the file immediately, then threw "Frame render did not produce a file" while the file was still arriving. It now waits up to 15 s for the file, then until its size has held steady for three checks, so callers never read a half-written PNG. The result includes bytes and waitedMs. It also deletes any existing file at the output path first. Previously a leftover PNG from an earlier call satisfied the existence check even when nothing new was written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Verified against AE 26.5 lights and cameras: - pointOfInterest mapped to "ADBE Camera Options Group/ADBE Camera Point of Interest", which doesn't exist. A camera's Point of Interest is its anchor point, relabelled: ADBE Transform Group/ADBE Anchor Point. A set_keyframe through the shortcut now keys it and reads back. - coneFeather mapped to "ADBE Light Cone Feather"; current lights use "ADBE Light Cone Feather 2". - castsShadows mapped to "ADBE Light Casts Shadows"; lights use the shared "ADBE Casts Shadows". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Adds a Particles section: which bundled effects provide particles, that emitters replace their layer, CC Particle Systems II's relative Radius/Velocity units, the Particle Type menu indices (with the separators), and HOLD-keyed bursts. - Notes that effect parameter lists reuse display names, and that a parameter's match name works as the last path segment. - create_logo_reveal "particle" is now implemented, so the warning and known-issue entry are gone. - Lists the camera and light shortcuts now that pointOfInterest, coneFeather and castsShadows resolve. - render_frame now waits for the file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Effects render inside their layer's bounds. With CC Scatterize on a
tight text or logo precomp, the scatter filled that rectangle and
stopped at its edges, which read as a hard box. The earlier fix only
lowered Scatter to make the box less visible.
create_logo_reveal "particle" now gives the logo room first:
- if the layer can collapse (precomps, solids, vector layers), it turns
on Collapse Transformations so effects render in the parent comp's
space;
- otherwise, for raster footage such as a PNG, it adds Grow Bounds
("ADBE GROW BOUNDS", by match name - a Red Giant plugin shares the
display name) before Scatterize.
With room available, Scatter starts at 450 instead of 150. The result
reports logoBoundsExpandedBy.
canSetCollapseTransformation is the test, not layer type: solids report
true, a PNG layer reports false.
modify_layer gains a collapseTransformation parameter, with a clear
error on layers that can't collapse.
Verified with renders of a text precomp (collapsed) and a PNG (grow
bounds): in both the scatter now extends past the source bounds with
soft edges.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…mple - Effects are clipped to their layer's bounds: use collapseTransformation for precomps, solids and vectors, and Grow Bounds (by match name) for raster footage. - apply_effect display names can resolve to a third-party plugin with the same name - "Grow Bounds" resolved to Red Giant's on a machine with it installed. Check the returned matchName, or pass one. - Particle previews should go through the render queue: a 4 s particle comp rendered in 4 s there, versus minutes frame by frame. - Example 10: a particle text reveal, the exact calls used to build the demo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The skill said every tool call is exactly one undo step named for the operation. That depends on the double-undo-group fix, which is left to PR ishu86#4 rather than duplicated here - and ishu86#4's design names the group after the command ID. Describe only what holds either way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…gle variants
Measured on AE 26.5 with keyframes at 0, 1 and 2 s, the physics
templates had these problems:
- All four (bounce, overshoot, inertia, springy) keyed off
key(numKeys), so they only ever affected motion after the final
keyframe - the deviation after a middle key was exactly 0. They now
find the most recent key at or before the playhead, so they act after
every keyframe.
- overshoot did nothing: it measured value - lastKey.value, which is 0
after the last key, so the layer stopped dead. It is now a damped sine
seeded with the incoming velocity.
- bounce was a damped-sine overshoot, not a bounce, and wasn't divided by
the angular frequency, so velocity jumped from 697 to 1291 px/s at the
keyframe - a visible kink. It is now a gravity/elasticity bounce-back
(elasticity, gravity, maxBounces) that rebounds off the key value. Old
amplitude/frequency/decay params are ignored and defaults apply.
- springy divided by the undamped frequency but oscillated at the damped
one, starting about 13% slow. It now divides by the damped frequency.
Incoming velocity is sampled a tenth of a frame before the key, so it's
the velocity arriving at the key rather than leaving it.
New templates:
- speedControl: accumulates a keyframed "units per second" slider as
trapezoid areas between keys. `slider * time` runs the property
backwards as the speed eases off (720 -> 450 -> 0 deg); this holds
at 720.
- loopingWiggle: crossfades wiggle(t) with wiggle(t - loopTime), so the
cycle repeats seamlessly.
- wiggleOneAxis: wiggles one axis and leaves the others on their keys.
The techniques follow Dan Ebberts' articles on motionscript.com
(Realistic Bounce and Overshoot; Expression Speed and Frequency
Control). The code is written for this project, not copied.
Template values are now checked before being pasted into expression
source: number params must be finite numbers (a string such as
"3); app.quit(" is rejected), string params are escaped for the quotes
they sit in, and followPath's progress is typed "expression" because it
is inserted as raw code.
add_expression_control now requires controlName. When it was missing
the control was named "undefined", so expressions looking it up by name
never found it.
Verified live: 28/28 checks on Position and Rotation - every template
acts after the middle key, no expression errors, velocity continuous
through the key for overshoot/inertia/springy, bounce rebounds at
elasticity x incoming speed and never passes the key, speedControl gives
the exact integral, loopingWiggle is identical at 0/3/6 s.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Lists every expression template with its params, grouped. bounce's params are now elasticity, gravity and maxBounces. - Explains that the physics templates act after every keyframe and take their size from the incoming speed, so an easy-eased landing key gets no effect. - speedControl, with the add_expression_control call it needs. - Example 1's bounce now keyframes only the fall and uses gravity 1500: verified to give an ~80 px first rebound and seven in all. At 5000 the first rebound was only 24 px. - Example 11: a speed ramp that doesn't rewind, verified to give 180 / 540 / 720 / 720 degrees at 1-4 s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary