@hyperframes/studio 0.8.48 → 0.8.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/hyperframes-player-CxxDxHTW.js +464 -0
- package/dist/assets/{index-D_j-0PD3.js → index-DUAIWxkI.js} +1 -1
- package/dist/assets/{index-B3-orIjW.js → index-Dt3qBI-9.js} +1 -1
- package/dist/assets/{index-C9gJKpqQ.js → index-n2ml4tUn.js} +214 -213
- package/dist/assets/index-pO6p0H27.css +1 -0
- package/dist/index.d.ts +16 -7
- package/dist/index.html +2 -2
- package/dist/index.js +6209 -5924
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +10 -8
- package/src/components/EditorShell.selectionSync.test.tsx +4 -0
- package/src/components/EditorShell.tsx +24 -0
- package/src/components/StudioOverlays.tsx +1 -5
- package/src/components/editor/PreviewGuides.test.tsx +88 -0
- package/src/components/editor/PreviewGuides.tsx +125 -0
- package/src/components/editor/SnapToolbar.test.tsx +21 -0
- package/src/components/editor/SnapToolbar.tsx +25 -1
- package/src/components/editor/domEditingAgentPrompt.ts +5 -0
- package/src/components/editor/previewGuidesStore.ts +26 -0
- package/src/components/nle/NLEPreview.test.ts +13 -0
- package/src/components/nle/NLEPreview.tsx +9 -4
- package/src/components/nle/PreviewPane.test.tsx +52 -0
- package/src/components/nle/PreviewPane.tsx +2 -0
- package/src/components/nle/TimelinePane.tsx +14 -0
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +1 -1
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +3 -3
- package/src/hooks/appHotkeysDispatch.ts +11 -0
- package/src/hooks/timelineAudioGroupCreate.ts +2 -2
- package/src/hooks/timelineTrackVisibility.ts +8 -11
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +1 -0
- package/src/hooks/useAppHotkeys.ts +3 -0
- package/src/hooks/useAskAgentModal.ts +2 -0
- package/src/hooks/useBlockHandlers.ts +7 -2
- package/src/hooks/useClipboard.test.ts +234 -0
- package/src/hooks/useClipboard.ts +264 -70
- package/src/hooks/useGroupCommits.test.ts +0 -1
- package/src/hooks/useRazorSplit.history.test.tsx +0 -1
- package/src/hooks/useRazorSplit.test.ts +0 -2
- package/src/hooks/useRazorSplit.test.tsx +0 -1
- package/src/hooks/useStudioContextValue.ts +5 -19
- package/src/hooks/useTimelineAssetDropOps.test.tsx +15 -0
- package/src/hooks/useTimelineAssetDropOps.ts +4 -2
- package/src/hooks/useTimelineEditing.test.tsx +0 -1
- package/src/hooks/useTimelineSelectionPreviewSync.test.tsx +215 -3
- package/src/hooks/useTimelineSelectionPreviewSync.ts +63 -32
- package/src/player/components/ClipContextMenu.tsx +87 -7
- package/src/player/components/Timeline.tsx +14 -14
- package/src/player/components/TimelineCanvas.tsx +30 -0
- package/src/player/components/TimelineGestureOverlay.test.tsx +35 -1
- package/src/player/components/TimelineGestureOverlay.tsx +1 -1
- package/src/player/components/TimelineLanes.test.tsx +22 -1
- package/src/player/components/TimelineOverlays.tsx +12 -0
- package/src/player/components/TimelineTypes.ts +4 -0
- package/src/player/components/timelineAuthoredMoveTarget.ts +1 -2
- package/src/player/components/timelineClipChildren.test.ts +2 -2
- package/src/player/components/timelineClipChildren.tsx +2 -1
- package/src/player/components/timelineClipDragCommit.test.ts +114 -0
- package/src/player/components/timelineClipDragCommit.ts +20 -51
- package/src/player/components/timelineClipDragPreview.test.ts +173 -1
- package/src/player/components/timelineClipDragPreview.ts +8 -1
- package/src/player/components/timelineCollision.test.ts +23 -0
- package/src/player/components/timelineCollision.ts +27 -0
- package/src/player/components/timelineDragDrop.test.tsx +31 -0
- package/src/player/components/timelineDragDrop.ts +30 -23
- package/src/player/components/timelineDragLanding.ts +108 -0
- package/src/player/components/timelineDropReveal.test.ts +34 -0
- package/src/player/components/timelineDropReveal.ts +12 -0
- package/src/player/components/timelineEditCapabilities.ts +1 -2
- package/src/player/components/timelineEditing.test.ts +0 -16
- package/src/player/components/timelineGroupEditing.test.ts +1 -3
- package/src/player/components/timelineGroupEditing.ts +0 -1
- package/src/player/components/timelineTrackDisplay.ts +1 -1
- package/src/player/components/timelineTrackPersistPipeline.test.ts +0 -53
- package/src/player/components/useTimelineClipContextMenu.ts +32 -0
- package/src/player/components/useTimelineFocusCoordinator.test.tsx +66 -0
- package/src/player/components/useTimelineFocusCoordinator.ts +43 -3
- package/src/player/components/useTimelineGeometry.test.tsx +117 -0
- package/src/player/components/useTimelineGeometry.ts +7 -2
- package/src/player/components/useTimelineGeometryFocusOrder.test.tsx +184 -0
- package/src/player/components/useTimelineLogicalFocus.ts +1 -0
- package/src/player/components/useTimelineTrackDerivations.ts +1 -1
- package/src/player/hooks/timelineDomBinding.test.ts +0 -3
- package/src/player/hooks/timelineSyncHydration.test.ts +18 -0
- package/src/player/hooks/timelineSyncHydration.ts +37 -18
- package/src/player/hooks/useTimelinePlayer.test.ts +3 -20
- package/src/player/hooks/useTimelineRowElements.test.ts +28 -0
- package/src/player/hooks/useTimelineRowElements.ts +17 -0
- package/src/player/hooks/useTimelineSyncCallbacks.emptyManifest.test.tsx +41 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -10
- package/src/player/lib/describeClips.test.ts +95 -0
- package/src/player/lib/describeClips.ts +103 -0
- package/src/player/lib/timelineDOM.test.ts +0 -80
- package/src/player/lib/timelineDOM.ts +1 -61
- package/src/player/lib/timelineElementHelpers.ts +10 -53
- package/src/player/store/playerStore.ts +5 -0
- package/src/player/store/readinessSlice.ts +4 -3
- package/src/player/store/timelineElement.ts +1 -3
- package/src/shims/canvasBrowserStub.js +50 -0
- package/src/utils/blockInstaller.test.ts +4 -0
- package/src/utils/blockInstaller.ts +9 -3
- package/src/utils/clipboardPayload.test.ts +42 -2
- package/src/utils/clipboardPayload.ts +57 -32
- package/src/utils/previewSafeMargins.test.ts +34 -0
- package/src/utils/previewSafeMargins.ts +10 -0
- package/src/utils/razorSplitTransaction.test.ts +0 -1
- package/src/utils/retryBudget.ts +20 -0
- package/src/utils/sdkCanvasComposition.test.ts +77 -0
- package/src/utils/sdkCutover.familyGate.test.ts +5 -1
- package/src/utils/sdkCutover.ts +7 -4
- package/src/utils/sdkCutoverEligibility.test.ts +96 -1
- package/src/utils/sdkCutoverEligibility.ts +34 -11
- package/src/utils/studioUiPreferences.test.ts +43 -0
- package/src/utils/studioUiPreferences.ts +27 -6
- package/src/utils/timelineCompositionInsert.ts +2 -1
- package/src/utils/timelineElementSplit.test.ts +1 -2
- package/src/utils/timelineElementSplit.ts +0 -1
- package/src/webmcp/tools/lookTools.test.ts +40 -0
- package/src/webmcp/tools/lookTools.ts +32 -1
- package/dist/assets/hyperframes-player-CtVX6jL_.js +0 -464
- package/dist/assets/index-B9LfWNyM.css +0 -1
- package/src/components/StudioGlobalDragOverlay.tsx +0 -24
- package/src/player/hooks/useExpandedTimelineElements.test.ts +0 -774
- package/src/player/hooks/useExpandedTimelineElements.ts +0 -460
|
@@ -37,6 +37,21 @@ describe("studio UI preferences", () => {
|
|
|
37
37
|
});
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
+
it("remembers the ruler and safe-margin toggles and drops non-boolean values", () => {
|
|
41
|
+
const storage = createStorage();
|
|
42
|
+
writeStudioUiPreferences({ rulerVisible: true, safeMarginsVisible: false }, storage);
|
|
43
|
+
expect(readStudioUiPreferences(storage)).toEqual({
|
|
44
|
+
rulerVisible: true,
|
|
45
|
+
safeMarginsVisible: false,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
storage.setItem(
|
|
49
|
+
"hf-studio-ui-preferences",
|
|
50
|
+
JSON.stringify({ rulerVisible: "yes", safeMarginsVisible: 1 }),
|
|
51
|
+
);
|
|
52
|
+
expect(readStudioUiPreferences(storage)).toEqual({});
|
|
53
|
+
});
|
|
54
|
+
|
|
40
55
|
it("ignores malformed stored values", () => {
|
|
41
56
|
const storage = createStorage();
|
|
42
57
|
storage.setItem(
|
|
@@ -101,6 +116,34 @@ describe("thumbnailMode preference", () => {
|
|
|
101
116
|
});
|
|
102
117
|
});
|
|
103
118
|
|
|
119
|
+
describe("per-project scoping", () => {
|
|
120
|
+
it("keeps two projects' preferences independent", () => {
|
|
121
|
+
const storage = createStorage();
|
|
122
|
+
writeStudioUiPreferences({ leftWidth: 300 }, storage, "alpha");
|
|
123
|
+
writeStudioUiPreferences({ leftWidth: 500 }, storage, "beta");
|
|
124
|
+
|
|
125
|
+
expect(readStudioUiPreferences(storage, "alpha").leftWidth).toBe(300);
|
|
126
|
+
expect(readStudioUiPreferences(storage, "beta").leftWidth).toBe(500);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("a project with nothing saved yet inherits the pre-scoping global entry once", () => {
|
|
130
|
+
const storage = createStorage();
|
|
131
|
+
storage.setItem("hf-studio-ui-preferences", JSON.stringify({ leftWidth: 600 }));
|
|
132
|
+
|
|
133
|
+
expect(readStudioUiPreferences(storage, "gamma").leftWidth).toBe(600);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("stops inheriting the global entry once the project has its own write", () => {
|
|
137
|
+
const storage = createStorage();
|
|
138
|
+
storage.setItem("hf-studio-ui-preferences", JSON.stringify({ leftWidth: 600 }));
|
|
139
|
+
|
|
140
|
+
writeStudioUiPreferences({ leftWidth: 250 }, storage, "gamma");
|
|
141
|
+
|
|
142
|
+
expect(readStudioUiPreferences(storage, "gamma").leftWidth).toBe(250);
|
|
143
|
+
expect(readStudioUiPreferences(storage, null).leftWidth).toBe(600);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
104
147
|
describe("timeline zoom pin persistence", () => {
|
|
105
148
|
it("round-trips a pinned manual zoom (survives the post-edit reload)", () => {
|
|
106
149
|
const storage = createStorage();
|
|
@@ -20,6 +20,8 @@ export interface StudioUiPreferences {
|
|
|
20
20
|
recentBlocks?: string[];
|
|
21
21
|
snapEnabled?: boolean;
|
|
22
22
|
gridVisible?: boolean;
|
|
23
|
+
rulerVisible?: boolean;
|
|
24
|
+
safeMarginsVisible?: boolean;
|
|
23
25
|
gridSpacing?: number;
|
|
24
26
|
snapToGrid?: boolean;
|
|
25
27
|
/** Timeline magnet: snap clip drags/trims/drops to playhead, clip edges, and beats. */
|
|
@@ -62,11 +64,15 @@ function getBrowserStorage(): Storage | null {
|
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
function storageKeyFor(projectId: string | null): string {
|
|
68
|
+
return projectId ? `${STUDIO_UI_PREFERENCES_KEY}:${projectId}` : STUDIO_UI_PREFERENCES_KEY;
|
|
69
|
+
}
|
|
70
|
+
|
|
65
71
|
// fallow-ignore-next-line complexity
|
|
66
|
-
function readStorage(storage: Storage | null): StudioUiPreferences {
|
|
72
|
+
function readStorage(storage: Storage | null, key: string): StudioUiPreferences {
|
|
67
73
|
if (!storage) return {};
|
|
68
74
|
try {
|
|
69
|
-
const raw = storage.getItem(
|
|
75
|
+
const raw = storage.getItem(key);
|
|
70
76
|
if (!raw) return {};
|
|
71
77
|
const parsed: unknown = JSON.parse(raw);
|
|
72
78
|
if (!isRecord(parsed)) return {};
|
|
@@ -130,6 +136,12 @@ function readStorage(storage: Storage | null): StudioUiPreferences {
|
|
|
130
136
|
if (typeof parsed.gridVisible === "boolean") {
|
|
131
137
|
preferences.gridVisible = parsed.gridVisible;
|
|
132
138
|
}
|
|
139
|
+
if (typeof parsed.rulerVisible === "boolean") {
|
|
140
|
+
preferences.rulerVisible = parsed.rulerVisible;
|
|
141
|
+
}
|
|
142
|
+
if (typeof parsed.safeMarginsVisible === "boolean") {
|
|
143
|
+
preferences.safeMarginsVisible = parsed.safeMarginsVisible;
|
|
144
|
+
}
|
|
133
145
|
if (typeof parsed.gridSpacing === "number" && Number.isFinite(parsed.gridSpacing)) {
|
|
134
146
|
preferences.gridSpacing = parsed.gridSpacing;
|
|
135
147
|
}
|
|
@@ -163,21 +175,30 @@ function readStorage(storage: Storage | null): StudioUiPreferences {
|
|
|
163
175
|
}
|
|
164
176
|
}
|
|
165
177
|
|
|
166
|
-
|
|
167
|
-
|
|
178
|
+
/** `projectId` opts a caller into a per-project entry (falls back once to the
|
|
179
|
+
* shared entry so a project's first read isn't blank). Defaults to `null`:
|
|
180
|
+
* most callers read once at mount, never on a live project switch. */
|
|
181
|
+
export function readStudioUiPreferences(
|
|
182
|
+
storage: Storage | null = getBrowserStorage(),
|
|
183
|
+
projectId: string | null = null,
|
|
184
|
+
): StudioUiPreferences {
|
|
185
|
+
const scoped = readStorage(storage, storageKeyFor(projectId));
|
|
186
|
+
if (!projectId || Object.keys(scoped).length > 0) return scoped;
|
|
187
|
+
return readStorage(storage, STUDIO_UI_PREFERENCES_KEY);
|
|
168
188
|
}
|
|
169
189
|
|
|
170
190
|
export function writeStudioUiPreferences(
|
|
171
191
|
patch: StudioUiPreferences,
|
|
172
192
|
storage: Storage | null = getBrowserStorage(),
|
|
193
|
+
projectId: string | null = null,
|
|
173
194
|
) {
|
|
174
195
|
if (!storage) return;
|
|
175
196
|
try {
|
|
176
197
|
const next = {
|
|
177
|
-
...
|
|
198
|
+
...readStudioUiPreferences(storage, projectId),
|
|
178
199
|
...patch,
|
|
179
200
|
};
|
|
180
|
-
storage.setItem(
|
|
201
|
+
storage.setItem(storageKeyFor(projectId), JSON.stringify(next));
|
|
181
202
|
} catch {
|
|
182
203
|
/* localStorage may be unavailable or full */
|
|
183
204
|
}
|
|
@@ -3,6 +3,7 @@ import { serializeStudioFileMutation } from "./studioFileMutationCoordinator";
|
|
|
3
3
|
import type { RecordEditInput } from "./studioFileHistory";
|
|
4
4
|
import { buildProjectApiPath } from "./projectRouting";
|
|
5
5
|
import { studioWriteHeaders } from "./studioFileVersion";
|
|
6
|
+
import { deriveTimelineStoreKeyForDomId } from "../player/lib/timelineElementHelpers";
|
|
6
7
|
|
|
7
8
|
interface TimelineCompositionInsertionResult {
|
|
8
9
|
path: string;
|
|
@@ -76,7 +77,7 @@ export async function commitTimelineCompositionInsertion(input: {
|
|
|
76
77
|
await input.writeFile(input.targetPath, result.before, result.after);
|
|
77
78
|
throw error;
|
|
78
79
|
}
|
|
79
|
-
input.selectHost(
|
|
80
|
+
input.selectHost(deriveTimelineStoreKeyForDomId(result.hostId, input.targetPath));
|
|
80
81
|
try {
|
|
81
82
|
input.resync?.();
|
|
82
83
|
} catch (error) {
|
|
@@ -78,9 +78,8 @@ describe("canSplitElementAt", () => {
|
|
|
78
78
|
).toBe(false);
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
it("rejects locked
|
|
81
|
+
it("rejects locked elements while allowing identified compositions", () => {
|
|
82
82
|
expect(canSplitElementAt(element({ timelineLocked: true }), 3)).toBe(false);
|
|
83
|
-
expect(canSplitElementAt(element({ timingSource: "implicit" }), 3)).toBe(false);
|
|
84
83
|
expect(
|
|
85
84
|
canSplitElementAt(
|
|
86
85
|
element({ kind: "composition", compositionSrc: "child.html", playbackRate: 2 }),
|
|
@@ -27,7 +27,6 @@ export function canSplitElement(el: TimelineElement): boolean {
|
|
|
27
27
|
el.playbackRate == null || (Number.isFinite(el.playbackRate) && el.playbackRate > 0);
|
|
28
28
|
return (
|
|
29
29
|
!el.timelineLocked &&
|
|
30
|
-
el.timingSource !== "implicit" &&
|
|
31
30
|
hasStableIdentity &&
|
|
32
31
|
hasValidRate &&
|
|
33
32
|
!!el.duration &&
|
|
@@ -370,3 +370,43 @@ function sceneDoc(): Document {
|
|
|
370
370
|
</main>`;
|
|
371
371
|
return doc;
|
|
372
372
|
}
|
|
373
|
+
|
|
374
|
+
describe("buildStudioLook clips", () => {
|
|
375
|
+
const clip = (overrides: Partial<TimelineElement>) =>
|
|
376
|
+
element({ tag: "audio", kind: "audio", domId: "sfx", start: 2, duration: 0.5, ...overrides });
|
|
377
|
+
|
|
378
|
+
it("returns timeline clips that are not scene layers, audio included", () => {
|
|
379
|
+
const result = buildStudioLook(
|
|
380
|
+
snapshot({
|
|
381
|
+
elements: [clip({ domId: "late", start: 9 }), clip({ domId: "early", volume: 0.85 })],
|
|
382
|
+
}),
|
|
383
|
+
);
|
|
384
|
+
if (!result.ok) throw new Error("expected ok");
|
|
385
|
+
expect(result.clipCount).toBe(2);
|
|
386
|
+
expect(result.clips.map((c) => c.id)).toEqual(["early", "late"]);
|
|
387
|
+
expect(result.clips[0]).toMatchObject({ kind: "audio", start: 2, volume: 0.85 });
|
|
388
|
+
expect(result.elements).toEqual([]);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it("filters clips by id, kind or src", () => {
|
|
392
|
+
const result = buildStudioLook(
|
|
393
|
+
snapshot({
|
|
394
|
+
elements: [
|
|
395
|
+
clip({ domId: "click", src: "click.mp3" }),
|
|
396
|
+
clip({ domId: "vo", src: "voice.mp3" }),
|
|
397
|
+
],
|
|
398
|
+
}),
|
|
399
|
+
{ filter: "voice" },
|
|
400
|
+
);
|
|
401
|
+
if (!result.ok) throw new Error("expected ok");
|
|
402
|
+
expect(result.clips.map((c) => c.id)).toEqual(["vo"]);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("caps clips at the limit and reports the full count and truncation", () => {
|
|
406
|
+
const elements = Array.from({ length: 5 }, (_, i) => clip({ domId: `c${i}`, start: i }));
|
|
407
|
+
const result = buildStudioLook(snapshot({ elements }), { limit: 2 });
|
|
408
|
+
if (!result.ok) throw new Error("expected ok");
|
|
409
|
+
expect(result.clips.map((c) => c.id)).toEqual(["c0", "c1"]);
|
|
410
|
+
expect([result.clipCount, result.clipsTruncated]).toEqual([5, true]);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { collectDomEditLayerItems } from "../../components/editor/domEditingLayers";
|
|
12
12
|
import type { DomEditLayerItem, DomEditSelection } from "../../components/editor/domEditingTypes";
|
|
13
13
|
import type { TimelineElement } from "../../player/store/timelineElement";
|
|
14
|
+
import { byStart, describeClip, type ClipFact } from "../../player/lib/describeClips";
|
|
14
15
|
import { mintElementHandle, patchTargetAddress, timelineElementAddress } from "../handles";
|
|
15
16
|
import { toolOk, type ToolResult } from "../toolResult";
|
|
16
17
|
|
|
@@ -107,8 +108,15 @@ export interface StudioLook {
|
|
|
107
108
|
elementCount: number;
|
|
108
109
|
truncated: boolean;
|
|
109
110
|
elements: LookElement[];
|
|
111
|
+
/** Every timeline clip, audio included, whether or not it is a visual layer. */
|
|
112
|
+
clipCount: number;
|
|
113
|
+
clipsTruncated: boolean;
|
|
114
|
+
clips: LookClip[];
|
|
110
115
|
}
|
|
111
116
|
|
|
117
|
+
/** A timeline clip; `handle` is null when the clip has no addressable source element. */
|
|
118
|
+
type LookClip = ClipFact & { handle: string | null };
|
|
119
|
+
|
|
112
120
|
export interface StudioLookInput {
|
|
113
121
|
/** Case-insensitive substring match against label, tag, and handle. */
|
|
114
122
|
filter?: string;
|
|
@@ -154,6 +162,22 @@ function describeSelection(
|
|
|
154
162
|
};
|
|
155
163
|
}
|
|
156
164
|
|
|
165
|
+
function matchesClipFilter(clip: LookClip, needle: string): boolean {
|
|
166
|
+
return [clip.id, clip.label, clip.kind, clip.src].some((field) =>
|
|
167
|
+
field?.toLowerCase().includes(needle),
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function describeClipsWithHandles(snapshot: StudioLookSnapshot): LookClip[] {
|
|
172
|
+
const activeCompositionPath = snapshot.compositionPath ?? "index.html";
|
|
173
|
+
return snapshot.elements.map((element) => ({
|
|
174
|
+
...describeClip(element),
|
|
175
|
+
handle: mintElementHandle(
|
|
176
|
+
timelineElementAddress(element, activeCompositionPath, snapshot.projectId),
|
|
177
|
+
),
|
|
178
|
+
}));
|
|
179
|
+
}
|
|
180
|
+
|
|
157
181
|
function matchesFilter(element: LookElement, needle: string): boolean {
|
|
158
182
|
return (
|
|
159
183
|
element.label.toLowerCase().includes(needle) ||
|
|
@@ -258,6 +282,8 @@ export function buildStudioLook(
|
|
|
258
282
|
const requested =
|
|
259
283
|
Number.isInteger(input.limit) && input.limit! > 0 ? input.limit! : DEFAULT_LIMIT;
|
|
260
284
|
const limit = Math.min(requested, DEFAULT_LIMIT);
|
|
285
|
+
const clips = describeClipsWithHandles(snapshot).sort(byStart);
|
|
286
|
+
const matchedClips = needle ? clips.filter((clip) => matchesClipFilter(clip, needle)) : clips;
|
|
261
287
|
|
|
262
288
|
return toolOk<StudioLook>({
|
|
263
289
|
projectId: snapshot.projectId,
|
|
@@ -281,6 +307,9 @@ export function buildStudioLook(
|
|
|
281
307
|
elementCount: matched.length,
|
|
282
308
|
truncated: matched.length > limit,
|
|
283
309
|
elements: matched.slice(0, limit),
|
|
310
|
+
clipCount: matchedClips.length,
|
|
311
|
+
clipsTruncated: matchedClips.length > limit,
|
|
312
|
+
clips: matchedClips.slice(0, limit),
|
|
284
313
|
});
|
|
285
314
|
}
|
|
286
315
|
|
|
@@ -296,7 +325,7 @@ export const STUDIO_LOOK_INPUT_SCHEMA = {
|
|
|
296
325
|
type: "integer",
|
|
297
326
|
minimum: 1,
|
|
298
327
|
maximum: DEFAULT_LIMIT,
|
|
299
|
-
description: `Cap the returned elements (default and max ${DEFAULT_LIMIT}). elementCount always
|
|
328
|
+
description: `Cap the returned elements and clips (default and max ${DEFAULT_LIMIT} each). elementCount and clipCount always report the full match count.`,
|
|
300
329
|
},
|
|
301
330
|
},
|
|
302
331
|
additionalProperties: false,
|
|
@@ -307,6 +336,8 @@ export const STUDIO_LOOK_DESCRIPTION = [
|
|
|
307
336
|
"the playhead and duration, what the human currently has selected (including what that",
|
|
308
337
|
"element will and will not accept), and the live nested scene in DOM preorder.",
|
|
309
338
|
"Each scene element includes source ownership, hierarchy, and optional timeline timing.",
|
|
339
|
+
"`clips` lists every timeline clip, audio included: kind, start, duration, track, volume,",
|
|
340
|
+
"volume/effect automation points, playback rate, audio group and role.",
|
|
310
341
|
"Pass a handle back to any tool that edits an element.",
|
|
311
342
|
"Returns an object with `ok: true`, or `ok: false` with `kind`, `reason` and often a `hint`.",
|
|
312
343
|
"`history.undoLabel` is worth checkpointing before a batch: if it later names something",
|