From 99d01fe944dfacb777a7b007b985f98ec92dd1a4 Mon Sep 17 00:00:00 2001 From: knoxHuang Date: Thu, 14 Nov 2024 14:20:15 +0800 Subject: [PATCH] inspector adds prefab and spine previews --- cocos/spine/skeleton.ts | 1 - editor/i18n/en/assets.js | 8 + editor/i18n/zh/assets.js | 8 + editor/inspector/assets-footer.js | 3 + editor/inspector/assets/image-preview.js | 25 +- editor/inspector/assets/mesh-preview.js | 49 ++- editor/inspector/assets/prefab-preview.js | 206 +++++++++++ editor/inspector/assets/skeleton-preview.js | 26 +- editor/inspector/assets/spine-preview.js | 385 ++++++++++++++++++++ 9 files changed, 677 insertions(+), 34 deletions(-) create mode 100644 editor/inspector/assets/prefab-preview.js create mode 100644 editor/inspector/assets/spine-preview.js diff --git a/cocos/spine/skeleton.ts b/cocos/spine/skeleton.ts index e16e6972785..62e7446cacd 100644 --- a/cocos/spine/skeleton.ts +++ b/cocos/spine/skeleton.ts @@ -1038,7 +1038,6 @@ export class Skeleton extends UIRenderer { */ public updateAnimation (dt: number): void { this.markForUpdateRenderData(); - if (EDITOR_NOT_IN_PREVIEW) return; if (this.paused) return; if (this.isAnimationCached()) { // On realTime mode, dt is multiplied at native side. diff --git a/editor/i18n/en/assets.js b/editor/i18n/en/assets.js index 349d2f06d02..e7ebef09e9b 100644 --- a/editor/i18n/en/assets.js +++ b/editor/i18n/en/assets.js @@ -8,6 +8,14 @@ module.exports = { inspector: { cloneToEdit: 'Clone it. Use and go into edit.', cloneToDirectoryIllegal: 'Please limit the saved path to the current project assets path', + preview: { + header: 'Preview', + }, + spine: { + skin: 'Skin', + animation: 'Animation', + loop: 'loop', + }, }, assets: { diff --git a/editor/i18n/zh/assets.js b/editor/i18n/zh/assets.js index ee26f01507d..c042fb44007 100644 --- a/editor/i18n/zh/assets.js +++ b/editor/i18n/zh/assets.js @@ -8,6 +8,14 @@ module.exports = { inspector: { cloneToEdit: '克隆出新资源,使用并编辑', cloneToDirectoryIllegal: '保存路径请限制在当前项目 /assets 路径内', + preview: { + header: '预览', + }, + spine: { + skin: '皮肤', + animation: '动画', + loop: '循环', + }, }, assets: { diff --git a/editor/inspector/assets-footer.js b/editor/inspector/assets-footer.js index 3e8765fbe82..e0d915d1dbd 100644 --- a/editor/inspector/assets-footer.js +++ b/editor/inspector/assets-footer.js @@ -10,4 +10,7 @@ module.exports = { gltf: join(__dirname, './assets/fbx/preview.js'), // reuse 'gltf-mesh': join(__dirname, './assets/mesh-preview.js'), 'gltf-skeleton': join(__dirname, './assets/skeleton-preview.js'), + 'gltf-scene': join(__dirname, './assets/prefab-preview.js'), + prefab: join(__dirname, './assets/prefab-preview.js'), + 'spine-data': join(__dirname, './assets/spine-preview.js'), }; diff --git a/editor/inspector/assets/image-preview.js b/editor/inspector/assets/image-preview.js index 147bd0753ab..8d1c9451814 100644 --- a/editor/inspector/assets/image-preview.js +++ b/editor/inspector/assets/image-preview.js @@ -1,22 +1,32 @@ 'use strict'; exports.template = /* html */` -
- -
- + +
+
+ +
+
+ +
-
+ `; exports.style = /* css */` + .preview-section { + margin-top: 0px; + } .image-preview { + position: relative; + display: flex; + } + .image-preview > .image-content { height: var(--inspector-footer-preview-height, 200px); background: var(--color-normal-fill-emphasis); - display: flex; - position: relative; box-sizing: border-box; } + .image-preview > .image { width: 100%; height: 100%; @@ -85,6 +95,7 @@ exports.update = function(assetList, metaList) { this.asset = assetList[0]; this.meta = metaList[0]; + // 如何多选就隐藏预览 if (assetList.length > 1) { this.$.container.style.display = 'none'; } else { diff --git a/editor/inspector/assets/mesh-preview.js b/editor/inspector/assets/mesh-preview.js index 49b308ffd7c..275a2f7bbb1 100644 --- a/editor/inspector/assets/mesh-preview.js +++ b/editor/inspector/assets/mesh-preview.js @@ -2,32 +2,36 @@ exports.template = /* html */` -
-
- - -
- - - - + +
+
+ + +
+ + + + +
+
+ +
+
+ +
-
- +
+
-
- -
-
-
-
-
+
`; exports.style = /* css */` +.preview-section { + margin-top: 0px; +} .preview { - border-top: 1px solid var(--color-normal-border); } .preview > .info { @@ -292,6 +296,13 @@ exports.update = function(assetList, metaList) { this.asset = assetList[0]; this.meta = metaList[0]; + // 如何多选就隐藏预览 + if (assetList.length > 1) { + this.$.container.style.display = 'none'; + } else { + this.$.container.style.display = 'block'; + } + for (const prop in Elements) { const element = Elements[prop]; if (element.update) { diff --git a/editor/inspector/assets/prefab-preview.js b/editor/inspector/assets/prefab-preview.js new file mode 100644 index 00000000000..369ba92aff2 --- /dev/null +++ b/editor/inspector/assets/prefab-preview.js @@ -0,0 +1,206 @@ +'use strict'; + +exports.template = /* html */` + +
+
+ +
+
+
+`; + +exports.style = /* css */` + .preview-section { + margin-top: 0px; + } + .preview { } + .preview > .image { + height: var(--inspector-footer-preview-height, 200px); + overflow: hidden; + display: flex; + flex: 1; + } + .preview >.image > .canvas { + flex: 1; + } +`; + + +exports.$ = { + container: '.preview', + canvas: '.canvas', + image: '.image', +}; + +async function callFunction(funcName, ...args) { + return await Editor.Message.request('scene', 'call-preview-function', 'scene:prefab-preview', funcName, ...args); +} + +const Elements = { + 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 callFunction('onMouseDown', { x: event.x, y: event.y, button: event.button }); + + async function mousemove(event) { + await callFunction('onMouseMove', { + movementX: event.movementX, + movementY: event.movementY, + }); + + panel.isPreviewDataDirty = true; + } + + async function mouseup(event) { + await callFunction('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 callFunction('onMouseWheel', { + wheelDeltaY: event.wheelDeltaY, + wheelDeltaX: event.wheelDeltaX, + }); + panel.isPreviewDataDirty = true; + }); + + + const GlPreview = Editor._Module.require('PreviewExtends').default; + panel.glPreview = new GlPreview('scene:prefab-preview', 'query-prefab-preview-data'); + + function observer() { + panel.isPreviewDataDirty = true; + } + + panel.resizeObserver = new window.ResizeObserver(observer); + panel.resizeObserver.observe(panel.$.image); + observer(); + }, + async update() { + const panel = this; + + if (!panel.$.canvas) { + return; + } + + await panel.glPreview.init({ width: panel.$.canvas.clientWidth, height: panel.$.canvas.clientHeight }); + await callFunction('setPrefab', panel.asset.uuid); + this.isPreviewDataDirty = true; + }, + close() { + const panel = this; + + panel.resizeObserver.unobserve(panel.$.image); + }, + }, +}; + +exports.methods = { + 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); + } + }; + + requestAnimationFrame(async () => { + await doDraw(); + panel.isPreviewDataDirty = false; + }); + }, +}; + +exports.ready = function() { + for (const prop in Elements) { + const element = Elements[prop]; + if (element.ready) { + element.ready.call(this); + } + } +}; + +exports.update = function(assetList, metaList) { + this.assetList = assetList; + this.metaList = metaList; + this.asset = assetList[0]; + this.meta = metaList[0]; + + // 如何多选就隐藏预览 + if (assetList.length > 1) { + this.$.container.style.display = 'none'; + } else { + this.$.container.style.display = 'block'; + } + + for (const prop in Elements) { + const element = Elements[prop]; + if (element.update) { + element.update.call(this); + } + } +}; + +exports.close = function() { + for (const prop in Elements) { + const element = Elements[prop]; + if (element.close) { + element.close.call(this); + } + } +}; diff --git a/editor/inspector/assets/skeleton-preview.js b/editor/inspector/assets/skeleton-preview.js index 3d68bbf01cf..4bd780f4ec6 100644 --- a/editor/inspector/assets/skeleton-preview.js +++ b/editor/inspector/assets/skeleton-preview.js @@ -1,17 +1,22 @@ 'use strict'; exports.template = /* html */` -
-
- + +
+
+ +
+
+ +
-
- -
-
+ `; exports.style = /* css */` +.preview-section { + margin-top: 0px; +} .preview { border-top: 1px solid var(--color-normal-border); } @@ -209,6 +214,13 @@ exports.update = function(assetList, metaList) { this.asset = assetList[0]; this.meta = metaList[0]; + // 如何多选就隐藏预览 + if (assetList.length > 1) { + this.$.container.style.display = 'none'; + } else { + this.$.container.style.display = 'block'; + } + for (const prop in Elements) { const element = Elements[prop]; if (element.update) { diff --git a/editor/inspector/assets/spine-preview.js b/editor/inspector/assets/spine-preview.js new file mode 100644 index 00000000000..5a79b52ece4 --- /dev/null +++ b/editor/inspector/assets/spine-preview.js @@ -0,0 +1,385 @@ +'use strict'; + +exports.template = /* html */` + +
+
+ + + + + + + + + + + + +
+
+ +
+ + + + + + + +
+ +
+
+
+`; + +exports.style = /* css */` +.preview-section { + margin-top: 0px; +} +.preview { + border-top: 1px solid var(--color-normal-border); +} +.preview > .control { + padding-top: 8px; + padding-bottom: 8px; + display: flex; + width: 100%; + flex-direction: column; + align-items: flex-start; +} +.preview > .control[hidden] { + display: none; +} +.preview > .control > ui-prop { + min-width: 250px; +} +.preview > .image { + height: var(--inspector-footer-preview-height, 200px); + overflow: hidden; + display: flex; + flex: 1; +} +.preview > .image > .anim-control { + position: absolute; + right: 15px; + bottom: 15px; + height: 25px; +} +.preview > .image > .anim-control[hidden] { + display: none; +} +.preview > .image .play[hidden] { + display: none; +} +.preview > .image .pause[hidden] { + display: none; +} +.preview > .image .duration { + position: absolute; + left: 50%; + bottom: 4px; + transform: translate(-50%); +} +.preview >.image > .canvas { + flex: 1; +} +`; + +exports.$ = { + container: '.preview', + canvas: '.canvas', + image: '.image', + skinSelectPro: '.skin-select-pro', + animationSelectPro: '.animation-select-pro', + control: '.control', + playState: '.play-state', + play: '.play', + pause: '.pause', + stop: '.stop', + duration: '.duration', + loop: '.loop-check-box', + animationCtrl: '.anim-control', +}; + +async function callFunction(funcName, ...args) { + return await Editor.Message.request('scene', 'call-preview-function', 'scene:spine-preview', funcName, ...args); +} + +const Elements = { + 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 callFunction('onMouseDown', { x: event.x, y: event.y, button: event.button }); + + async function mousemove(event) { + await callFunction('onMouseMove', { + movementX: event.movementX, + movementY: event.movementY, + }); + + panel.isPreviewDataDirty = true; + } + + async function mouseup(event) { + await callFunction('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 callFunction('onMouseWheel', { + wheelDeltaY: event.wheelDeltaY, + wheelDeltaX: event.wheelDeltaX, + }); + panel.isPreviewDataDirty = true; + }); + + + const GlPreview = Editor._Module.require('PreviewExtends').default; + panel.glPreview = new GlPreview('scene:spine-preview', 'query-spine-preview-data'); + + function observer() { + panel.isPreviewDataDirty = true; + } + + panel.resizeObserver = new window.ResizeObserver(observer); + panel.resizeObserver.observe(panel.$.image); + observer(); + }, + async update() { + const panel = this; + + if (!panel.$.canvas) { + return; + } + + await panel.glPreview.init({ width: panel.$.canvas.clientWidth, height: panel.$.canvas.clientHeight }); + const spineData = await callFunction('setSpine', panel.asset.uuid); + panel.spinUpdate(spineData); + this.isPreviewDataDirty = true; + }, + close() { + const panel = this; + + panel.resizeObserver.unobserve(panel.$.image); + }, + }, + spine: { + updateEnum($selectPro, info) { + const panel = this; + + $selectPro.innerHTML = ''; + for (const key in info.list) { + const value = info.list[key]; + const option = document.createElement('ui-select-option-pro'); + option.setAttribute('label', key); + option.setAttribute('value', value); + if (info.index === value) { + panel.animationIndex = Number(value); + option.setAttribute('selected', ''); + } + $selectPro.appendChild(option); + } + }, + ready() { + const panel = this; + + panel.animationIndex = 0; + panel.$.skinSelectPro.addEventListener('confirm', (event) => { + callFunction('setSkinIndex', Number(event.detail)).then(() => {}); + }); + panel.$.animationSelectPro.addEventListener('confirm', (event) => { + panel.animationIndex = Number(event.detail); + callFunction('play', panel.animationIndex).then(() => {}); + }); + panel.spinUpdate = Elements.spine.update.bind(panel); + }, + update(info) { + const panel = this; + + if (!info) { + panel.$.loop.setAttribute('disabled', ''); + panel.$.play.setAttribute('disabled', ''); + panel.$.stop.setAttribute('disabled', ''); + panel.$.skinSelectPro.setAttribute('disabled', ''); + panel.$.animationSelectPro.setAttribute('disabled', ''); + return; + } + + panel.$.loop.removeAttribute('disabled'); + panel.$.play.removeAttribute('disabled'); + panel.$.stop.removeAttribute('disabled'); + panel.$.skinSelectPro.removeAttribute('disabled'); + panel.$.animationSelectPro.removeAttribute('disabled'); + + Elements.spine.updateEnum.call(panel, panel.$.skinSelectPro, info.skin); + Elements.spine.updateEnum.call(panel, panel.$.animationSelectPro, info.animation); + Elements.spine.updateDuration.call(panel, 0, info.animation.durations[panel.animationIndex]); + Elements.control.update.call(panel, false); + Elements.control.updateLoop.call(panel, info.loop); + panel.isPreviewDataDirty = true; + }, + updateDuration(delay, duration) { + const panel = this; + + panel.$.duration.setAttribute('value', `${Number(delay).toFixed(3)} / ${duration.toFixed(3)}`); + }, + }, + control: { + ready() { + const panel = this; + + panel.$.loop.addEventListener('confirm', (event) => { + callFunction('setLoop', Boolean(event.target.value)).then(() => {}); + }); + panel.$.play.addEventListener('click', () => { + callFunction('play', panel.animationIndex).then(() => {}); + }); + panel.$.pause.addEventListener('confirm', (event) => { + callFunction('pause').then(() => {}); + }); + panel.$.stop.addEventListener('confirm', (event) => { + callFunction('stop').then(() => {}); + }); + }, + updateLoop(loop) { + const panel = this; + + panel.$.loop.setAttribute('value', Boolean(loop)); + }, + update(playing) { + const panel = this; + + if (playing) { + panel.$.play.setAttribute('hidden', ''); + panel.$.pause.removeAttribute('hidden'); + } else { + panel.$.pause.setAttribute('hidden', ''); + panel.$.play.removeAttribute('hidden'); + } + }, + }, +}; + +exports.methods = { + 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); + } + }; + + requestAnimationFrame(async () => { + await doDraw(); + panel.isPreviewDataDirty = false; + }); + }, + + onAnimationUpdate(playing, delay, duration) { + const panel = this; + // this.$.playState.setAttribute(''); + Elements.spine.updateDuration.call(panel, delay, duration); + Elements.control.update.call(panel, playing); + panel.isPreviewDataDirty = true; + }, +}; + +exports.ready = function() { + this.onAnimationUpdateBind = this.onAnimationUpdate.bind(this); + + Editor.Message.addBroadcastListener('scene:spine-preview-animation-time-change', this.onAnimationUpdateBind); + + for (const prop in Elements) { + const element = Elements[prop]; + if (element.ready) { + element.ready.call(this); + } + } +}; + +exports.update = function(assetList, metaList) { + this.assetList = assetList; + this.metaList = metaList; + this.asset = assetList[0]; + this.meta = metaList[0]; + + // 如何多选就隐藏预览 + if (assetList.length > 1) { + this.$.container.style.display = 'none'; + } else { + this.$.container.style.display = 'block'; + } + + for (const prop in Elements) { + const element = Elements[prop]; + if (element.update) { + element.update.call(this); + } + } +}; + +exports.close = function() { + for (const prop in Elements) { + const element = Elements[prop]; + if (element.close) { + element.close.call(this); + } + } +};