Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/studio/src/components/StudioPreviewArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import { NLELayout } from "./nle/NLELayout";
import { CaptionOverlay } from "../captions/components/CaptionOverlay";
import { CaptionTimeline } from "../captions/components/CaptionTimeline";
import { DomEditOverlay } from "./editor/DomEditOverlay";
import { MotionPathOverlay } from "./editor/MotionPathOverlay";
import { useCompositionDimensions } from "../hooks/useCompositionDimensions";
import { SnapToolbar } from "./editor/SnapToolbar";
import { StudioFeedbackBar } from "./StudioFeedbackBar";
import type { TimelineElement } from "../player";
import { usePlayerStore } from "../player/store/playerStore";
import type { BlockedTimelineEditIntent } from "../player/components/timelineEditing";
import {
STUDIO_INSPECTOR_PANELS_ENABLED,
STUDIO_KEYFRAMES_ENABLED,
STUDIO_PREVIEW_MANUAL_EDITING_ENABLED,
STUDIO_PREVIEW_SELECTION_ENABLED,
} from "./editor/manualEditingAvailability";
Expand Down Expand Up @@ -108,6 +111,7 @@ export function StudioPreviewArea({
isPlaying,
refreshPreviewDocumentVersion,
} = useStudioPlaybackContext();
const compositionDimensions = useCompositionDimensions();

const {
domEditHoverSelection,
Expand Down Expand Up @@ -337,6 +341,14 @@ export function StudioPreviewArea({
onToggleRecording={onToggleRecording}
/>
<SnapToolbar onSnapChange={setSnapPrefs} />
{STUDIO_KEYFRAMES_ENABLED && (
<MotionPathOverlay
iframeRef={previewIframeRef}
selection={shouldShowSelectedDomBounds ? domEditSelection : null}
compositionSize={compositionDimensions}
isPlaying={isPlaying}
/>
)}
{gestureOverlay}
</>
) : null
Expand Down
1 change: 1 addition & 0 deletions packages/studio/src/components/editor/DomEditOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
if (!selection) return "none";
return `${selection.sourceFile}:${selection.id ?? selection.selector ?? selection.label}:${selection.selectorIndex ?? 0}`;
}, [selection]);

const groupBounds = useMemo(
() => resolveDomEditGroupOverlayRect(groupOverlayItems.map((item) => item.rect)),
[groupOverlayItems],
Expand Down
98 changes: 98 additions & 0 deletions packages/studio/src/components/editor/MotionPathNode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type React from "react";

// Editor primary color (themeable via --hf-accent). Applied through inline
// style because CSS var() isn't valid in SVG presentation attributes.
export const ACCENT = "var(--hf-accent, #3CE6AC)";

/** One path node: a diamond (matching the timeline keyframe), a wider transparent
* grab target (when editable), and a hover-revealed × delete badge (when removable). */
export function MotionPathNode(props: {
cx: number;
cy: number;
r: number;
interactive: boolean;
removable: boolean;
grabbing: boolean;
selected: boolean;
onEnter: () => void;
onLeave: () => void;
onPointerDown: (e: React.PointerEvent) => void;
onPointerMove: (e: React.PointerEvent) => void;
onPointerUp: (e: React.PointerEvent) => void;
onRemove: (e: React.PointerEvent) => void;
onContextMenu?: (e: React.MouseEvent) => void;
}) {
const { cx, cy, r, interactive, removable, grabbing, selected } = props;
const bx = cx + r * 1.8;
const by = cy - r * 1.8;
const k = r * 0.55;
// Diamond matching the timeline keyframe (a 45°-rotated rounded square).
// `side` is chosen so the diamond's points reach ~`r` from center, matching the
// old dot's footprint; selection is shown by enlarging it (no extra shape).
const side = (selected ? r * 1.5 : r) * 1.414;
return (
<g onPointerEnter={props.onEnter} onPointerLeave={props.onLeave}>
<rect
x={cx - side / 2}
y={cy - side / 2}
width={side}
height={side}
rx={side * 0.17}
transform={`rotate(45 ${cx} ${cy})`}
stroke="#0b0f1a"
strokeWidth={1.5}
vectorEffect="non-scaling-stroke"
style={{ fill: ACCENT }}
/>
{interactive && (
<circle
cx={cx}
cy={cy}
r={r * 2.4}
fill="transparent"
className="pointer-events-auto"
style={{ cursor: grabbing ? "grabbing" : "grab" }}
onPointerDown={props.onPointerDown}
onPointerMove={props.onPointerMove}
onPointerUp={props.onPointerUp}
onContextMenu={props.onContextMenu}
/>
)}
{removable && (
<g
className="pointer-events-auto"
style={{ cursor: "pointer" }}
onPointerDown={props.onRemove}
>
<circle
cx={bx}
cy={by}
r={r * 1.3}
stroke="#0b0f1a"
strokeWidth={1}
vectorEffect="non-scaling-stroke"
style={{ fill: ACCENT }}
/>
<line
x1={bx - k}
y1={by - k}
x2={bx + k}
y2={by + k}
stroke="#0b0f1a"
strokeWidth={1.5}
vectorEffect="non-scaling-stroke"
/>
<line
x1={bx + k}
y1={by - k}
x2={bx - k}
y2={by + k}
stroke="#0b0f1a"
strokeWidth={1.5}
vectorEffect="non-scaling-stroke"
/>
</g>
)}
</g>
);
}
Loading
Loading