'use strict'; const animation = require('./animation'); const events = require('./events'); const eventEditor = require('./event-editor'); exports.template = /* html */`
`; exports.style = /* css*/` .tips { text-align: center; min-height: 200px; padding-top: 16px; border-top: 1px solid var(--color-normal-border); } .tips > ui-label { display: block; } .tips > ui-label.big { font-size: 14px; } .flex { display: flex; } .f1 { flex: 1; } .preview-container { min-height: 200px; } .preview-container > .animation-info { padding-right: 4px; } .preview[hoving] > .preview-container { outline: 2px solid var(--color-focus-fill-weaker); outline-offset: -1px; } .preview[hoving] > .tips { outline: 2px solid var(--color-focus-fill-weaker); outline-offset: -2px; } .preview-container > .model-info { display: none; padding: 2px 4px; } .preview-container > .model-info > ui-label { margin-right: 6px; } .preview-container > .image { height: var(--inspector-footer-preview-height, 200px); overflow: hidden; display: flex; flex: 1; } .preview-container >.image > .canvas { flex: 1; } .preview-container .toolbar { display: flex; margin-top: 4px; justify-content: space-between; } .preview-container .toolbar ui-num-input { display: flex; flex: 1; } .preview-container .toolbar > * { margin-left: 4px; } ui-icon { cursor: pointer; } .time-line { position: relative; padding: 4px; } .time-line .events { position: absolute; bottom: 2px; z-index: 1; box-sizing: border-box; padding: 0 8px; width: 100%; } .time-line ui-scale-plate { width: 100%; } .events ui-icon { position: absolute; bottom: 0; } .events ui-icon:hover { color: white; cursor: pointer; } .events ui-icon[active] { color: var(--color-focus-fill); } .mask { position: absolute; z-index: 2; width: 100%; height: 100vh; top: 0; right: 0; bottom: 0; left: 0; background: var(--color-normal-fill); opacity: 0.3; } .event-editor { background: var(--color-normal-fill); } #event-editor { position: relative; line-height: 20px; width: 100%; height: 70%; overflow: hidden; margin: 0 auto; background: #312f2f; display: flex; flex-direction: column; border: var(--color-normal-fill-emphasis) 1px solid; box-sizing: border-box; } #event-editor > header { display: flex; justify-content: space-between; height: 20px; background: var(--color-normal-fill-emphasis); padding: 2px 4px; } #event-editor > header .title { color: var(--color-focus-fill); } #event-editor > .header .name { margin: 0 4px; } #event-editor > .header .dirty { color: var(--color-focus-fill); margin: 0 2px; } #event-editor > .functions { overflow-y: auto; flex: 1; height: 100%; padding-top: 0; background: var(--color-normal-fill-normal); margin-bottom: 2px; } #event-editor > .functions .header, #event-editor > .functions .line { display: flex; justify-content: space-between; flex: 1; background: unset; margin: 4px 0; } #event-editor > .functions .line .name { width: 40px; text-align: center; } #event-editor > .functions .line ui-select { margin-right: 8px; } #event-editor > .functions .line .operate { visibility: hidden; } #event-editor > .functions .line:hover .operate { visibility: visible; } #event-editor > .functions .header ui-input { background-color: transparent; border-color: transparent; } #event-editor > .functions .header ui-input:hover { background-color: var(--color-normal-fill-emphasis); } #event-editor ui-input, #event-editor ui-checkbox, #event-editor ui-num-input { flex: 1; margin-left: 4px; } #event-editor ui-section { width: 100%; } #event-editor ui-icon:hover { cursor: pointer; color: #cccccc; } #event-editor .tools { display: flex; padding: 4px 0; } #event-editor .tools ui-icon { margin: 0 4px; } #event-editor .params { } #event-editor .header ui-icon, #event-editor .params ui-icon { margin: 0 5px; } #event-editor .empty { font-style: italic; text-align: center; } #event-editor .toast { position: absolute; top: 54px; right: 4px; z-index: 1; padding: 0 4px; background-color: var(--color-normal-fill-emphasis); color: var(--color-warn-fill); } `; exports.$ = { noModel: '.noModel', multiple: '.multiple', container: '.preview', previewContainer: '.preview-container', vertices: '.vertices', triangles: '.triangles', image: '.image', canvas: '.canvas', animationInfo: '.animation-info', modelInfo: '.model-info', playButtonIcon: '#playButtonIcon', animationTime: '#animationTime', currentTime: '#currentTime', events: ".events", duration: ".duration", eventEditor: ".event-editor", timeCtrl: "#timeCtrl", }; const PLAY_STATE = { STOP: 0, PLAYING: 1, PAUSE: 2, }; async function callModelPreviewFunction(funcName, ...args) { return await Editor.Message.request('scene', 'call-preview-function', 'scene:model-preview', funcName, ...args); } const Elements = { container: { ready() { const panel = this; function observer() { window.requestAnimationFrame(() => { panel.isPreviewDataDirty = true; panel.updateEventInfo(); }); } panel.resizeObserver = new window.ResizeObserver(observer); panel.resizeObserver.observe(panel.$.container); // Identify dragged FBX resources panel.$.container.addEventListener('drop', async (event) => { event.preventDefault(); event.stopPropagation(); // Multiple drag-and-drop options are not supported, and only the first value is taken const values = []; const { additional, value } = JSON.parse(JSON.stringify(Editor.UI.DragArea.currentDragInfo)) || {}; if (additional) { additional.forEach((info) => { if (info.type === 'cc.Prefab') { values.push(info.value); } if (Array.isArray(info.extends)) { info.extends.forEach(v => { if (v === 'cc.Prefab') { values.push(info.value); } }); } if (info.subAssets) { Object.values(info.subAssets).forEach(subAsset => { if (subAsset.type === 'cc.Prefab') { values.push(subAsset.value); } }); } }); } if (value && !values.includes(value)) { values.push(value); } if (!values.length) { return; } const info = await callModelPreviewFunction('setModel', values[0]); panel.infoUpdate(info); }); }, close() { const panel = this; panel.resizeObserver.unobserve(panel.$.container); }, }, preview: { ready() { const panel = this; let _isPreviewDataDirty = false; Object.defineProperty(panel, 'isPreviewDataDirty', { get() { return _isPreviewDataDirty; }, set(value) { if (value !== _isPreviewDataDirty) { _isPreviewDataDirty = value; value && panel.refreshPreview(); } }, }); panel.$.canvas.addEventListener('mousedown', async (event) => { await callModelPreviewFunction('onMouseDown', { x: event.x, y: event.y, button: event.button }); async function mousemove(event) { await callModelPreviewFunction('onMouseMove', { movementX: event.movementX, movementY: event.movementY, }); panel.isPreviewDataDirty = true; } async function mouseup(event) { await callModelPreviewFunction('onMouseUp', { x: event.x, y: event.y, }); document.removeEventListener('mousemove', mousemove); document.removeEventListener('mouseup', mouseup); panel.isPreviewDataDirty = false; } document.addEventListener('mousemove', mousemove); document.addEventListener('mouseup', mouseup); panel.isPreviewDataDirty = true; }); panel.$.canvas.addEventListener('wheel', async (event) => { await callModelPreviewFunction('onMouseWheel', { wheelDeltaY: event.wheelDeltaY, wheelDeltaX: event.wheelDeltaX, }); panel.isPreviewDataDirty = true; }); const GlPreview = Editor._Module.require('PreviewExtends').default; panel.glPreview = new GlPreview('scene:model-preview', 'query-model-preview-data'); panel.isPreviewDataDirty = true; }, async update() { const panel = this; if (!panel.$.canvas) { return; } await panel.glPreview.init({ width: panel.$.canvas.clientWidth, height: panel.$.canvas.clientHeight }); const prefabAsset = Object.values(panel.asset.subAssets).find((asset) => asset.type === 'cc.Prefab'); if (prefabAsset) { const info = await callModelPreviewFunction('setModel', prefabAsset.uuid); panel.infoUpdate(info); } else { this.updatePanelHidden(false); } panel.isPreviewDataDirty = true; }, }, modelInfo: { ready() { this.infoUpdate = Elements.modelInfo.update.bind(this); }, update(info) { if (!info) { return; } this.$.vertices.value = `Vertices:${info.vertices}`; this.$.triangles.value = `Triangles:${info.polygons}`; this.isPreviewDataDirty = true; this.updatePanelHidden(true); }, close() { callModelPreviewFunction('hide'); }, }, animationTime: { ready() { const timeline = this.$.animationTime; timeline.addEventListener('change', this.onAnimationTimeChange.bind(this)); timeline.addEventListener('transform', this.updateEventInfo.bind(this)); }, }, currentTime: { ready() { const currentTime = this.$.currentTime; currentTime.addEventListener('confirm', this.onAnimationTimeChange.bind(this)); }, }, timeCtrl: { ready() { this.$.timeCtrl.addEventListener('click', this.onTimeCtrlClick.bind(this)); }, }, }; exports.methods = { /** * * @param {boolean} hasModel */ updatePanelHidden(hasModel) { this.$.noModel.hidden = hasModel; this.$.previewContainer.hidden = this.isMultiple || !hasModel; this.$.multiple.hidden = !this.isMultiple; }, async apply() { // save animation event info await this.events.apply.call(this); }, async refreshPreview() { const panel = this; // After await, the panel no longer exists if (!panel.$.canvas) { return; } const doDraw = async () => { try { const canvas = panel.$.canvas; const image = panel.$.image; const width = image.clientWidth; const height = image.clientHeight; if (canvas.width !== width || canvas.height !== height) { canvas.width = width; canvas.height = height; await panel.glPreview.initGL(canvas, { width, height }); await panel.glPreview.resizeGL(width, height); } const info = await panel.glPreview.queryPreviewData({ width: canvas.width, height: canvas.height, }); panel.glPreview.drawGL(info); } catch (e) { console.warn(e); } }; if (panel.isPreviewDataDirty || this.curPlayState === PLAY_STATE.PLAYING) { requestAnimationFrame(async () => { await doDraw(); panel.isPreviewDataDirty = false; }); } }, async onTabChanged(activeTab) { if (typeof activeTab === 'string') { this.activeTab = activeTab; const isAnimationTab = this.activeTab === 'animation'; this.$.animationInfo.style.display = isAnimationTab ? 'block' : 'none'; if (!isAnimationTab) { this.eventEditorVm.show = false; } this.$.modelInfo.style.display = this.activeTab === 'model' ? 'block' : 'none'; await this.stopAnimation(); } }, onTimeCtrlClick(event) { const name = event.target.getAttribute('name'); if (!name || !this.curEditClipInfo) { return; } switch (name) { case 'play': this.onPlayButtonClick(); break; case 'stop': this.stopAnimation(); break; case 'jump_first_frame': this.setCurrentFrame(0); break; case 'jump_next_frame': this.setCurrentFrame(this.$.currentTime.value + 1); break; case 'jump_prev_frame': this.setCurrentFrame(this.$.currentTime.value - 1); break; case 'jump_last_frame': this.setCurrentFrame(this.curTotalFrames); break; case 'add_event': if (this.checkDisabledEditEvent()) { return; } this.addEventToCurTime(); break; } }, checkDisabledEditEvent() { if (!this.curEditClipInfo.userData) { Editor.Dialog.info(Editor.I18n.t('ENGINE.assets.fbx.addEvent.shouldSave'), { buttons: [Editor.I18n.t('ENGINE.assets.fbx.addEvent.ok')], }); return true; } return false; }, addEventToCurTime() { this.events.addEvent.call(this, this.$.animationTime.value / this.curEditClipInfo.fps); }, updateEventInfo() { let eventInfos = []; if (this.curEditClipInfo && this.curEditClipInfo.userData) { const events = this.curEditClipInfo.userData.events; if (Array.isArray(events)) { eventInfos = events.map((info) => { return { info, x: this.$.animationTime.valueToPixel(info.frame * this.curEditClipInfo.fps), }; }); } } this.events.update.call(this, eventInfos); }, async stopAnimation() { if (!this.curEditClipInfo) { return; } await callModelPreviewFunction('stop'); }, async onPlayButtonClick() { if (!this.curEditClipInfo) { return; } switch (this.curPlayState) { case PLAY_STATE.PAUSE: await callModelPreviewFunction('resume'); break; case PLAY_STATE.PLAYING: await callModelPreviewFunction('pause'); break; case PLAY_STATE.STOP: await callModelPreviewFunction('play', this.curEditClipInfo.rawClipUUID); break; default: break; } this.isPreviewDataDirty = true; }, async onAnimationTimeChange(event) { event.stopPropagation(); if (!this.curEditClipInfo) { return; } const frame = event.target.value; await this.setCurrentFrame(frame); this.isPreviewDataDirty = true; }, async setCurrentFrame(frame) { frame = Editor.Utils.Math.clamp(frame, 0, this.curTotalFrames); const curTime = frame / this.curEditClipInfo.fps + this.curEditClipInfo.from; await callModelPreviewFunction('setCurEditTime', curTime); }, onModelAnimationUpdate(time) { if (!this.curEditClipInfo) { return; } if (this.$.animationTime) { let timeFromRangeStart = Math.max(time - this.curEditClipInfo.from, 0); this.$.animationTime.value = Math.round(timeFromRangeStart * this.curEditClipInfo.fps); this.$.currentTime.value = this.$.animationTime.value; } this.isPreviewDataDirty = true; }, setCurPlayState(state) { this.curPlayState = state; let buttonIconName = ''; switch (state) { case PLAY_STATE.STOP: buttonIconName = 'play'; break; case PLAY_STATE.PLAYING: buttonIconName = 'pause'; break; case PLAY_STATE.PAUSE: buttonIconName = 'play'; break; default: break; } if (this.$.playButtonIcon) { this.$.playButtonIcon.value = buttonIconName; } this.isPreviewDataDirty = true; }, async setCurEditClipInfo(clipInfo) { this.curEditClipInfo = clipInfo; this.curTotalFrames = 0; if (clipInfo) { this.curTotalFrames = Math.round(clipInfo.duration * clipInfo.fps); // update animation events, clipInfo.clipUUID may be undefined if (clipInfo.clipUUID) { const subId = clipInfo.clipUUID.match(/@(.*)/)[1]; this.curEditClipInfo.userData = this.meta.subMetas[subId] && this.meta.subMetas[subId].userData || {}; } await callModelPreviewFunction( 'setPlaybackRange', clipInfo.from, clipInfo.to, ); await callModelPreviewFunction( 'setClipConfig', { wrapMode: clipInfo.wrapMode, speed: clipInfo.speed, } ); await this.stopAnimation(); } this.$.animationTime.setConfig({ max: this.curTotalFrames, }); this.$.duration.innerHTML = `Totals: ${this.curTotalFrames}`; this.updateEventInfo(); }, onAnimationPlayStateChanged(state) { this.setCurPlayState(state); }, addAssetChangeListener(add = true) { if (!add && this.hasListenAssetsChange) { Editor.Message.__protected__.removeBroadcastListener('asset-db:asset-change', this.onAssetChangeBind); this.hasListenAssetsChange = false; return; } Editor.Message.__protected__.addBroadcastListener('asset-db:asset-change', this.onAssetChangeBind); this.hasListenAssetsChange = true; }, async onAssetChange(uuid) { if (this.asset.uuid === uuid) { // Update the animation dump when the parent assets changes this.meta = await Editor.Message.request('asset-db', 'query-asset-meta', this.asset.uuid); const clipInfo = animation.methods.getCurClipInfo.call(this); await this.onEditClipInfoChanged(clipInfo); } }, }; exports.ready = function() { this.gridWidth = 0; this.gridTableWith = 0; this.activeTab = 'animation'; this.isPreviewDataDirty = true; this.curEditClipInfo = null; this.curPlayState = PLAY_STATE.STOP; this.curTotalFrames = 0; this.onTabChangedBind = this.onTabChanged.bind(this); this.onModelAnimationUpdateBind = this.onModelAnimationUpdate.bind(this); this.onAnimationPlayStateChangedBind = this.onAnimationPlayStateChanged.bind(this); this.onEditClipInfoChanged = async (clipInfo) => { if (clipInfo) { await callModelPreviewFunction('setEditClip', clipInfo.rawClipUUID, clipInfo.rawClipIndex); await this.setCurEditClipInfo(clipInfo); } }; Editor.Message.addBroadcastListener('scene:model-preview-animation-time-change', this.onModelAnimationUpdateBind); Editor.Message.addBroadcastListener('scene:model-preview-animation-state-change', this.onAnimationPlayStateChangedBind); Editor.Message.addBroadcastListener('fbx-inspector:change-tab', this.onTabChangedBind); Editor.Message.addBroadcastListener('fbx-inspector:animation-change', this.onEditClipInfoChanged); for (const prop in Elements) { const element = Elements[prop]; if (element.ready) { element.ready.call(this); } } this.onAssetChangeBind = this.onAssetChange.bind(this); this.addAssetChangeListener(true); this.events = events; this.events.ready.call(this); this.eventEditor = eventEditor; this.events.eventsMap = {}; this.eventEditor.ready.call(this); }; exports.update = async function(assetList, metaList) { this.assetList = assetList; this.metaList = metaList; this.isMultiple = this.assetList.length > 1; this.$.previewContainer.hidden = this.isMultiple; this.$.multiple.hidden = !this.isMultiple; this.$.noModel.hidden = true; if (this.isMultiple) { return; } this.asset = assetList[0]; this.meta = metaList[0]; for (const prop in Elements) { const element = Elements[prop]; if (element.update) { element.update.call(this); } } animation.methods.initAnimationNameToUUIDMap.call(this); animation.methods.initAnimationInfos.call(this); if (this.animationInfos) { this.rawClipIndex = 0; this.splitClipIndex = 0; const clipInfo = animation.methods.getCurClipInfo.call(this); await this.onEditClipInfoChanged(clipInfo); } else { await this.setCurEditClipInfo(); } this.eventEditorVm.show = false; this.setCurPlayState(PLAY_STATE.STOP); this.isPreviewDataDirty = true; }; exports.close = function() { for (const prop in Elements) { const element = Elements[prop]; if (element.close) { element.close.call(this); } } Editor.Message.removeBroadcastListener('scene:model-preview-animation-time-change', this.onModelAnimationUpdateBind); Editor.Message.removeBroadcastListener('scene:model-preview-animation-state-change', this.onAnimationPlayStateChangedBind); Editor.Message.removeBroadcastListener('fbx-inspector:change-tab', this.onTabChangedBind); Editor.Message.removeBroadcastListener('fbx-inspector:animation-change', this.onEditClipInfoChanged); this.addAssetChangeListener(false); };