feat(timeline): 支持角色轨迹关键帧与轨道拖拽排序

This commit is contained in:
2026-09-17 14:47:29 +08:00
parent 0794486857
commit bab92c05b8
18 changed files with 1862 additions and 135 deletions

View File

@@ -30,6 +30,7 @@ import {
VIEWPORT_CAMERA_VISUAL_SCALE,
} from "../schema/cameraGeometry";
import { VIEWPORT_OBJECT_LABEL_VERTICAL_GAP } from "../schema/viewportLabels";
import { evaluateTrajectoryMotionAtTime, getTrajectoryPointTime } from "../schema/trajectoryMotion";
import type { TransformMode } from "../store/directorStore";
import { useDirectorStore } from "../store/directorStore";
import { usePlaybackStore } from "../runtime/playbackStore";
@@ -498,14 +499,20 @@ function ObjectSceneNode({
const updateObjectTransform = useDirectorStore((state) => state.updateObjectTransform);
const isPlaying = usePlaybackStore((state) => state.isPlaying);
const motionTime = usePlaybackStore((state) => state.playhead);
// 当前选中的轨迹点若属于本角色,则把角色「预览」到该路点的位置与朝向上,
// 让用户直接看到角色在某个轨迹点的姿态(水平角/俯仰角/横滚角)。
// 播放时角色要沿路径动画,故预览只在非播放时生效(拖拽 gizmo 也已排除播放态)。
const playbackDuration = usePlaybackStore((state) => state.duration);
// 当前选中的轨迹点若属于本角色,则抑制角色自身的变换 gizmo(拖拽交给轨迹点 gizmo)。
const selectedTrajectoryPoint = useDirectorStore((state) => state.selectedTrajectoryPoint);
const activeTrajectoryPoint =
!isPlaying && selectedTrajectoryPoint?.objectId === item.id
? item.trajectoryPoints?.find((point) => point.id === selectedTrajectoryPoint.pointId)
: undefined;
// 带轨迹的角色:位置 / 朝向由「人物轨迹 + 播放头」决定。
// 这里把同一个结果也算进 props,和 PlaybackTransformSync 每帧的写入保持一致,
// 否则 React 每次重渲染都会把角色拉回起始姿态,和轨迹走位互相打架(表现为抖动/闪回)。
const trajectoryMotion = useMemo(
() => evaluateTrajectoryMotionAtTime(item, playbackDuration, motionTime),
[item, playbackDuration, motionTime]
);
const isImportedModel = asset?.sourceType === "model";
const characterLabelKey = `${item.id}:${item.bodyType ?? ""}:${item.characterRig?.rigType ?? ""}`;
const fallbackCharacterLabelY =
@@ -549,10 +556,10 @@ function ObjectSceneNode({
const node = (
<group
ref={groupRef}
position={activeTrajectoryPoint ? activeTrajectoryPoint.position : item.transform.position}
position={trajectoryMotion ? trajectoryMotion.position : item.transform.position}
// 数据模型里旋转是度数,应用到 Three.js 的 rotation 属性(弧度)前要转换,
// 否则 360° 会被当成 360 弧度 ≈ 57 圈。
rotation={degreesRotationToRadians(activeTrajectoryPoint ? activeTrajectoryPoint.rotation : item.transform.rotation)}
rotation={degreesRotationToRadians(trajectoryMotion ? trajectoryMotion.rotation : item.transform.rotation)}
scale={item.transform.scale}
userData={{ directorObjectId: item.id }}
onClick={(event) => {
@@ -867,6 +874,9 @@ function TrajectoryPath({
const selectTrajectoryPoint = useDirectorStore((state) => state.selectTrajectoryPoint);
const updateTrajectoryPoint = useDirectorStore((state) => state.updateTrajectoryPoint);
const isPlaying = usePlaybackStore((state) => state.isPlaying);
const setPlaying = usePlaybackStore((state) => state.setPlaying);
const setPlayhead = usePlaybackStore((state) => state.setPlayhead);
const playbackDuration = usePlaybackStore((state) => state.duration);
const translationSnap = useDirectorStore((state) => (state.project.scene.snapToGrid ? 1 : null));
const selectedGroupRef = useRef<Group>(null!);
const points = owner.trajectoryPoints ?? [];
@@ -903,6 +913,14 @@ function TrajectoryPath({
function selectPoint(pointId: string, event: ThreeEvent<MouseEvent>) {
event.stopPropagation();
selectTrajectoryPoint(owner.id, pointId);
// 轨迹点本身就是时间轴上的关键帧:选中即把指针定位到该点时间,
// 角色按「沿轨迹行走」的插值正好预览到这一点的位置与朝向(0s = 角色起始位置)。
const pointIndex = points.findIndex((point) => point.id === pointId);
if (pointIndex < 0) return;
setPlaying(false);
setPlayhead(getTrajectoryPointTime(pointIndex, points.length, playbackDuration));
}
/**