diff --git a/packages/studio/src/player/store/playerStore.ts b/packages/studio/src/player/store/playerStore.ts index adf60e723..6e090bba2 100644 --- a/packages/studio/src/player/store/playerStore.ts +++ b/packages/studio/src/player/store/playerStore.ts @@ -1,6 +1,6 @@ import { create } from "zustand"; import { attachPlayerStoreDevHandle } from "./playerStoreDevHandle"; -import { nextSelectionSet } from "./playerStoreSelection"; +import { nextSelectionSet, revealTargetsSelection } from "./playerStoreSelection"; import type { MusicBeatAnalysis } from "@hyperframes/core/beats"; import type { GsapAnimation } from "@hyperframes/core/gsap-parser"; import type { BeatEditState } from "../../utils/beatEditing"; @@ -541,7 +541,13 @@ export const usePlayerStore = create((set, get) => ({ // selection lands afterwards and used to clear the very request that // caused it — the panel then read null and the section never opened. // Any OTHER selection still drops it: a request aimed elsewhere is stale. - const revealSurvives = s.revealedAudioFxTarget?.elementKey === id; + // + // Compared across the ID-SPACE BOUNDARY, which is why this needs saying: + // a request carries the BARE dom id (`runtimeAudioId`, because the panel + // and the runtime speak that), while this store's ids are + // `sourceFile#domId`. A direct `===` was silently never true — the exact + // shape of failure the id-space split produces. + const revealSurvives = revealTargetsSelection(s.revealedAudioFxTarget, id); return id !== s.selectedElementId ? { selectedElementId: id, diff --git a/packages/studio/src/player/store/playerStoreSelection.ts b/packages/studio/src/player/store/playerStoreSelection.ts index 33817091b..ab514399b 100644 --- a/packages/studio/src/player/store/playerStoreSelection.ts +++ b/packages/studio/src/player/store/playerStoreSelection.ts @@ -1,9 +1,12 @@ /** - * Selection-set arithmetic for the player store. + * Selection-set arithmetic for the player store, and the reveal request's + * cross-id-space match. * * Its own module so `playerStore.ts` stays under the studio's 600-line cap. */ +import { splitTimelineElementKey } from "../lib/timelineElementHelpers"; + /** * The id set a selection change leaves behind. * @@ -19,3 +22,20 @@ export function nextSelectionSet( if (preserveSet) return id && current.has(id) ? new Set(current) : new Set(); return id ? new Set([id]) : new Set(); } + +/** + * Is a pending reveal request aimed at the element being selected? + * + * Compared across the ID-SPACE BOUNDARY, which is why it is a named function: + * a request carries the BARE dom id (`runtimeAudioId` — the panel and the + * runtime speak that), while the store's ids are `sourceFile#domId`. A direct + * `===` between the two is silently never true, which is the exact shape of + * failure the split produces and how the reveal came to be dead. + */ +export function revealTargetsSelection( + request: { elementKey: string } | null, + id: string | null, +): boolean { + if (!request || id === null) return false; + return request.elementKey === splitTimelineElementKey(id).domId; +}