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

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

View File

@@ -95,6 +95,10 @@ export interface DirectorAssetRef {
* 用户在画布上双击地面打点,新建时定位只取 x / z(y 固定为 0),所以默认轨迹是贴地平面的;
* rotation 是该路点处角色的朝向,单位是度,顺序与 DirectorTransform 一致:
* [俯仰角 pitch(X), 水平角 yaw(Y), 横滚角 roll(Z)]。
* 语义(角色正面朝 +Z、头顶朝 +Y):
* - 俯仰角 +90° = 趴着(脸朝下)、-90° = 躺着(脸朝上)
* - 水平角 0–360° 是一整圈(360° ≡ 0°)
* - 横滚角 -180° – 180° 是一整圈(180° ≡ -180°)
*/
export interface DirectorTrajectoryPoint {
id: string;
@@ -108,6 +112,88 @@ export interface DirectorTrajectoryPointRef {
pointId: string;
}
/**
* 轨迹点朝向角的取值范围(度),下标与 DirectorTrajectoryPoint.rotation 一致:
* - 0 俯仰角 pitch(X):-90 – 90(趴着 ↔ 躺着,物理限位,不是整圈)
* - 1 水平角 yaw(Y):0 – 360(绕身体竖直轴转一整圈)
* - 2 横滚角 roll(Z):-180 – 180(绕身体前后轴侧倾一整圈)
*/
export const TRAJECTORY_ROTATION_LIMITS: Record<0 | 1 | 2, { min: number; max: number }> = {
0: { min: -90, max: 90 },
1: { min: 0, max: 360 },
2: { min: -180, max: 180 },
};
export const DEGREES_TO_RADIANS = Math.PI / 180;
export const RADIANS_TO_DEGREES = 180 / Math.PI;
export function degreesToRadians(degrees: number): number {
return degrees * DEGREES_TO_RADIANS;
}
export function radiansToDegrees(radians: number): number {
return radians * RADIANS_TO_DEGREES;
}
export function degreesRotationToRadians(rotation: [number, number, number]): [number, number, number] {
return [degreesToRadians(rotation[0]), degreesToRadians(rotation[1]), degreesToRadians(rotation[2])];
}
export function radiansRotationToDegrees(rotation: [number, number, number]): [number, number, number] {
return [radiansToDegrees(rotation[0]), radiansToDegrees(rotation[1]), radiansToDegrees(rotation[2])];
}
/**
* 把角度按整圈收敛到 [min, max];端点保留,避免滑块拖到尽头又跳回另一端:
* - 区间内的值原样返回;
* - 大于 max 按 (value - min) % (max - min) + min 绕回;
* - 小于 min 按 max - (min - value) % (max - min) 绕回。
* 非法值(NaN / 空)按 0 处理(0 落在区间内时)。
*/
export function wrapAngleIntoRange(value: number, min: number, max: number): number {
if (!Number.isFinite(value)) return min <= 0 && max >= 0 ? 0 : min;
const span = max - min;
if (value > max) return ((value - min) % span) + min;
if (value < min) return max - ((min - value) % span);
return value;
}
/**
* 把轨迹点朝向角归一化到合法区间:
* - 俯仰角(轴 0)是物理限位 [-90, 90],越界直接夹(趴着 / 躺着是端点,不是整圈)。
* - 水平角 / 横滚角是整圈,越界按模收敛(360° ≡ 0°、181° ≡ -179°),端点保留。
* - 非法值按 0 处理。
*/
export function normalizeTrajectoryRotation(axis: 0 | 1 | 2, value: number): number {
if (!Number.isFinite(value)) return 0;
if (axis === 0) {
const { min, max } = TRAJECTORY_ROTATION_LIMITS[0];
return Math.min(max, Math.max(min, value));
}
const { min, max } = TRAJECTORY_ROTATION_LIMITS[axis];
return wrapAngleIntoRange(value, min, max);
}
/** 角色 / 道具 / 群众变换旋转的取值范围(度):每个轴都是 0–360° 一整圈。 */
export const TRANSFORM_ROTATION_LIMITS = { min: 0, max: 360 } as const;
/**
* 把对象变换旋转归一化为「每个轴 0–360° 一整圈」:越界按模绕回,端点保留。
* 对象旋转是通用欧拉角,不区分俯仰/横滚语义;与轨迹点一致,存入的是度数。
*/
export function normalizeTransformRotation(rotation: [number, number, number]): [number, number, number] {
return [
wrapAngleIntoRange(rotation[0], TRANSFORM_ROTATION_LIMITS.min, TRANSFORM_ROTATION_LIMITS.max),
wrapAngleIntoRange(rotation[1], TRANSFORM_ROTATION_LIMITS.min, TRANSFORM_ROTATION_LIMITS.max),
wrapAngleIntoRange(rotation[2], TRANSFORM_ROTATION_LIMITS.min, TRANSFORM_ROTATION_LIMITS.max),
];
}
export interface DirectorObject {
id: string;
name: string;