Skip to content

Commit da6681e

Browse files
committed
refactor(studio): one owner for the selected node lookup, and scrub indexes only audio
1 parent 276ded9 commit da6681e

7 files changed

Lines changed: 33 additions & 29 deletions

File tree

‎packages/studio/src/components/editor/domEditingElement.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,19 @@ export function findElementForSelection(
314314
return findAll(selection.selector)[selection.selectorIndex ?? 0] ?? null;
315315
}
316316

317+
// The selected node while mounted, else its copy in the selection's own file (ids repeat across files), else any copy.
318+
export function findPreviewNode(
319+
doc: Document | null | undefined,
320+
el: DomEditSelection,
321+
): Element | null {
322+
if (!doc) return null;
323+
if (el.element?.isConnected && el.element.ownerDocument === doc) return el.element;
324+
return (
325+
findElementForSelection(doc, el) ??
326+
findElementForSelection(doc, { ...el, sourceFile: undefined })
327+
);
328+
}
329+
317330
// fallow-ignore-next-line complexity
318331
export function findElementForTimelineElement(
319332
doc: Document,

‎packages/studio/src/components/editor/gsapLivePreview.ts‎

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { DomEditSelection } from "./domEditingTypes";
2-
import { findElementForSelection } from "./domEditingElement";
2+
import { findPreviewNode } from "./domEditingElement";
33

44
/**
55
* Build the "live preview" callback the 3D-transform sub-view fires while a
@@ -9,20 +9,6 @@ import { findElementForSelection } from "./domEditingElement";
99
* Extracted so the identical closure exists once — shared by the legacy
1010
* PropertyPanel Layout section and the flat Layout group (PropertyPanelFlat).
1111
*/
12-
// The selected node while it is still mounted; after a reload, the copy in the selection's own file (hf-ids and ids
13-
// repeat across flattened sub-compositions), then anywhere.
14-
export function findPreviewNode(
15-
doc: Document | null | undefined,
16-
el: DomEditSelection,
17-
): Element | null {
18-
if (!doc) return null;
19-
if (el.element?.isConnected && el.element.ownerDocument === doc) return el.element;
20-
return (
21-
findElementForSelection(doc, el) ??
22-
findElementForSelection(doc, { ...el, sourceFile: undefined })
23-
);
24-
}
25-
2612
export function createGsapLivePreview(iframeRef: { readonly current: HTMLIFrameElement | null }) {
2713
return (el: DomEditSelection, props: Record<string, number>) => {
2814
const iframe = iframeRef.current;

‎packages/studio/src/components/editor/propertyPanelHelpers.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { DomEditSelection } from "./domEditing";
44
import type { GsapAnimation } from "@hyperframes/parsers/gsap-parser";
55
import type { TimelineElement } from "../../player";
66
import { roundToCenti } from "../../utils/rounding";
7-
import { findPreviewNode } from "./gsapLivePreview";
7+
import { findPreviewNode } from "./domEditingElement";
88

99
export type {
1010
BackgroundRemovalProgress,

‎packages/studio/src/hooks/useGestureRecording.ts‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,6 @@ function readBasePosition(element: HTMLElement, iframeEl: HTMLIFrameElement): Ba
8080
function connectGsapRuntime(
8181
element: HTMLElement,
8282
iframeEl: HTMLIFrameElement,
83-
selector: string | null,
8483
elementEndTime: number | undefined,
8584
): GsapRuntime | null {
8685
try {
@@ -97,7 +96,7 @@ function connectGsapRuntime(
9796
([key, value]) => key !== "__proxied" && typeof value?.seek === "function",
9897
)?.[1] ?? null)
9998
: null;
100-
if (win?.gsap?.set && tl?.seek && selector) {
99+
if (win?.gsap?.set && tl?.seek && element.id) {
101100
const tlDuration = tl.duration();
102101
return {
103102
timeline: tl,
@@ -322,8 +321,7 @@ export function useGestureRecording() {
322321
r.scale = computeIframeScale(iframeEl);
323322

324323
// --- Phase 3: Connect to the iframe GSAP runtime ---
325-
const selector = element.id ? `#${element.id}` : null;
326-
r.runtime = connectGsapRuntime(element, iframeEl, selector, elementEndTime);
324+
r.runtime = connectGsapRuntime(element, iframeEl, elementEndTime);
327325
// Clear the optimistic path offset only while a live runtime owns the
328326
// preview. releaseRuntimePreview restores it on every exit path.
329327
if (r.runtime && (base.cssOffX || base.cssOffY)) {

‎packages/studio/src/player/lib/timelineElementHelpers.ts‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -462,13 +462,21 @@ export type PreviewTarget = Pick<TimelineElement, "hfId" | "domId" | "id" | "sou
462462

463463
/** Finds a row's preview element by `data-hf-id`, then id, preferring one in the row's own file: both repeat across
464464
* files. Indexes the document once, so a pass over every row costs one scan. */
465-
export function previewElementFinder(doc: Document): (target: PreviewTarget) => Element | null {
465+
export function previewElementFinder(
466+
doc: Document,
467+
selector = "[data-hf-id], [id]",
468+
): (target: PreviewTarget) => Element | null {
466469
const byKey = new Map<string, Element[]>();
467-
const add = (key: string, node: Element) => byKey.set(key, [...(byKey.get(key) ?? []), node]);
468-
for (const node of doc.querySelectorAll("[data-hf-id], [id]")) {
470+
const add = (key: string, node: Element) => {
471+
const nodes = byKey.get(key);
472+
if (nodes) nodes.push(node);
473+
else byKey.set(key, [node]);
474+
};
475+
for (const node of doc.querySelectorAll(selector)) {
469476
const hfId = node.getAttribute("data-hf-id");
477+
const id = node.getAttribute("id");
470478
if (hfId) add(`hf:${hfId}`, node);
471-
if (node.id) add(`id:${node.id}`, node);
479+
if (id) add(`id:${id}`, node);
472480
}
473481
return (target) => {
474482
const matches = [

‎packages/studio/src/player/lib/timelineIframeHelpers.test.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
stopScrubPreviewAudio,
1010
} from "./timelineIframeHelpers";
1111
import type { IframeWindow } from "./playbackTypes";
12-
import type { TimelineElement } from "../store/playerStore";
1312

1413
function makeDoc(html: string): Document {
1514
const d = document.implementation.createHTMLDocument();

‎packages/studio/src/player/lib/timelineIframeHelpers.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,9 @@ let scrubStopTimer: ReturnType<typeof setTimeout> | null = null;
218218
let scrubPrevMuted: boolean | null = null;
219219
let scrubPrevVolume: number | null = null;
220220

221-
// Resolve the SAME element the store identified as music: prefer its id, then
222-
// the role attribute, and only fall back to the first <audio> (which could be a
223-
// voiceover, so the id hint matters).
221+
// Resolve the SAME element the store identified as music: prefer that row's own
222+
// element, then the role attribute, and only fall back to the first <audio>
223+
// (which could be a voiceover, so the row hint matters).
224224
/**
225225
* `doc` is the preview iframe's document, so its `<audio>` nodes are instances of
226226
* the IFRAME's `HTMLAudioElement`, never this module's. `instanceof
@@ -239,7 +239,7 @@ function isAudioNode(node: Element | null): node is HTMLAudioElement {
239239

240240
function resolveScrubAudioEl(doc: Document, music?: PreviewTarget | null): HTMLAudioElement | null {
241241
if (music) {
242-
const byId = previewElementFinder(doc)(music);
242+
const byId = previewElementFinder(doc, "audio")(music);
243243
if (isAudioNode(byId)) return byId;
244244
}
245245
return (

0 commit comments

Comments
 (0)