feat: 旋转单位改为度并支持轨迹点拖拽

统一数据模型中的旋转为度数并在渲染时转弧度,新增 InspectorAngleField 角度控件,为场景与轨迹点提供滑块和重置按钮;选中轨迹点时角色预览到该点并支持拖拽 gizmo 调整位置;机位点击统一走 selectObject 以正确切换右侧属性卡片。
This commit is contained in:
tongtong committed 2026-09-17 11:18:58 +08:00
1 parent e74cf6d28b
commit 0794486857
21 files changed
+659 -99

No files matched your search

+57 -7
View File
@@ -20,6 +20,10 @@ import type {
DirectorObject,
GeometryPrimitiveType,
} from "../schema/directorProject";
import {
degreesRotationToRadians,
radiansRotationToDegrees,
} from "../schema/directorProject";
import {
VIEWPORT_CAMERA_ASPECT,
VIEWPORT_CAMERA_FRUSTUM_DEPTH,
@@ -494,6 +498,14 @@ function ObjectSceneNode({
const updateObjectTransform = useDirectorStore((state) => state.updateObjectTransform);
const isPlaying = usePlaybackStore((state) => state.isPlaying);
const motionTime = usePlaybackStore((state) => state.playhead);
// 当前选中的轨迹点若属于本角色,则把角色「预览」到该路点的位置与朝向上,
// 让用户直接看到角色在某个轨迹点的姿态(水平角/俯仰角/横滚角)。
// 播放时角色要沿路径动画,故预览只在非播放时生效(拖拽 gizmo 也已排除播放态)。
const selectedTrajectoryPoint = useDirectorStore((state) => state.selectedTrajectoryPoint);
const activeTrajectoryPoint =
!isPlaying && selectedTrajectoryPoint?.objectId === item.id
? item.trajectoryPoints?.find((point) => point.id === selectedTrajectoryPoint.pointId)
: undefined;
const isImportedModel = asset?.sourceType === "model";
const characterLabelKey = `${item.id}:${item.bodyType ?? ""}:${item.characterRig?.rigType ?? ""}`;
const fallbackCharacterLabelY =
@@ -526,9 +538,10 @@ function ObjectSceneNode({
const group = groupRef.current;
if (!group) return;
// group.rotation 是弧度,存入数据模型前要转成度数(数据模型里旋转一律用度数)
updateObjectTransform(item.id, {
position: [group.position.x, group.position.y, group.position.z],
rotation: [group.rotation.x, group.rotation.y, group.rotation.z],
rotation: radiansRotationToDegrees([group.rotation.x, group.rotation.y, group.rotation.z]),
scale: [group.scale.x, group.scale.y, group.scale.z],
});
}
@@ -536,8 +549,10 @@ function ObjectSceneNode({
const node = (
<group
ref={groupRef}
position={item.transform.position}
rotation={item.transform.rotation}
position={activeTrajectoryPoint ? activeTrajectoryPoint.position : item.transform.position}
// 数据模型里旋转是度数,应用到 Three.js 的 rotation 属性(弧度)前要转换,
// 否则 360° 会被当成 360 弧度 ≈ 57 圈。
rotation={degreesRotationToRadians(activeTrajectoryPoint ? activeTrajectoryPoint.rotation : item.transform.rotation)}
scale={item.transform.scale}
userData={{ directorObjectId: item.id }}
onClick={(event) => {
@@ -571,7 +586,9 @@ function ObjectSceneNode({
</group>
);
const showGizmo = selected && transformable && !isPlaying;
// 选中了本角色的某个轨迹点时,角色被预览到该路点上,此时不再显示角色自身的变换 gizmo,
// 避免和轨迹点的拖拽 gizmo 重叠;拖拽由轨迹点 gizmo 负责。
const showGizmo = selected && transformable && !isPlaying && !activeTrajectoryPoint;
if (!showGizmo) return node;
@@ -612,12 +629,13 @@ function CrowdTransformRig({
const group = groupRef.current;
if (!group) return;
// group.rotation 是弧度,存入数据模型前要转成度数
updateCrowdTransform(crowdId, {
position: [group.position.x, group.position.y, group.position.z],
rotation: [group.rotation.x, group.rotation.y, group.rotation.z],
rotation: radiansRotationToDegrees([group.rotation.x, group.rotation.y, group.rotation.z]),
scale: [group.scale.x, group.scale.y, group.scale.z],
});
}
}
if (!selected || !transformable || !crowdAnchor) return null;
@@ -626,7 +644,7 @@ function CrowdTransformRig({
<group
ref={groupRef}
position={crowdAnchor.position}
rotation={crowdAnchor.rotation}
rotation={degreesRotationToRadians(crowdAnchor.rotation)}
scale={crowdAnchor.scale}
/>
<ViewportTransformControls
@@ -847,8 +865,14 @@ function TrajectoryPath({
interactive: boolean;
}) {
const selectTrajectoryPoint = useDirectorStore((state) => state.selectTrajectoryPoint);
const updateTrajectoryPoint = useDirectorStore((state) => state.updateTrajectoryPoint);
const isPlaying = usePlaybackStore((state) => state.isPlaying);
const translationSnap = useDirectorStore((state) => (state.project.scene.snapToGrid ? 1 : null));
const selectedGroupRef = useRef<Group>(null!);
const points = owner.trajectoryPoints ?? [];
const color = owner.color ?? TRAJECTORY_FALLBACK_COLOR;
// 只有在「可交互(非打点模式)且非播放中」且确实有选中点时,才给选中点挂拖拽 gizmo
const draggable = interactive && !isPlaying && Boolean(selectedPointId);
/** 平滑曲线采样点:从「角色自身位置」出发,依次穿过每个路点(Catmull-Rom 样条,
* centripetal 型避免路径自交打环)。这样轨迹是从角色身上延伸出来的,而不是凭空
@@ -881,6 +905,21 @@ function TrajectoryPath({
selectTrajectoryPoint(owner.id, pointId);
}
/**
* 拖拽过程中实时把 gizmo 移动后的坐标写回选中的轨迹点:
* 路点组在场景组局部坐标里抬高了 GROUND_GRID_ELEVATION(小球贴地用),
* 这里把抬升减回去,存回的是轨迹语义坐标(y 默认 0 = 贴地)。
* 写回后「角色预览」会跟着同一份坐标实时移动,所见即所得。
*/
function commitSelectedPoint() {
const group = selectedGroupRef.current;
if (!group || !selectedPointId) return;
updateTrajectoryPoint(owner.id, selectedPointId, {
position: [group.position.x, group.position.y - GROUND_GRID_ELEVATION, group.position.z],
});
}
return (
<group name={`trajectory-${owner.id}`} userData={{ [HIDE_FROM_VIEWPORT_CAPTURE_KEY]: true }}>
{sampledCurve ? (
@@ -898,6 +937,7 @@ function TrajectoryPath({
return (
<group
key={point.id}
ref={selected ? selectedGroupRef : undefined}
position={[point.position[0], point.position[1] + GROUND_GRID_ELEVATION, point.position[2]]}
>
{/* 透明大圆盘只为了让小球更好点中(俯视时点地面圆盘比点小球容易) */}
@@ -921,6 +961,16 @@ function TrajectoryPath({
</group>
);
})}
{/* 选中点挂拖拽 gizmo:拖到哪,角色预览就跟到哪。打点模式 / 播放中 / 无选中点时不挂。 */}
{draggable ? (
<ViewportTransformControls
mode="translate"
object={selectedGroupRef}
onObjectChange={commitSelectedPoint}
translationSnap={translationSnap}
editingId={owner.id}
/>
) : null}
</group>
);
}