335 lines
11 KiB
TypeScript
335 lines
11 KiB
TypeScript
import {reactive, toRefs, h,computed} from "vue";
|
||
import {NIcon} from 'naive-ui';
|
||
import type {TreeOption} from 'naive-ui'
|
||
import {defineStore} from 'pinia';
|
||
import {ConditionPoint} from '@vicons/carbon';
|
||
import {store} from '@/store';
|
||
import {t} from "@/language";
|
||
import type {TimelineTrack,ITimelineRow} from "@astral3d/engine";
|
||
import {App} from "@astral3d/engine";
|
||
|
||
export interface IAnimationItem {
|
||
name: string,
|
||
uuid: string,
|
||
isRunning: boolean,
|
||
isPaused: boolean,
|
||
}
|
||
|
||
interface IAnimationState {
|
||
// 选中模型的动画列表
|
||
list: Array<IAnimationItem>,
|
||
// 动画编辑器当前选中的动画
|
||
current: IAnimationItem | null,
|
||
// 当前选中动画的所有轨道
|
||
trackTree: TreeOption[],
|
||
// 全局的动画播放速率
|
||
mixerTimeScale: number,
|
||
// 当前播放的时间(毫秒)
|
||
currentTime: number,
|
||
// 当前action总时长(毫秒)
|
||
duration: number,
|
||
}
|
||
|
||
// 动画轨道类实例
|
||
let timelineInstance: TimelineTrack, waitRows: ITimelineRow[] = [];
|
||
|
||
/**
|
||
* 模型动画(动画编辑器)
|
||
*/
|
||
export const useAnimationStore = defineStore('model-animation', () => {
|
||
const state = reactive<IAnimationState>({
|
||
list: [],
|
||
current: null,
|
||
trackTree: [],
|
||
mixerTimeScale: 1.0,
|
||
currentTime: 0,
|
||
duration: 0
|
||
})
|
||
|
||
/**
|
||
* getter
|
||
**/
|
||
// 动画时间帧一步的长度,默认一步走过一个像素代表1000ms (即1px代表多少ms)
|
||
const getStepVal = () => {
|
||
if (timelineInstance) {
|
||
return (<number>timelineInstance.timeline._options.stepVal) * timelineInstance.timeline._currentZoom / (<number>timelineInstance.timeline._options.zoom);
|
||
} else {
|
||
return 1000;
|
||
}
|
||
};
|
||
// 获取格式化后的总时长 00:00:00
|
||
const getFormattedDuration = computed(() => {
|
||
const _time = state.duration / 1000;
|
||
const hours = Math.floor(_time / 3600);
|
||
const minutes = Math.floor((_time - hours * 3600) / 60);
|
||
const seconds = _time - hours * 3600 - minutes * 60;
|
||
return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.toFixed(0).padStart(2, "0")}`;
|
||
});
|
||
// 获取格式化后的播放时间 00:00:00
|
||
const getFormattedCurrentTime = computed(() => {
|
||
const _time = state.currentTime / 1000;
|
||
const hours = Math.floor(_time / 3600);
|
||
const minutes = Math.floor((_time - hours * 3600) / 60);
|
||
const seconds = (_time- hours * 3600 - minutes * 60).toFixed(0);
|
||
return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${seconds.padStart(2, "0")}`;
|
||
});
|
||
|
||
/**
|
||
* actions
|
||
**/
|
||
const setTimelineInstance = (instance: TimelineTrack) => {
|
||
timelineInstance = instance;
|
||
|
||
if (waitRows.length) {
|
||
timelineInstance.setRows(waitRows);
|
||
waitRows = [];
|
||
|
||
const action = App.animationManager.actionMap.get(state.current?.uuid as string);
|
||
if (!action) return;
|
||
timelineInstance.bindAction = action;
|
||
}
|
||
}
|
||
// 设置动画列表
|
||
const setList = (_list: Array<IAnimationItem>, _current: IAnimationItem | null = null) => {
|
||
state.list = _list;
|
||
if (_current) {
|
||
setCurrent(_current);
|
||
} else {
|
||
setCurrent(_list.length ? _list[0] : null);
|
||
}
|
||
}
|
||
// 设置当前选中的动画(会刷新树列表及动画轨道)
|
||
const setCurrent = (value: IAnimationItem | null) => {
|
||
state.current = value;
|
||
|
||
if (!value) {
|
||
state.trackTree = [];
|
||
state.currentTime = 0;
|
||
state.duration = 0;
|
||
|
||
if (timelineInstance) {
|
||
timelineInstance.setRows([]);
|
||
timelineInstance.bindAction = null;
|
||
waitRows = [];
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 取出当前动画下的所有轨道组织rows关系
|
||
const action = App.animationManager.actionMap.get(value.uuid);
|
||
if (!action) return;
|
||
|
||
const actionClip = action.getClip();
|
||
const actionObject = action.getRoot();
|
||
state.duration = actionClip.duration * 1000;
|
||
|
||
// 取出所有轨道并按名称排序
|
||
const tracks = [...actionClip.tracks].sort((a, b) => a.name.localeCompare(b.name));
|
||
|
||
const _trackTree: TreeOption = {
|
||
key: actionObject.uuid,
|
||
label: actionObject.name,
|
||
children: [],
|
||
isLeaf: false
|
||
};
|
||
waitRows = [{
|
||
id: actionObject.uuid,
|
||
name: actionObject.name
|
||
}];
|
||
const pathMap = new Map(); // 用于记录已有路径
|
||
// 处理每个轨道
|
||
tracks.forEach(track => {
|
||
const parts = track.name.split('.');
|
||
let currentPath = '';
|
||
let parentNode: TreeOption | null = null;
|
||
|
||
parts.forEach((part, index) => {
|
||
const isLeaf = index === parts.length - 1;
|
||
const nodePath = currentPath ? `${currentPath}!!${part}` : part;
|
||
|
||
if (index === 0 && (part === actionObject.name || part === actionObject.uuid)) return;
|
||
|
||
// 如果节点已存在则直接使用
|
||
if (pathMap.has(nodePath)) {
|
||
parentNode = pathMap.get(nodePath);
|
||
currentPath = nodePath;
|
||
return;
|
||
}
|
||
|
||
// 记录到rows
|
||
const _row: ITimelineRow = {
|
||
id: nodePath,
|
||
name: part
|
||
}
|
||
|
||
// 创建新节点
|
||
const newNode: TreeOption = {
|
||
key: nodePath,
|
||
label: part,
|
||
children: [],
|
||
...(isLeaf && {
|
||
isLeaf: true,
|
||
suffix: () =>
|
||
h(
|
||
NIcon,
|
||
{
|
||
size: 16,
|
||
onClick: () => {
|
||
// 取最后一段(如 'nodeName.property[accessor]')
|
||
const lastSegment = parts[parts.length - 1];
|
||
// 再按 '[' 分割,取第一部分(如 'property')
|
||
let attr = lastSegment?.split('[')[0];
|
||
// 如果parts倒数第二个元素为material,则添加至前面
|
||
if(parts.length > 1 && parts[parts.length - 2] ==='material'){
|
||
attr = `material.${attr}`;
|
||
}
|
||
addKeyframe(attr);
|
||
}
|
||
},
|
||
{default: () => h(ConditionPoint)}
|
||
),
|
||
})
|
||
};
|
||
|
||
// 如果是叶子节点,则写入关键帧值
|
||
if (isLeaf) {
|
||
_row.keyframes = [];
|
||
// 每个关键帧的值的长度
|
||
const dataLength = Math.floor(track.values.length / track.times.length);
|
||
track.times.forEach((time, i) => {
|
||
_row.keyframes?.push({
|
||
val: time * 1000,
|
||
// @ts-ignore
|
||
data: track.values.slice(i * dataLength, (i + 1) * dataLength)
|
||
})
|
||
})
|
||
_row.track = track;
|
||
}
|
||
waitRows.push(_row);
|
||
|
||
// 挂载到父节点或根
|
||
if (parentNode) {
|
||
parentNode.children?.push(newNode);
|
||
} else {
|
||
_trackTree.children?.push(newNode);
|
||
}
|
||
|
||
// 更新路径记录
|
||
pathMap.set(nodePath, newNode);
|
||
parentNode = newNode;
|
||
currentPath = nodePath;
|
||
});
|
||
});
|
||
|
||
state.trackTree = [_trackTree];
|
||
if (timelineInstance) {
|
||
timelineInstance.setRows(waitRows);
|
||
timelineInstance.bindAction = action;
|
||
waitRows = [];
|
||
|
||
state.duration = timelineInstance._maxDuration;
|
||
}
|
||
}
|
||
const play = () => {
|
||
if (!state.current) return;
|
||
|
||
if (timelineInstance) {
|
||
timelineInstance.play();
|
||
} else {
|
||
const action = App.animationManager.actionMap.get(state.current.uuid);
|
||
if (!action) return;
|
||
|
||
action.play();
|
||
action.paused = false;
|
||
}
|
||
|
||
state.current.isPaused = false;
|
||
state.current.isRunning = true;
|
||
}
|
||
const pause = () => {
|
||
if (!state.current) return;
|
||
|
||
if (timelineInstance) {
|
||
timelineInstance.pause();
|
||
} else {
|
||
const action = App.animationManager.actionMap.get(state.current.uuid);
|
||
if (!action) return;
|
||
|
||
action.paused = !state.current.isPaused;
|
||
}
|
||
|
||
state.current.isPaused = !state.current.isPaused;
|
||
}
|
||
const stop = () => {
|
||
if (!state.current) return;
|
||
|
||
if (timelineInstance) {
|
||
timelineInstance.stop();
|
||
} else {
|
||
const action = App.animationManager.actionMap.get(state.current.uuid);
|
||
if (!action) return;
|
||
|
||
action.stop();
|
||
}
|
||
state.current.isPaused = false;
|
||
state.current.isRunning = false;
|
||
}
|
||
// 跳转至开始
|
||
const jumpToStart = () => {
|
||
if (!timelineInstance) return;
|
||
|
||
timelineInstance.timeline.setTime(0);
|
||
}
|
||
// 跳转至最后
|
||
const jumpToEnd = () => {
|
||
if (!timelineInstance) return;
|
||
|
||
timelineInstance.timeline.setTime(state.duration);
|
||
}
|
||
// 添加关键帧
|
||
const addKeyframe = (attr: string) => {
|
||
if (!state.current) {
|
||
window.$message?.error(t("prompt.Please select an animation"));
|
||
return;
|
||
}
|
||
|
||
if (state.current.isRunning) {
|
||
window.$message?.error(t("prompt['The animation is running, please stop the animation before performing operations']"));
|
||
return;
|
||
}
|
||
|
||
if (timelineInstance) {
|
||
timelineInstance.addKeyframe(attr);
|
||
|
||
state.duration = timelineInstance._maxDuration;
|
||
}
|
||
}
|
||
// 设置动画播放时间(单位:ms)
|
||
const setPlayTime = (time: number) => {
|
||
if (timelineInstance) {
|
||
timelineInstance.timeline.setTime(time);
|
||
}
|
||
};
|
||
|
||
return {
|
||
...toRefs(state),
|
||
getStepVal,
|
||
getFormattedDuration,
|
||
getFormattedCurrentTime,
|
||
setTimelineInstance,
|
||
setList,
|
||
setCurrent,
|
||
play,
|
||
pause,
|
||
stop,
|
||
jumpToStart,
|
||
jumpToEnd,
|
||
addKeyframe,
|
||
setPlayTime
|
||
}
|
||
});
|
||
|
||
// setup 之外使用
|
||
export function useAnimationStoreWithOut() {
|
||
return useAnimationStore(store);
|
||
} |