fix(studio): address review findings on graded-element editing

Review follow-ups (both reviewers, all findings):

- resize captures scope to the resize group: convert-to-keyframes
  resolvedFromValues and the whole-offset backfill pass the group filter,
  so an opacity-touching intro tween can't ride into a converted scale
  tween (the rotation fix's contract, now uniform across intercepts)
- commitStaticSet resolves every group's target set BEFORE committing and
  coalesces groups landing on the same legacy mixed set into one commit —
  the second commit can no longer chase a stale group-derived id
- installAuthoredOpacityCapture also stamps an element the moment it GAINS
  data-color-grading at runtime (attributeFilter), not just at insertion
- both writer twins now share the same emitted-set dedupe shape
- applySoftReload's positional tail becomes a SoftReloadOptions object
- readAllAnimatedProperties builds the group-filtered key set immutably
  instead of deleting from the set mid-iteration
- applyAuthoredInlineOpacity documents the priority-lossy round-trip
- the marquee hit-test reads activeCompositionPathRef like its neighbors

New tests: resize intercept (scale route + group filter + non-uniform
longhands), after-write-HTML / stamp / empty-stamp opacity restore, the
no-op-commit-with-missed-instant-patch soft-reload contract, and the
runtime-gained-grading stamp.
This commit is contained in:
Miguel Angel Simon Sierra
2026-07-11 15:18:49 -04:00
parent 67cfae2587
commit 5d1cafff82
13 changed files with 367 additions and 88 deletions
@@ -643,4 +643,26 @@ describe("installAuthoredOpacityCapture", () => {
expect(el.getAttribute("data-hf-authored-opacity")).toBe("");
el.remove();
});
it("stamps an already-inserted element the moment it GAINS grading at runtime", async () => {
installAuthoredOpacityCapture();
const el = document.createElement("img");
el.style.opacity = "0.9";
document.body.appendChild(el);
await Promise.resolve();
expect(el.hasAttribute("data-hf-authored-opacity")).toBe(false);
// Studio applies a preset to a previously ungraded element — no re-insert.
el.setAttribute(HF_COLOR_GRADING_ATTR, serializeHfColorGrading({ adjust: { exposure: 0.5 } }));
await Promise.resolve();
expect(el.getAttribute("data-hf-authored-opacity")).toBe("0.9");
// Later attribute rewrites (preset tweaks) never overwrite the stamp,
// even if a transient is live by then.
el.style.opacity = "0";
el.setAttribute(HF_COLOR_GRADING_ATTR, serializeHfColorGrading({ adjust: { exposure: 0.9 } }));
await Promise.resolve();
expect(el.getAttribute("data-hf-authored-opacity")).toBe("0.9");
el.remove();
});
});
+14 -1
View File
@@ -239,8 +239,21 @@ export function installAuthoredOpacityCapture(): void {
new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) scan(node);
// An element can also GAIN grading at runtime (studio applies a preset to
// a previously ungraded element). Stamp at that moment — strictly earlier
// than the engine's hide, so the captured value can never be worse than
// the hide-time fallback, and after a soft reload's authored restore it
// IS the authored value. stamp() is idempotent: an existing stamp wins.
if (mutation.type === "attributes" && mutation.target instanceof Element) {
if (mutation.target.hasAttribute(HF_COLOR_GRADING_ATTR)) stamp(mutation.target);
}
}
}).observe(root, { childList: true, subtree: true });
}).observe(root, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: [HF_COLOR_GRADING_ATTR],
});
}
// Map insertion order gives us simple FIFO eviction for authoring sessions that cycle LUTs.