mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
feat(studio): expand sub-composition groups + children in the timeline (#1761)
* feat(studio): element groups — source mutations Wrap/unwrap source mutations (group geometry, the wrap-elements / unwrap-elements routes) that the studio group feature is built on. Studio UI lands in the next PR. * fix(studio): hoverable group interior + non-sticky drill-in Two group selection bugs with animated members: 1) Empty space inside a group's overlay didn't hover/select the group. Members animated outside the wrapper's static box (110px box vs 340px member union), so elementsFromPoint hit only the full-bleed background there. Add a member-union hit-test fallback: a point inside a group's live member bounds resolves to that group (innermost wins). 2) After drilling into a group and selecting a child, nothing else was selectable — out-of-scope resolved to null. Make drill-in non-sticky: interacting outside the drilled group re-resolves normally and exits the drill-in, so a later click on the group selects it as a unit again. * feat(studio): enable animation editing for static inline timelines The unsupported-pattern banner now clears for static window.__timelines["id"] = gsap.timeline() (the parser reports it editable), and the banner copy is retargeted to the genuinely-unsupported case: computed/dynamic keys (window.__timelines[var]). * fix(studio): correct keyframes + expansion for sub-composition timeline clips Two gaps for elements inside a sub-composition: 1) Clip keyframes rendered off-clip. The keyframe cache computes clip-relative percentages from the element's start/duration, but sub-comp internals aren't in the timeline elements list, so duration defaulted to 1s and percentages blew past 100%. Resolve the timing basis from the sub-comp HOST's bounds (via domClipChildren, since the host's data-composition-src is stripped in the rendered DOM). Shared resolveClipTimingBasis used by both cache populators, which now re-run when the sub-comp children appear. 2) Only GROUPED sub-comp children expanded. Generalize the DOM-children collector to gather id'd children of the sub-comp inner-root (grouped OR ungrouped), descending through id-less structural wrappers; one level into groups for drill-in. Ungrouped pills now expand into timeline rows too.
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { liveTime, usePlayerStore } from "../store/playerStore";
|
||||
import type { TimelineElement } from "../store/playerStore";
|
||||
import type { TimelineElement, DomClipChild } from "../store/playerStore";
|
||||
import type { PlaybackAdapter, ClipManifestClip, IframeWindow } from "../lib/playbackTypes";
|
||||
import {
|
||||
parseTimelineFromDOM,
|
||||
@@ -85,8 +85,8 @@ export function useTimelineSyncCallbacks({
|
||||
| (Window & { __clipTree?: import("@hyperframes/core/runtime/clipTree").ClipTree })
|
||||
| null;
|
||||
const clipTree = iframeWin?.__clipTree;
|
||||
const parentMap = new Map<string, string>();
|
||||
if (clipTree) {
|
||||
const parentMap = new Map<string, string>();
|
||||
const walk = (nodes: typeof clipTree.roots) => {
|
||||
for (const node of nodes) {
|
||||
if (node.id && node.parentId) parentMap.set(node.id, node.parentId);
|
||||
@@ -94,11 +94,50 @@ export function useTimelineSyncCallbacks({
|
||||
}
|
||||
};
|
||||
walk(clipTree.roots);
|
||||
usePlayerStore.getState().setClipParentMap(parentMap);
|
||||
}
|
||||
|
||||
// Descend into each sub-composition host: its internal elements (group
|
||||
// wrappers + their children) carry no `data-start`, so the clip
|
||||
// tree/manifest never enumerate them. Surface them studio-side as DOM
|
||||
// children + parent links so the timeline can expand a sub-comp/group
|
||||
// row to show them. Manifest stays lean (timed clips only).
|
||||
const domClipChildren: DomClipChild[] = [];
|
||||
if (iframeDoc) {
|
||||
for (const clip of data.clips) {
|
||||
if (clip.kind !== "composition" || !clip.id) continue;
|
||||
const hostEl = iframeDoc.getElementById(clip.id);
|
||||
if (!hostEl) continue;
|
||||
const hostId = clip.id;
|
||||
const innerRoot = hostEl.querySelector("[data-hf-inner-root]") ?? hostEl;
|
||||
// Collect the sub-comp's id'd descendants (grouped OR ungrouped) so they
|
||||
// expand into timeline rows. Descends through id-less structural wrappers
|
||||
// (the inlined sub-comp body), and one level into groups for drill-in.
|
||||
const collect = (parentEl: Element, parentId: string) => {
|
||||
for (const child of Array.from(parentEl.children)) {
|
||||
if (!child.id) {
|
||||
collect(child, parentId); // unwrap id-less structural containers
|
||||
continue;
|
||||
}
|
||||
const isGroup = child.hasAttribute("data-hf-group");
|
||||
domClipChildren.push({
|
||||
id: child.id,
|
||||
parentId,
|
||||
hostId,
|
||||
label: isGroup ? child.getAttribute("data-hf-group") || child.id : child.id,
|
||||
});
|
||||
parentMap.set(child.id, parentId);
|
||||
if (isGroup) collect(child, child.id);
|
||||
}
|
||||
};
|
||||
collect(innerRoot, hostId);
|
||||
}
|
||||
}
|
||||
usePlayerStore.getState().setClipParentMap(parentMap);
|
||||
usePlayerStore.getState().setDomClipChildren(domClipChildren);
|
||||
} catch {
|
||||
// cross-origin or __clipTree not available — parentMap stays empty
|
||||
// cross-origin or __clipTree not available — maps stay empty
|
||||
}
|
||||
|
||||
const usedHostEls = new Set<Element>();
|
||||
const els: TimelineElement[] = filtered.map((clip, index) => {
|
||||
const hostEl = iframeDoc
|
||||
|
||||
Reference in New Issue
Block a user