Upstream gained region-limited coverage, clip regions, a group nesting cap
in the PDF builder and a render budget. The clipping fast paths (canvas-covering
clips drawn without a surface, plain clip groups on region-sized surfaces) are
ported onto those; the crop-based shape/image/paint changes and the PDF
builder depth cap are superseded by upstream's.
* #3099: placed .ai pages fill the size After Effects stored; null anchors default to the corner
Footage of an .aep keeps the size After Effects stored for a placed PDF / Illustrator / EPS
file (the ArtBox, rounded up), and masks and anchors are measured against it. The page was
drawn as a PDF viewer frames it (CropBox, /Rotate) at its own size, scaled by the width ratio
at full resolution and stretched to the stored aspect below 3/4: masks too large, art off
centre, squeezed at half resolution.
- effectcraft-pdf: Framing::Placed / parse_page_framed (ArtBox clipped to the CropBox,
/Rotate ignored); boxes with a non-finite corner or span are not used
- Footage::placed_page (set by the .aep importer, serialized only when true): the page is
framed so and filled into the stored size (effectcraft_render::vector_fit), decoded,
continuously rasterised and in Create Shapes from Vector Layer; Reload keeps the size;
the frame cache keys it by size and framing
- .aep import/export: a null's absent Anchor Point is [0, 0], as After Effects reads it
- fixture ai-mask-3099.aep (own synthetic files, scrubbed, profiles stubbed) and tests
against After Effects 2026's read-back
* #3099: placed-page framing only for .ai and .pdf, EPS as before; doc comments
After Effects' sizing was measured with PDFs named .ai only; an EPS from an .aep keeps its
bounding box at its own size (listed as not measured in docs/gaps.md and docs/aep-import.md).
Doc comments on the touched functions.
* #3099: shapes from a placed page keep its frame (track matte)
Create Shapes from Vector Layer converted every path of a placed page, so bleed, art past the
artboard or the page's background showed where the footage's raster hides them. The layers it
makes from a placed page now take a hidden "<layer> Frame" shape layer (the footage's
width x height) as their Alpha track matte: drawn at the comp's resolution, its edge is the
raster's on shape and image layers alike. The command returns the frame layer's id too.
Continuously rasterised vector footage (an A4 PDF scaled 200-400 %) is a page of
millions of pixels, and the rasteriser did page-sized work per element:
- Every path, run of text and image computed and walked a coverage mask the size of
the whole page. Coverage and paint now run over the pixels the shape (fill, or stroke
outline) or image can reach. A generated A4 page of 720 text-run paths and 60 strokes
rasterises at 300 % in 0.18 s instead of 4.97 s (200 %: 0.13 s instead of 2.44 s), with
the same pixels.
- Every clipping group copied the whole backdrop and drew on a second page-size surface,
so deeply nested clips ran past the 512 MB surface budget and their artwork was dropped
(at 200 % from the eighth level, at 400 % from the second). A plain clipping group (no
soft mask, blend mode, group opacity or knockout, Normal content throughout) is now drawn
straight onto the page when every clip is an axis-aligned rectangle covering each pixel
the content can reach, and otherwise on a surface the size of its clip. Both give the
same pixels; groups with a blend mode or knockout inside keep the backdrop path.
Never crash: drawing stops below 256 levels of groups, and the PDF/EPS builder keeps groups
at most 200 deep (a crafted page nesting `q ... W n` twenty thousand deep overflowed the
stack when the document was drawn or dropped).
The 596 x 842 px import size of an A4 page is unchanged: the page in points at 72 dpi,
rounded up, as After Effects takes a point as a pixel.
Refs #2895
Every undoable edit keeps the previous project for undo, so the comp it touches
is shared and Arc::make_mut deep-copies it: every layer and every keyframe. An
edit therefore costs time in proportion to the comp's size, and a script that
sets many keyframes in a big comp goes quadratic (a 1,600-layer build with
~100k sampled keys took ~45 minutes).
Comp::layers is now a Layers collection backed by Vec<Arc<Layer>>: copying the
stack copies pointers, and only the layer an edit changes is copied. The API
mirrors Vec<Layer>; serialized form is unchanged (a list of layers).
Comp::layer_mut and the iter_mut().filter/find call sites use find_mut /
iter_mut_where so they copy only the layers they actually change.
- prop.addKey in a 400-layer comp: 1.36 ms -> 0.16 ms per key (flat in comp size)
- the 1,600-layer script build: ~45 min -> 47 s, frames pixel-identical
- regression tests: an edit leaves the other layers shared with undo; layers
round-trip as a plain list
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VkpGcKfWyCudfSsQNiWAaG
* Dev profile: line tables only for workspace crates, no debug info for dependencies
* xtask ci: dev-profile gate, nextest, tests without examples, wasm --keep-going
* One integration-test binary per crate
* xtask ci: run effectcraft-gpu's shared-device tests in one cargo test process
* xtask ci: run every stage and report all failures
* xtask ci: name the stage type (clippy type_complexity)
* xtask ci: run every test pass and report all failures
* cargo fmt