fix: 优化空格播放和截图角色标签对不上
This commit is contained in:
@@ -70,6 +70,11 @@ type TimelineTimeDragOwner =
|
||||
| { kind: "trajectory"; objectId: string; pointId: string }
|
||||
| { kind: "camera"; cameraId: string };
|
||||
|
||||
/** 时间轴片段(人物轨迹条 / 运镜片段条)的稳定标识 */
|
||||
function getTimelineClipKey(kind: "trajectory" | "camera", id: string) {
|
||||
return `${kind}:${id}`;
|
||||
}
|
||||
|
||||
function formatTime(t: number) {
|
||||
return `${t.toFixed(1)}s`;
|
||||
}
|
||||
@@ -335,6 +340,8 @@ export function TimelinePanel() {
|
||||
const editableDuration = project.timeline.duration ?? duration;
|
||||
|
||||
const [selectedKeyframe, setSelectedKeyframe] = useState<SelectedKeyframe | null>(null);
|
||||
/** 选中的片段(轨迹条 / 运镜条):点条本体或条上的关键帧都会选中它 */
|
||||
const [selectedClip, setSelectedClip] = useState<string | null>(null);
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [exportMenuOpen, setExportMenuOpen] = useState(false);
|
||||
// 设置弹窗:fps / loop 先放草稿里改,点「完成」才写回工程(不点则原样丢弃)
|
||||
@@ -758,6 +765,29 @@ export function TimelinePanel() {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 点轨道空白处 = 取消片段选中。
|
||||
* 片段 / 关键帧自己的手势里会 stopPropagation,正常情况冒泡不到这里;
|
||||
* 但手势可能因为尺寸未就绪等原因提前返回(那时还没走到 stopPropagation),
|
||||
* 所以这里再按事件来源兜一层,避免把刚选中的片段立刻清掉。
|
||||
*/
|
||||
function handleTimelineGridPointerDown(event: ReactPointerEvent<HTMLDivElement>) {
|
||||
if (event.target instanceof Element && event.target.closest(".timeline-trajectory-bar, .timeline-keyframe")) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedClip(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 片段是否处于选中态。只有一个来源:`selectedClip` ——
|
||||
* 任何一次片段手势(拖条本体 / 拖把手 / 拖条上的关键帧)都会把这条片段标成选中,
|
||||
* 点轨道空白处取消。不跟关键帧 / 轨迹点选中做联动,是为了让「点空白处取消选中」始终有效。
|
||||
*/
|
||||
function isClipSelected(kind: "trajectory" | "camera", id: string) {
|
||||
return selectedClip === getTimelineClipKey(kind, id);
|
||||
}
|
||||
|
||||
/** 拖整段时长(条右端把手 / 键盘微调):末帧落到 newEnd,其余关键帧按比例缩放,片段起点固定 */
|
||||
function scaleClipDuration(
|
||||
owner: TimelineTimeDragOwner,
|
||||
@@ -837,6 +867,9 @@ export function TimelinePanel() {
|
||||
if (event.button !== 0) return;
|
||||
if (isTrackDragBlockedTarget(event.target)) return;
|
||||
|
||||
// 任何一次片段手势(拖条本体 / 拖把手 / 拖关键帧)都先把这段片段标成选中
|
||||
setSelectedClip(getTimelineClipKey(owner.kind, owner.kind === "trajectory" ? owner.objectId : owner.cameraId));
|
||||
|
||||
const lane = event.currentTarget.closest(".timeline-track-lane");
|
||||
const laneWidth = lane instanceof HTMLElement ? lane.getBoundingClientRect().width : 0;
|
||||
if (laneWidth <= 0 || duration <= 0) return;
|
||||
@@ -1077,6 +1110,18 @@ export function TimelinePanel() {
|
||||
action();
|
||||
}
|
||||
|
||||
// 焦点在时间轴上时,空格键切换播放 / 暂停(正在播放则暂停,暂停或停止则播放)。
|
||||
// 焦点落在原生可激活控件(按钮 / 输入框等)上时交给控件自身处理,避免与它们的空格行为重复触发。
|
||||
function handlePanelKeyDown(event: ReactKeyboardEvent<HTMLElement>) {
|
||||
if (event.key !== " " && event.key !== "Spacebar") return;
|
||||
const target = event.target as HTMLElement | null;
|
||||
if (target && target.closest("button, input, textarea, select, [contenteditable='true']")) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
togglePlaying();
|
||||
}
|
||||
|
||||
const addKeyframeDisabled = selectionIsManualCamera ? !canAddCameraKeyframe : !selectedObject;
|
||||
const addKeyframeTitle = selectionIsManualCamera
|
||||
? canAddCameraKeyframe
|
||||
@@ -1091,8 +1136,11 @@ export function TimelinePanel() {
|
||||
className={`timeline-panel${draggingTrackKey ? " is-track-dragging" : ""}`}
|
||||
aria-label="时间轴"
|
||||
ref={panelRef}
|
||||
onKeyDown={handlePanelKeyDown}
|
||||
>
|
||||
<div className="timeline-toolbar">
|
||||
{/* tabIndex 让工具栏区可聚焦:点工具栏空白处后焦点落进面板,
|
||||
这样「焦点在 timeline-toolbar 时空格切换播放/暂停」才生效(否则焦点在面板外,空格够不到面板级键盘处理) */}
|
||||
<div className="timeline-toolbar" tabIndex={0}>
|
||||
<div className="timeline-toolbar-group">
|
||||
{/* 读数 = 当前帧号 / 当前时间;总时长单独放在右边的时长输入框里 */}
|
||||
<span
|
||||
@@ -1335,7 +1383,10 @@ export function TimelinePanel() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="timeline-grid-body">
|
||||
{/* 点轨道空白处(不含片段与关键帧)=取消片段选中 */}
|
||||
{/* tabIndex 让整块轨道区可聚焦:点轨道空白处后焦点会落进面板,
|
||||
这样「焦点在轨道上时空格切换播放/暂停」才能生效(否则焦点在面板外,空格够不到面板级的键盘处理) */}
|
||||
<div className="timeline-grid-body" tabIndex={0} onPointerDown={handleTimelineGridPointerDown}>
|
||||
{timeline.tracks.map((track) => {
|
||||
const obj = project.objects.find((o) => o.id === track.objectId);
|
||||
const KindIcon = (obj ? OBJECT_KIND_ICONS[obj.kind] : undefined) ?? Box;
|
||||
@@ -1442,7 +1493,9 @@ export function TimelinePanel() {
|
||||
拖右端把手整体缩放这段运镜的时长;只有一个关键帧时没有「运镜过程」,不画条。 */}
|
||||
{cameraTimes.length >= 2 && duration > 0 ? (
|
||||
<div
|
||||
className="timeline-trajectory-bar timeline-clip-bar-camera"
|
||||
className={`timeline-trajectory-bar timeline-clip-bar-camera${
|
||||
isClipSelected("camera", track.cameraId) ? " is-selected" : ""
|
||||
}`}
|
||||
style={{ left: `${clipLeftPercent}%`, width: `${clipWidthPercent}%` }}
|
||||
title={`运镜片段 ${formatTime(clipStart)} → ${formatTime(
|
||||
clipEnd
|
||||
@@ -1595,7 +1648,9 @@ export function TimelinePanel() {
|
||||
{trajectoryTimes.length > 0 && duration > 0 ? (
|
||||
<>
|
||||
<div
|
||||
className="timeline-trajectory-bar"
|
||||
className={`timeline-trajectory-bar${
|
||||
isClipSelected("trajectory", character.id) ? " is-selected" : ""
|
||||
}`}
|
||||
style={{ left: `${trajectoryLeftPercent}%`, width: `${trajectoryWidthPercent}%` }}
|
||||
title={`轨迹片段 ${formatTime(trajectoryStart)} → ${formatTime(
|
||||
trajectoryEndTime
|
||||
|
||||
Reference in New Issue
Block a user