契约先行(三端逐字节一致,已同步 windows/android):
- rest-api -> 0.5.0:新增 GET/POST /api/scenes、GET/PATCH/DELETE /api/scenes/{id}
- data-models -> 0.7.0:新增 §7 Scene 与 7.1 SceneConfig(物理/环境/出生点/相机)
- database -> 0.4.0:新增 scenes 表(config/stats 用 MEDIUMTEXT,迁移可逆)
后端:
- entities/scene.entity.ts + 迁移 AddScenes1789650000000(scenes 表)
- scenes/scene-config.ts:SceneConfig 定义 + 默认值 + **边界收敛**(越界夹紧、
非法枚举回退),保证客户端拿到的参数一定可解释;服务端只存参数不跑物理
- scenes/scenes.service.ts:上传(.glb/.gltf 直接导入;.zip/.pmx/.fbx 转 GLB,
场景不做 humanoid 重绑定、不转 VRM)、列表/详情/更新/软删除、缩略图复用模型
光栅化器、分析复用模型检查器、config 同时接受 JSON 字符串与平铺字段
- scenes/scenes.controller.ts + scenes.module.ts(app.module 注册)
- 审核复用 /api/reviews/:type/:id:TargetType.Scene 接入待审列表与状态流转
- catalog/export 增 scenes[],目录指纹纳入场景变更
- 枚举:TargetType.Scene、AssetKind.Scene / SceneThumbnail
测试:新增 test/scene-config.spec.ts 6 项(默认值/夹紧/枚举回退/出生点上限/颜色/
序列化);规模 19 文件 / 243 项;check-routes 35 条一致。版本 0.6.0。
注:管理端场景页(GLB 预览 + 物理参数可视化)与 Windows 场景选择留待下一轮。
133 lines
3.8 KiB
TypeScript
133 lines
3.8 KiB
TypeScript
/**
|
||
* 跨模块共享枚举。
|
||
* 与 contracts/data-models.md 中的枚举**逐字对应**,改动必须同步契约版本。
|
||
*/
|
||
|
||
export enum SourceType {
|
||
/** 原创 */
|
||
Original = 'original',
|
||
/** 已获授权 */
|
||
Licensed = 'licensed',
|
||
/** Creative Commons */
|
||
Cc = 'cc',
|
||
/** 公共领域 */
|
||
PublicDomain = 'public_domain',
|
||
/** 用户上传(需审核) */
|
||
UserUpload = 'user_upload',
|
||
/** 其他(须在 sourceName 说明) */
|
||
Other = 'other',
|
||
}
|
||
|
||
export enum PublishStatus {
|
||
Draft = 'draft',
|
||
Pending = 'pending',
|
||
Published = 'published',
|
||
Rejected = 'rejected',
|
||
}
|
||
|
||
export enum MotionFormat {
|
||
Bvh = 'bvh',
|
||
/** MikuMikuDance 动作数据(二进制,含 Shift-JIS 骨骼名与四元数关键帧) */
|
||
Vmd = 'vmd',
|
||
Fbx = 'fbx',
|
||
Gltf = 'gltf',
|
||
Glb = 'glb',
|
||
KeypointsJson = 'keypoints-json',
|
||
}
|
||
|
||
export enum ModelFormat {
|
||
Vrm = 'vrm',
|
||
Glb = 'glb',
|
||
Gltf = 'gltf',
|
||
Fbx = 'fbx',
|
||
}
|
||
|
||
export enum PreprocessState {
|
||
None = 'none',
|
||
Running = 'running',
|
||
Done = 'done',
|
||
Failed = 'failed',
|
||
}
|
||
|
||
export enum ReviewAction {
|
||
Submit = 'submit',
|
||
Approve = 'approve',
|
||
Reject = 'reject',
|
||
Withdraw = 'withdraw',
|
||
}
|
||
|
||
export enum TargetType {
|
||
Song = 'song',
|
||
Motion = 'motion',
|
||
Model = 'model',
|
||
/** 场景(T-WEB-015) */
|
||
Scene = 'scene',
|
||
}
|
||
|
||
export enum AssetKind {
|
||
SongAudio = 'song-audio',
|
||
SongCover = 'song-cover',
|
||
Motion = 'motion',
|
||
Model = 'model',
|
||
/** 上传的模型压缩包(zip)原件,仅用于溯源;入库的是转换后的模型文件 */
|
||
ModelArchive = 'model-archive',
|
||
Thumbnail = 'thumbnail',
|
||
/** 动作关键点序列的骨架缩略图(T-WEB-014) */
|
||
MotionThumbnail = 'motion-thumbnail',
|
||
/** 场景 GLB(T-WEB-015) */
|
||
Scene = 'scene',
|
||
/** 场景缩略图 */
|
||
SceneThumbnail = 'scene-thumbnail',
|
||
Keypoints = 'keypoints',
|
||
}
|
||
|
||
export const SOURCE_TYPE_VALUES = Object.values(SourceType) as string[];
|
||
export const PUBLISH_STATUS_VALUES = Object.values(PublishStatus) as string[];
|
||
export const MOTION_FORMAT_VALUES = Object.values(MotionFormat) as string[];
|
||
export const MODEL_FORMAT_VALUES = Object.values(ModelFormat) as string[];
|
||
export const TARGET_TYPE_VALUES = Object.values(TargetType) as string[];
|
||
|
||
/** 允许上传的扩展名 → 动作格式 */
|
||
export const MOTION_EXTENSION_MAP: Record<string, MotionFormat> = {
|
||
'.bvh': MotionFormat.Bvh,
|
||
'.vmd': MotionFormat.Vmd,
|
||
'.fbx': MotionFormat.Fbx,
|
||
'.gltf': MotionFormat.Gltf,
|
||
'.glb': MotionFormat.Glb,
|
||
'.json': MotionFormat.KeypointsJson,
|
||
};
|
||
|
||
/** 允许上传的扩展名 → 模型格式 */
|
||
export const MODEL_EXTENSION_MAP: Record<string, ModelFormat> = {
|
||
'.vrm': ModelFormat.Vrm,
|
||
'.glb': ModelFormat.Glb,
|
||
'.gltf': ModelFormat.Gltf,
|
||
'.fbx': ModelFormat.Fbx,
|
||
};
|
||
|
||
export const AUDIO_EXTENSIONS = ['.mp3', '.wav', '.ogg', '.m4a', '.aac', '.flac'];
|
||
export const IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.webp'];
|
||
|
||
/**
|
||
* 状态机:契约 database.md §状态机。
|
||
* key = 当前状态,value = 允许流转到的状态集合。
|
||
*/
|
||
export const STATUS_TRANSITIONS: Record<PublishStatus, PublishStatus[]> = {
|
||
[PublishStatus.Draft]: [PublishStatus.Pending, PublishStatus.Published],
|
||
[PublishStatus.Pending]: [PublishStatus.Published, PublishStatus.Rejected, PublishStatus.Draft],
|
||
[PublishStatus.Published]: [PublishStatus.Draft, PublishStatus.Rejected],
|
||
[PublishStatus.Rejected]: [PublishStatus.Pending, PublishStatus.Draft],
|
||
};
|
||
|
||
export function canTransition(from: PublishStatus, to: PublishStatus): boolean {
|
||
return (STATUS_TRANSITIONS[from] ?? []).includes(to);
|
||
}
|
||
|
||
/** 审核动作 → 目标状态 */
|
||
export const ACTION_TARGET_STATUS: Record<ReviewAction, PublishStatus> = {
|
||
[ReviewAction.Submit]: PublishStatus.Pending,
|
||
[ReviewAction.Approve]: PublishStatus.Published,
|
||
[ReviewAction.Reject]: PublishStatus.Rejected,
|
||
[ReviewAction.Withdraw]: PublishStatus.Draft,
|
||
};
|