plans/issues/pr-3749.md
Executor instructions: Follow this plan step by step. Run every verification command and confirm the expected result before moving on. If anything in "STOP conditions" occurs, stop and report — do not improvise. When done, update this plan's row in
plans/issues/README.md.Drift check (run first):
gh pr view 3749 --json state,headRefOid,mergeStateStatusExpected:state: OPEN,headRefOid: 80d85dbeb2a9bc0391b72f9da2b6ada6b3b8eac1. If closed/merged or head differs, STOP.
42bfbe3ed, 2026-06-11worktree-style-effect)This unifies the two render pipelines: VisualElement stops using
build()/renderState/renderInstance() and instead binds values through
the effects system (MotionValueState with an ordered slot/contributor
model). It is the substrate the repo's roadmap is sequenced behind:
plans/README.md:33,37 defer both the WAAPI transform-shorthand
implementation (plan 004) and the framework-agnostic MotionNode/vanilla-drag
arc until "the effects/VisualElement unification (worktree-style-effect)
direction is settled". It also resolves the effects composition debt (the
pathRotation 3-render-site hack) via the contributor/slot model. CI is fully
green; what remains is review-thread disposition and a release-compat check.
MotionValueState, (2) remove() semantics, (3) the pipeline swap.packages/motion-dom/src/effects/MotionValueState.ts):
contribute(name: string, index: number, builder: SlotBuilder) {
...
chain[index] = builder
this.get(name)?.dirty()
return () => { chain[index] = undefined; this.get(name)?.dirty() }
}
packages/motion-dom/src/render/VisualElement.ts):
granular binding for normal elements, full-render path for projection-driven
ones, with a re-bind loop in update() when an element becomes
projection-driven after mount (lazy-loaded layout features).
isProjectionDriven() is gated on
options.layout || options.layoutId || options.alwaysMeasureLayout — NOT on
node existence (every element gets a projection node once layout features
load; see memory "Effects/VisualElement unification" invariants).buildHTMLStyles, buildSVGAttrs,
buildSVGPath, renderHTML, renderSVG removed (replaced by
buildStyles, buildSVGProps); VisualElement.build/renderInstance/triggerBuild/removeValueFromRenderState removed.cypress/integration/svg.ts now
asserts getComputedStyle($circle).fill, not the attribute); each bound
value now has two change subscriptions (animate-prop.test.tsx 1 → 2).size-rollup-m, +0.92 kB size-rollup-motion.| Purpose | Command | Expected |
|---|---|---|
| Checkout | gh pr checkout 3749 | branch worktree-style-effect |
| Rebase (after 3748 lands) | git fetch origin main && git rebase origin/main | no conflicts expected (zero file overlap with 3748) |
| Build | yarn build (repo root) | exit 0 |
| Jest | yarn test | pass (modulo known SSR TextEncoder / use-velocity failures) |
| HTML E2E incl. animate-layout + projection | the repo's test-html CI job locally, or make test-e2e HTML portion | 159/159 per PR body, incl. 24 animate-layout + 108 projection |
| CI | git push --force-with-lease && gh pr checks 3749 --watch | green |
In scope: disposing the 3 Greptile threads (one may need a small code change, Step 1), rebase over #3748, the Framer-compat checklist, merge.
Out of scope: new features on the effects system; restoring the removed
exports wholesale (the removal is the point — unless the compat check in
Step 3 forces a deprecation shim, which is a maintainer decision); touching
LayoutAnimationBuilder.ts.
SVGVisualElement.renderValues ignores
the _projection param; the old renderSVG forwarded it to
applyProjectionStyles. Mitigating fact verified on main:
SVGVisualElement.measureInstanceViewportBox = createBox (always a zero
box), so SVG layout projection was already inert. Resolution options:
(a) reply documenting "SVG projection was never functional; intentionally
dropped" — needs maintainer sign-off, or (b) one-line defensive forward of
the projection param matching the HTML path. If the operator marked this
plan APPROVED in plans/issues/README.md, take (a) by posting the reply;
otherwise mark the row BLOCKED ("SVG-projection thread needs maintainer
disposition").bindToMotionValue begins with
if (this.valueSubscriptions.has(key)) { this.valueSubscriptions.get(key)!() }
on both main and the branch, so lazy-load re-binding cleans up. Reply on
the thread with that line reference to close it.packages/motion-dom/src/effects/style/index.ts
(says "HTML element" where the guard is !isHTML): fix the comment text
(one line) and push.Verify: all three threads have replies; any code change is committed and CI re-green.
Land order is fixed: #3748 first (see plans/issues/pr-3748.md). After it
merges, rebase this branch onto main. No file overlap exists
(#3748's only source file is LayoutAnimationBuilder.ts, untouched here), so
expect a clean rebase.
Verify: rebase clean; yarn build exit 0.
The PR removes motion-dom exports (buildHTMLStyles, buildSVGAttrs,
buildSVGPath, renderHTML, renderSVG) and VisualElement methods. Before
merge, produce the evidence for the maintainer: search the Framer codebase /
motion-plus (/Users/matt/Sites/plus) for usages:
grep -rn "buildHTMLStyles\|buildSVGAttrs\|buildSVGPath\|renderHTML\|renderSVG\|triggerBuild\|removeValueFromRenderState" /Users/matt/Sites/plus --include=*.ts*.
If you cannot access the Framer app codebase itself, say so explicitly in the
README row — absence of motion-plus usage is necessary but not sufficient.
Record findings as a PR comment.
Verify: comment posted listing each removed symbol and its found/not-found status, with the searched codebases named.
The animate-layout fixtures exercise #3748's engine rendering through #3749's
new pipeline — the combination that has never run in CI before the rebase.
Run the full HTML E2E suite locally (expect 159/159 + the fixtures #3748
added, incl. fire-and-forget). Push; wait for all checks green;
gh pr merge 3749 --squash. Do NOT use gh pr edit.
Verify: gh pr view 3749 --json state → MERGED.
plans/issues/README.md row updatedlayoutCurve revival (all referenced in plans/README.md:33,37-38).
Also revisit memory note "Effects composition debt" — the slot model this PR
ships is the "proper fix" that note describes; pathRotation should migrate
to a contributor slot as follow-up.