diff --git a/README.en.md b/README.en.md index 1fc65f9..4321892 100644 --- a/README.en.md +++ b/README.en.md @@ -94,7 +94,7 @@ Click on *Extension -> Skeleton Viewer -> View* option to open the view panel. Click on *Extension -> Skeleton Viewer -> Settings* option to open the setting panel. -In the setting panel, you can choose a hotkey(shortcut, for opening the view panel quickly) in preset list, or customize one by yourself. +In the setting panel, you can choose a shortcut key(for opening the view panel quickly) in preset list, or customize one by yourself. One thing you should know, not every keys/keys-combinations can be used, because some keys/keys-combinations have been used by the system or Cocos Creator. diff --git a/i18n/en.js b/i18n/en.js index 04a686a..bf275c2 100644 --- a/i18n/en.js +++ b/i18n/en.js @@ -40,17 +40,19 @@ module.exports = { 'canvasColor': 'Canvas Color', // settings 'none': 'None', - 'selectKey': 'Hotkey', - 'selectKeyTooltip': 'Choose a hotkey', + 'selectKey': 'Shortcut Key', + 'selectKeyTooltip': 'Choose a shortcut key', 'customKey': 'Custom', - 'customKeyPlaceholder': 'Choose a hotkey above or customize one by yourself', - 'customKeyTooltip': 'You can also customize your own hotkey', - 'autoCheck': 'Auto Check Update', - 'autoCheckTooltip': 'Check if there is a new version when the extension is loaded', + 'customKeyPlaceholder': 'Choose a shortcut key above or customize one by yourself', + 'customKeyTooltip': 'You can also customize your own shortcut key', + 'alwaysOnTop': 'Always On Top', + 'alwaysOnTopTooltip': 'Keep view panel on top (3.3+)', + 'autoCheckUpdate': 'Auto Check Update', + 'autoCheckUpdateTooltip': 'Check if there is a new version when the extension is loaded', 'reference': '· Hotkey customization reference: ', 'accelerator': 'Keyboard Shortcuts', 'repository': '· Git repository of this extension: ', 'apply': 'Apply', 'quoteError': 'Do not use double quotes!', - 'customKeyError': 'Please specify a hotkey!', + 'customKeyError': 'Please specify a shortcut key!', }; diff --git a/i18n/zh.js b/i18n/zh.js index a44ec55..b8f5cea 100644 --- a/i18n/zh.js +++ b/i18n/zh.js @@ -45,8 +45,10 @@ module.exports = { 'customKey': '自定义', 'customKeyPlaceholder': '在上方选择一个快捷键或自定义一个快捷键', 'customKeyTooltip': '自定义快捷键', - 'autoCheck': '自动检查更新', - 'autoCheckTooltip': '扩展启动时自动检查是否有新版本', + 'alwaysOnTop': '窗口置顶', + 'alwaysOnTopTooltip': '预览窗口将始终显示在最前端 (3.3+)', + 'autoCheckUpdate': '自动检查更新', + 'autoCheckUpdateTooltip': '扩展启动时自动检查是否有新版本', 'reference': '· 快捷键自定义请参考:', 'accelerator': '键盘快捷键', 'repository': '· 本扩展的 Git 仓库:', diff --git a/images/icon.png b/images/icon.png new file mode 100644 index 0000000..3132f2f Binary files /dev/null and b/images/icon.png differ diff --git a/images/setting.png b/images/settings.png similarity index 100% rename from images/setting.png rename to images/settings.png diff --git a/package.json b/package.json index 724fc8b..1bb27ef 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,10 @@ { "name": "ccc-skeleton-viewer", - "version": "1.3.0.20210915", + "package_version": 2, + "version": "1.4.0.20220128", "description": "用于快速预览骨骼动画,提供独立窗口,也可以附着在编辑器中...", - "author": { - "name": "陈皮皮 (ifaswind)", + "author": "陈皮皮 (ifaswind)", + "author-info": { "email": "952157129@qq.com", "url": "https://chenpipi.cn", "wechat": "im_chenpipi", @@ -13,43 +14,104 @@ "repository": "https://gitee.com/ifaswind/ccc-skeleton-viewer", "license": "MIT", "main": "src/main/index.js", - "scene-script": "src/main/scene-worker.js", - "main-menu": { - "i18n:MAIN_MENU.package.title/i18n:ccc-skeleton-viewer.name/i18n:ccc-skeleton-viewer.view": { - "message": "ccc-skeleton-viewer:open-view-panel", - "icon": "/images/view.png" + "contributions": { + "menu": [ + { + "path": "i18n:menu.extension/i18n:ccc-skeleton-viewer.name", + "label": "i18n:ccc-skeleton-viewer.view", + "message": "open-view-panel", + "icon": "/images/view.png" + }, + { + "path": "i18n:menu.extension/i18n:ccc-skeleton-viewer.name", + "label": "i18n:ccc-skeleton-viewer.settings", + "message": "open-settings-panel", + "icon": "/images/settings.png" + }, + { + "path": "i18n:menu.extension/i18n:ccc-skeleton-viewer.name", + "label": "i18n:ccc-skeleton-viewer.checkUpdate", + "message": "menu-check-update", + "icon": "/images/update.png" + }, + { + "path": "i18n:menu.extension/i18n:ccc-skeleton-viewer.name", + "label": "v1.3.0.20210915", + "message": "menu-version", + "icon": "/images/version.png" + } + ], + "messages": { + "open-view-panel": { + "public": true, + "description": "打开预览面板", + "methods": [ + "openViewPanel" + ] + }, + "open-settings-panel": { + "public": true, + "description": "打开设置面板", + "methods": [ + "openSettingsPanel" + ] + }, + "menu-check-update": { + "public": true, + "description": "检查扩展是否有更新", + "methods": [ + "menuCheckUpdate" + ] + }, + "menu-version": { + "public": false, + "methods": [ + "menuVersion" + ] + }, + "scene:ready": { + "methods": [ + "onSceneReady" + ] + }, + "selection:select": { + "methods": [ + "onSelectionSelect" + ] + } }, - "i18n:MAIN_MENU.package.title/i18n:ccc-skeleton-viewer.name/i18n:ccc-skeleton-viewer.settings": { - "message": "ccc-skeleton-viewer:open-settings-panel", - "icon": "/images/setting.png" - }, - "i18n:MAIN_MENU.package.title/i18n:ccc-skeleton-viewer.name/i18n:ccc-skeleton-viewer.checkUpdate": { - "message": "ccc-skeleton-viewer:menu-check-update", - "icon": "/images/update.png" - }, - "i18n:MAIN_MENU.package.title/i18n:ccc-skeleton-viewer.name/v1.3.0.20210915": { - "message": "ccc-skeleton-viewer:menu-version", - "icon": "/images/version.png" + "shortcuts": [ + { + "message": "open-view-panel", + "mac": "", + "win": "" + } + ], + "assets": { + "menu": { + "methods": "src/editor/assets-menu.js", + "createMenu": "", + "assetMenu": "onAssetMenu", + "dbMenu": "", + "panelMenu": "" + } } }, - "panel.view": { - "main": "src/renderer/view/entry.js", - "type": "dockable", - "title": "i18n:ccc-skeleton-viewer.name", - "width": 500, - "height": 600, - "min-width": 300, - "min-height": 250 - }, - "reload": { - "renderer": [ - "src/renderer/**/*" - ], - "ignore": [ - "config.json", - "CHANGELOG.md", - "README.md", - "README.en.md" - ] + "panels": { + "view": { + "title": "i18n:ccc-skeleton-viewer.name", + "type": "dockable", + "main": "src/renderer/view/entry.js", + "icon": "/images/icon.png", + "size": { + "width": 500, + "height": 600, + "min-width": 300, + "min-height": 250 + }, + "flags": { + "alwaysOnTop": true + } + } } } \ No newline at end of file diff --git a/src/common/config-manager.js b/src/common/config-manager.js index fd1369e..e4fd2a2 100644 --- a/src/common/config-manager.js +++ b/src/common/config-manager.js @@ -1,6 +1,5 @@ const Path = require('path'); const Fs = require('fs'); -const PackageUtil = require('../eazax/package-util'); /** 配置文件路径 */ const CONFIG_PATH = Path.join(__dirname, '../../config.json'); @@ -8,14 +7,8 @@ const CONFIG_PATH = Path.join(__dirname, '../../config.json'); /** package.json 的路径 */ const PACKAGE_PATH = Path.join(__dirname, '../../package.json'); -/** 包名 */ -const PACKAGE_NAME = PackageUtil.name; - -/** 快捷键行为 */ -const ACTION_NAME = 'view'; - -/** package.json 中的菜单项 key */ -const MENU_ITEM_KEY = `i18n:MAIN_MENU.package.title/i18n:${PACKAGE_NAME}.name/i18n:${PACKAGE_NAME}.${ACTION_NAME}`; +/** 快捷键消息 */ +const SHORTCUT_MESSAGE = 'open-view-panel'; /** * 配置管理器 @@ -36,8 +29,8 @@ const ConfigManager = { * 读取配置 */ get() { - // 配置 const config = ConfigManager.defaultConfig; + // 配置 if (Fs.existsSync(CONFIG_PATH)) { const localConfig = JSON.parse(Fs.readFileSync(CONFIG_PATH)); for (const key in config) { @@ -46,54 +39,82 @@ const ConfigManager = { } } } - - // 快捷键 - config.hotkey = ConfigManager.getAccelerator(); - + // 快捷键和置顶 + const packageConfig = ConfigManager.getPackageConfig(); + config.shortcutKey = packageConfig.shortcutKey; + config.alwaysOnTop = packageConfig.alwaysOnTop; // Done return config; }, /** * 保存配置 - * @param {*} config 配置 + * @param {*} value 配置 */ set(value) { - // 配置 const config = ConfigManager.defaultConfig; + // 配置 for (const key in config) { if (value[key] !== undefined) { config[key] = value[key]; } } Fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2)); - // 快捷键 - ConfigManager.setAccelerator(value.hotkey); + ConfigManager.setPackageConfig({ + shortcutKey: value.shortcutKey, + alwaysOnTop: value.alwaysOnTop, + }); }, /** - * 获取快捷键 - * @returns {string} + * 获取 package 配置 + * @returns {{ shortcutKey: string, alwaysOnTop: boolean }} */ - getAccelerator() { - const package = JSON.parse(Fs.readFileSync(PACKAGE_PATH)), - item = package['main-menu'][MENU_ITEM_KEY]; - return item['accelerator'] || ''; - }, - - /** - * 设置快捷键 - * @param {string} value - */ - setAccelerator(value) { - const package = JSON.parse(Fs.readFileSync(PACKAGE_PATH)), - item = package['main-menu'][MENU_ITEM_KEY]; - if (value != undefined && value !== '') { - item['accelerator'] = value; - } else { - delete item['accelerator']; + getPackageConfig() { + const config = { + shortcutKey: '', + alwaysOnTop: false, + }; + const package = JSON.parse(Fs.readFileSync(PACKAGE_PATH)); + // 快捷键 + const shortcuts = package['contributions']['shortcuts']; + if (shortcuts && shortcuts.length > 0) { + config.shortcutKey = shortcuts[0]['win'] || shortcuts[0]['mac'] || ''; } + // 置顶 + config.alwaysOnTop = package['panels']['view']['flags']['alwaysOnTop']; + // Done + return config; + }, + + /** + * 设置 package 配置 + * @param {{ shortcutKey: string, alwaysOnTop: boolean }} config + */ + setPackageConfig(config) { + const package = JSON.parse(Fs.readFileSync(PACKAGE_PATH)); + // 快捷键 + let shortcuts = package['contributions']['shortcuts']; + if (!shortcuts) { + shortcuts = package['contributions']['shortcuts'] = []; + } + let item = shortcuts[0]; + if (!item) { + item = shortcuts[0] = { + message: SHORTCUT_MESSAGE, + mac: '', + mac: '', + }; + } + if (config.shortcutKey != undefined) { + item['win'] = item['mac'] = config.shortcutKey; + } else { + item['win'] = item['mac'] = ''; + } + // 置顶 + package['panels']['view']['flags']['alwaysOnTop'] = config.alwaysOnTop; + // 写入 Fs.writeFileSync(PACKAGE_PATH, JSON.stringify(package, null, 2)); }, diff --git a/src/common/editor-adapter.js b/src/common/editor-adapter.js new file mode 100644 index 0000000..977ce1f --- /dev/null +++ b/src/common/editor-adapter.js @@ -0,0 +1,125 @@ +/** + * 编辑器适配器 + */ +const EditorAdapter = { + + /** + * 获取编辑器语言 + * @returns {string} + */ + getLanguage() { + return Editor.I18n.getLanguage(); + }, + + /** + * 获取资源信息 + * @param {string} uuid + * @returns {Promise} + */ + getAssetInfoByUuid(uuid) { + return Editor.Message.request('asset-db', 'query-asset-info', uuid); + }, + + /** + * 获取资源 META + * @param {string} uuid + * @returns {Promise} + */ + getAssetMetaByUuid(uuid) { + return Editor.Message.request('asset-db', 'query-asset-meta', uuid); + }, + + /** + * 获取资源绝对路径 + * @param {string} uuid + * @returns {Promise} + */ + getPathByUuid(uuid) { + return Editor.Message.request('asset-db', 'query-path', uuid); + }, + + /** + * 获取资源绝对路径 + * @param {string} url + * @returns {Promise} + */ + getPathByUrl(url) { + return Editor.Message.request('asset-db', 'query-path', url); + }, + + /** + * 获取资源 uuid + * @param {string} path + * @returns {Promise} + */ + getUuidByPath(path) { + return Editor.Message.request('asset-db', 'query-uuid', path); + }, + + /** + * 面板 + */ + Panel: { + + /** + * 打开面板 + * @param {string} panel + */ + open(panel) { + Editor.Panel.open(panel); + }, + + /** + * 关闭面板 + * @param {string} panel + */ + close(panel) { + Editor.Panel.close(panel); + }, + + }, + + /** + * 选择 + */ + Selection: { + + /** + * 清除编辑器选中 + * @param {'asset' | 'node'} type + */ + clear(type) { + Editor.Selection.clear(type); + }, + + /** + * 清除编辑器选中 + * @param {'asset' | 'node'} type + * @param {string} uuid + */ + select(type, uuid) { + Editor.Selection.select(type, uuid); + }, + + /** + * 获取编辑器选中的类型 + * @returns {'asset' | 'node'} + */ + getSelectedType() { + return Editor.Selection.getLastSelectedType(); + }, + + /** + * 获取编辑器选中 + * @param {'asset' | 'node'} type + * @returns {string} + */ + getSelected(type) { + return Editor.Selection.getSelected(type); + }, + + }, + +}; + +module.exports = EditorAdapter; diff --git a/src/eazax/css/cocos-class.css b/src/eazax/css/cocos-class.css index 4ea03b1..0036e3c 100644 --- a/src/eazax/css/cocos-class.css +++ b/src/eazax/css/cocos-class.css @@ -1,5 +1,5 @@ /* - Cocos Creator 风格样式 + Cocos Creator (2.x) 编辑器风格样式 版本: 20210911 作者: 陈皮皮 (ifaswind) 主页: https://gitee.com/ifaswind diff --git a/src/eazax/css/cocos-tag.css b/src/eazax/css/cocos-tag.css index ed85581..4202437 100644 --- a/src/eazax/css/cocos-tag.css +++ b/src/eazax/css/cocos-tag.css @@ -1,5 +1,5 @@ /* - Cocos Creator 风格标签 (橙黑) + Cocos Creator (2.x) 编辑器风格标签 (橙黑) 版本: 20210725 作者: 陈皮皮 (ifaswind) 主页: https://gitee.com/ifaswind diff --git a/src/eazax/css/eazax-colors.css b/src/eazax/css/eazax-colors.css index 2e1a17e..a2b4e97 100644 --- a/src/eazax/css/eazax-colors.css +++ b/src/eazax/css/eazax-colors.css @@ -1,3 +1,11 @@ +/* + Cocos Creator (2.x) 编辑器风格颜色 (橙黑) + 版本: 20210725 + 作者: 陈皮皮 (ifaswind) + 主页: https://gitee.com/ifaswind + 公众号: 菜鸟小栈 +*/ + :root { /* 背景颜色 */ --eazax-bg-color: #454545; diff --git a/src/editor/assets-menu.js b/src/editor/assets-menu.js new file mode 100644 index 0000000..04e5abd --- /dev/null +++ b/src/editor/assets-menu.js @@ -0,0 +1,36 @@ +const RendererEvent = require("../eazax/renderer-event"); + +// 资源管理器菜单 +exports.onAssetMenu = function (assetInfo) { + if (test(assetInfo.file)) { + return [ + // 骨骼查看器 -> 预览 + { + label: 'i18n:ccc-skeleton-viewer.name', + submenu: [ + { + label: 'i18n:ccc-skeleton-viewer.view', + enabled: true, + click() { + // (主进程)预览 + RendererEvent.send('view', assetInfo.uuid); + }, + }, + ], + }, + ]; + } + return []; +}; + +/** + * 测试 + * @param {string} name + */ +function test(name) { + return ( + name.endsWith('json') || name.endsWith('skel') || + name.endsWith('png') || + name.endsWith('atlas') || name.endsWith('txt') + ); +} diff --git a/src/main/index.js b/src/main/index.js index e7aae6d..659d795 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1,19 +1,28 @@ +'use strict'; + const MainEvent = require('../eazax/main-event'); const EditorMainKit = require('../eazax/editor-main-kit'); -const { checkUpdate } = require('../eazax/editor-main-util'); +const { checkUpdate, reload } = require('../eazax/editor-main-util'); const { openRepository } = require('../eazax/package-util'); const ConfigManager = require('../common/config-manager'); const Opener = require('./opener'); const PanelManager = require('./panel-manager'); +const Updater = require('../eazax/updater'); +const EditorAdapter = require('../common/editor-adapter'); /** * 生命周期:加载 */ function load() { + // 设置仓库分支 + Updater.branch = 'v3'; // 监听事件 EditorMainKit.register(); MainEvent.on('ready', onReadyEvent); + MainEvent.on('close', onCloseEvent); + MainEvent.on('reload', onReloadEvent); MainEvent.on('select', onSelectEvent); + MainEvent.on('view', onViewEvent); } /** @@ -23,18 +32,49 @@ function unload() { // 取消事件监听 EditorMainKit.unregister(); MainEvent.removeAllListeners('ready'); + MainEvent.removeAllListeners('close'); + MainEvent.removeAllListeners('reload'); MainEvent.removeAllListeners('select'); + MainEvent.removeAllListeners('view'); } /** - * (渲染进程)就绪事件回调 + * (渲染进程)预览面板就绪事件回调 * @param {Electron.IpcMainEvent} event */ function onReadyEvent(event) { + // 保存预览面板的 WebContents + PanelManager.viewWebContents = event.sender; // 检查编辑器选中 Opener.checkEditorCurSelection(); } +/** + * (渲染进程)预览面板关闭事件回调 + * @param {Electron.IpcMainEvent} event + */ +function onCloseEvent(event) { + PanelManager.viewWebContents = null; +} + +/** + * (渲染进程)重新加载事件回调 + * @param {Electron.IpcMainEvent} event + */ +function onReloadEvent(event) { + reload(); +} + +/** + * (渲染进程)预览事件回调 + * @param {Electron.IpcMainEvent} event + */ +function onViewEvent(event, uuid) { + PanelManager.openViewPanel(); + EditorAdapter.Selection.clear('asset'); + EditorAdapter.Selection.select('asset', uuid); +} + /** * (渲染进程)选择文件事件回调 * @param {Electron.IpcMainEvent} event @@ -48,74 +88,65 @@ function onSelectEvent(event) { * @param {string} type 类型 * @param {string[]} uuids uuids */ -function onEditorSelection(type, uuids) { - if (PanelManager.getViewPanel()) { +function onSelectionSelect(type, uuids) { + if (PanelManager.getViewPanelWebContents()) { Opener.identifySelection(type, uuids); } } -module.exports = { +exports.load = load; + +exports.unload = unload; + +exports.methods = { /** - * 扩展消息 + * 打开预览面板 */ - messages: { - - /** - * 编辑器选中事件回调 - * @param {Electron.IpcMainEvent} event - * @param {string} type 类型 - * @param {string[]} uuids uuids - */ - 'selection:selected'(event, type, uuids) { - onEditorSelection(type, uuids); - }, - - /** - * 打开预览面板 - */ - 'open-view-panel'() { - PanelManager.openViewPanel(); - }, - - /** - * 打开设置面板 - */ - 'open-settings-panel'() { - PanelManager.openSettingsPanel(); - }, - - /** - * 检查更新 - */ - 'menu-check-update'() { - checkUpdate(true); - }, - - /** - * 版本 - * @param {*} event - */ - 'menu-version'(event) { - openRepository(); - }, - - /** - * 场景面板加载完成后 - * @param {*} event - */ - 'scene:ready'(event) { - // 自动检查更新 - const config = ConfigManager.get(); - if (config.autoCheckUpdate) { - checkUpdate(false); - } - }, - + openViewPanel() { + PanelManager.openViewPanel(); }, - load, + /** + * 打开设置面板 + */ + openSettingsPanel() { + PanelManager.openSettingsPanel(); + }, - unload, + /** + * 检查更新 + */ + menuCheckUpdate() { + checkUpdate(true); + }, + + /** + * 版本号 + */ + menuVersion() { + openRepository(); + }, + + /** + * 场景编辑器就绪后 + */ + onSceneReady() { + // 自动检查更新 + const config = ConfigManager.get(); + if (config.autoCheckUpdate) { + checkUpdate(false); + } + }, + + /** + * 编辑器选中事件回调 + * @param {'asset' | 'node'} type 类型 + * @param {string} uuid uuid + */ + onSelectionSelect(type, uuid) { + const uuids = EditorAdapter.Selection.getSelected(type); + onSelectionSelect(type, uuids); + }, }; diff --git a/src/main/opener.js b/src/main/opener.js index c48f860..5dd8b87 100644 --- a/src/main/opener.js +++ b/src/main/opener.js @@ -1,28 +1,25 @@ const { dialog } = require('electron'); const Fs = require('fs'); const Path = require('path'); +const EditorAdapter = require('../common/editor-adapter'); const { print, translate } = require('../eazax/editor-main-util'); const MainEvent = require('../eazax/main-event'); -const PackageUtil = require('../eazax/package-util'); const PanelManager = require('./panel-manager'); -/** 包名 */ -const PACKAGE_NAME = PackageUtil.name; - +/** + * 资源检索器 + */ const Opener = { /** * 编辑器选择 - * @param {string} type + * @param {'asset' | 'node'} type * @param {string[]} uuids */ async identifySelection(type, uuids) { - // 选中资源 - if (type === 'asset') { + if (type === 'asset') { // 选中资源 Opener.identifyByUuids(uuids); - } - // 选中节点 - else if (type === 'node') { + } else if (type === 'node') { // 选中节点 const skeletonUuid = await Opener.querySkeletonOnNode(uuids[0]); if (skeletonUuid) { Opener.identifyByUuids([skeletonUuid]); @@ -38,9 +35,10 @@ const Opener = { * 检查编辑器当前选中 */ checkEditorCurSelection() { - const { type, id } = Editor.Selection.curGlobalActivate(); - if (type && id) { - Opener.identifySelection(type, [id]); + const type = EditorAdapter.Selection.getSelectedType(), + uuids = EditorAdapter.Selection.getSelected(type); + if (type && uuids && uuids.length > 0) { + Opener.identifySelection(type, uuids); } else { Opener.updateView(null); } @@ -48,15 +46,21 @@ const Opener = { /** * 查找节点上引用的骨骼资源 - * @param {string} uuid + * @param {string} nodeUuid * @returns {Promise} */ - querySkeletonOnNode(uuid) { - return new Promise(res => { - Editor.Scene.callSceneScript(PACKAGE_NAME, 'query-skeleton', uuid, (error, uuid) => { - res(error ? null : uuid); - }); - }); + async querySkeletonOnNode(nodeUuid) { + const node = await Editor.Message.request('scene', 'query-node', nodeUuid); + if (node && node['__comps__']) { + const components = node['__comps__']; + for (let i = 0; i < components.length; i++) { + if (components[i].type === 'sp.Skeleton') { + const uuid = components[i].value.skeletonData.value.uuid; + return (uuid !== '' ? uuid : null); + } + } + } + return null; }, /** @@ -85,40 +89,36 @@ const Opener = { * 通过 uuid 识别资源 * @param {string[]} uuids */ - identifyByUuids(uuids) { + async identifyByUuids(uuids) { // 资源路径 - let spinePath, texturePath, atlasPath; + let skeletonPath, texturePath, atlasPath; // 遍历选中的资源 uuid for (let i = 0; i < uuids.length; i++) { - const assetInfo = Editor.assetdb.assetInfoByUuid(uuids[i]), - { type, path } = assetInfo; - if (type === 'spine') { - spinePath = path; // Spine 资源 - } else if (type === 'texture') { - texturePath = path; // 纹理资源 - } else if (path.endsWith('.atlas') || path.endsWith('.txt')) { - atlasPath = path; // 图集资源 + const assetInfo = await EditorAdapter.getAssetInfoByUuid(uuids[0]), + { type, file } = assetInfo; + if (type === 'sp.SkeletonData') { + skeletonPath = file; // 骨骼资源 + } else if (type === 'cc.ImageAsset') { + texturePath = file; // 纹理资源 + } else if (file.endsWith('.atlas') || file.endsWith('.txt')) { + atlasPath = file; // 图集资源 } // 只识别一套资源 - if (spinePath && texturePath && atlasPath) { + if (skeletonPath && texturePath && atlasPath) { break; } } - // 是否有选中 Spine 资源 - if (!spinePath) { + // 未选中骨骼资源 + // if (!skeletonPath) { + // return; + // } + // 无效 + if (!skeletonPath && !texturePath && !atlasPath) { Opener.updateView(null); return; } - // 是否有选中图集资源 - if (!texturePath) { - // 读取 Spine 资源的 meta 信息中获取关联的纹理资源 - const spineMeta = Editor.assetdb.loadMetaByPath(spinePath); - if (spineMeta.textures[0]) { - texturePath = Editor.assetdb.uuidToFspath(spineMeta.textures[0]); - } - } // 处理路径 - let paths = { spinePath, texturePath, atlasPath }; + const paths = { skeletonPath, texturePath, atlasPath }; const assets = Opener.collectAssets(paths); Opener.updateView(assets); }, @@ -129,7 +129,7 @@ const Opener = { */ identifyByPaths(paths) { // 资源路径 - let spinePath, texturePath, atlasPath; + let skeletonPath, texturePath, atlasPath; // 遍历选中的文件路径 for (let i = 0; i < paths.length; i++) { const path = paths[i], @@ -137,7 +137,7 @@ const Opener = { switch (extname) { case '.json': case '.skel': { - spinePath = path; + skeletonPath = path; break; } case '.png': { @@ -151,65 +151,85 @@ const Opener = { } } // 只识别一套资源 - if (spinePath && texturePath && atlasPath) { + if (skeletonPath && texturePath && atlasPath) { break; } } - // 是否有选中 spine 资源 - if (!spinePath) { - print('warn', translate('noSkeleton')); + // 未选中骨骼资源 + // if (!skeletonPath) { + // print('warn', translate('noSkeleton')); + // return; + // } + // 无效 + if (!skeletonPath && !texturePath && !atlasPath) { + // print('warn', translate('noSkeleton')); return; } // 处理路径 - paths = { spinePath, texturePath, atlasPath }; + paths = { skeletonPath, texturePath, atlasPath }; const assets = Opener.collectAssets(paths); Opener.updateView(assets); }, /** * 收集资源 - * @param {{ spinePath: string, texturePath: string, atlasPath: string }} paths 资源路径 + * @param {{ skeletonPath: string, texturePath: string, atlasPath: string }} paths 资源路径 */ collectAssets(paths) { - let { spinePath, texturePath, atlasPath } = paths; + let { skeletonPath, texturePath, atlasPath } = paths; + const testPath = skeletonPath || texturePath || atlasPath; + // 骨骼资源 + if (!skeletonPath) { + // 暴力查找 + skeletonPath = Opener.getRelatedFile(testPath, 'json'); + // 还没有的话再试试 skel 格式 + if (!skeletonPath) { + skeletonPath = Opener.getRelatedFile(testPath, 'skel'); + } + // 找不到骨骼啊 + if (!skeletonPath) { + // print('warn', translate('noSkeleton')); + return null; + } + } // 纹理资源 if (!texturePath) { // 暴力查找 - texturePath = Opener.getRelatedFile(spinePath, 'png'); - } - // 找不到纹理啊 - if (!texturePath) { - print('warn', translate('noTexture')); - return null; + texturePath = Opener.getRelatedFile(testPath, 'png'); + // 找不到纹理啊 + if (!texturePath) { + print('warn', translate('noTexture')); + return null; + } } // 图集资源 if (!atlasPath) { // 暴力查找 - atlasPath = Opener.getRelatedFile(spinePath, 'atlas'); + atlasPath = Opener.getRelatedFile(testPath, 'atlas'); // 还没有的话再试试 txt 格式 if (!atlasPath) { - atlasPath = Opener.getRelatedFile(spinePath, 'txt'); + atlasPath = Opener.getRelatedFile(testPath, 'txt'); } // 还没有的话再试试 atlas.txt 格式 if (!atlasPath) { - atlasPath = Opener.getRelatedFile(spinePath, 'atlas.txt'); + atlasPath = Opener.getRelatedFile(testPath, 'atlas.txt'); + } + // 找不到图集啊 + if (!atlasPath) { + print('warn', translate('noAtlas')); + return null; } } - // 找不到图集啊 - if (!atlasPath) { - print('warn', translate('noAtlas')); - return null; - } // 文件类型(json 或 skel) - const spineType = Path.extname(spinePath); + const skeletonType = Path.extname(skeletonPath); // 打包资源信息 const assets = { // 目录路径 dir: undefined, // 骨骼数据(JSON) - json: (spineType === '.json') ? spinePath : undefined, + json: (skeletonType === '.json') ? skeletonPath : undefined, // 骨骼数据(二进制) - skel: (spineType === '.skel') ? spinePath : undefined, + skel: (skeletonType === '.skel') ? skeletonPath : undefined, // 纹理 png: texturePath, // 图集 @@ -247,7 +267,7 @@ const Opener = { * @param {{ dir: string, json: string, atlas: string, png: string } | null} assets */ updateView(assets) { - const webContents = PanelManager.getViewPanel(); + const webContents = PanelManager.getViewPanelWebContents(); if (webContents) { MainEvent.send(webContents, 'assets-selected', assets); } diff --git a/src/main/panel-manager.js b/src/main/panel-manager.js index d5c7467..611a6f5 100644 --- a/src/main/panel-manager.js +++ b/src/main/panel-manager.js @@ -2,7 +2,8 @@ const { BrowserWindow } = require('electron'); const { join } = require('path'); const PackageUtil = require('../eazax/package-util'); const { language, translate } = require('../eazax/editor-main-util'); -const { calcWindowPosition } = require('../eazax/window-util'); +const { calcWindowPositionByFocused } = require('../eazax/window-util'); +const EditorAdapter = require('../common/editor-adapter'); /** 包名 */ const PACKAGE_NAME = PackageUtil.name; @@ -15,22 +16,33 @@ const EXTENSION_NAME = translate('name'); */ const PanelManager = { + /** + * 预览面板的 WebContents + * @type {Electron.WebContents} + */ + viewWebContents: null, + /** * 打开预览面板 */ openViewPanel() { - Editor.Panel.open(`${PACKAGE_NAME}.view`); + EditorAdapter.Panel.open(`${PACKAGE_NAME}.view`); }, /** - * 获取预览面板 - * @returns {Electron.WebContents | null} + * 关闭预览面板 */ - getViewPanel() { - const panel = Editor.Panel.findWindow(`${PACKAGE_NAME}.view`); - if (panel) { - const webContents = panel.nativeWin.webContents; - return webContents; + closeViewPanel() { + EditorAdapter.Panel.close(`${PACKAGE_NAME}.view`); + }, + + /** + * 获取预览面板的 WebContents + * @returns {Electron.WebContents} + */ + getViewPanelWebContents() { + if (PanelManager.viewWebContents && !PanelManager.viewWebContents.isDestroyed()) { + return PanelManager.viewWebContents; } return null; }, @@ -50,9 +62,9 @@ const PanelManager = { PanelManager.settings.show(); return; } - // 窗口高度和位置 - const winSize = [500, 290], - winPos = calcWindowPosition(winSize, 'center'); + // 窗口尺寸和位置(macOS 标题栏高 28px) + const winSize = [500, 350], + winPos = calcWindowPositionByFocused(winSize, 'center'); // 创建窗口 const win = PanelManager.settings = new BrowserWindow({ width: winSize[0], @@ -61,7 +73,6 @@ const PanelManager = { minHeight: winSize[1], x: winPos[0], y: winPos[1] - 100, - useContentSize: true, frame: true, title: `${EXTENSION_NAME} | Cocos Creator`, autoHideMenuBar: true, @@ -78,7 +89,7 @@ const PanelManager = { contextIsolation: false, }, }); - // 就绪后展示(避免闪烁) + // 就绪后(展示,避免闪烁) win.on('ready-to-show', () => win.show()); // 关闭后 win.on('closed', () => (PanelManager.settings = null)); @@ -86,7 +97,7 @@ const PanelManager = { win.webContents.on('before-input-event', (event, input) => { if (input.key === 'Escape') PanelManager.closeSettingsPanel(); }); - // 调试用的 devtools(detach 模式需要取消失焦自动关闭) + // 调试用的 devtools // win.webContents.openDevTools({ mode: 'detach' }); // 加载页面 const path = join(__dirname, '../renderer/settings/index.html'); diff --git a/src/main/scene-worker.js b/src/main/scene-worker.js deleted file mode 100644 index 911429b..0000000 --- a/src/main/scene-worker.js +++ /dev/null @@ -1,32 +0,0 @@ -module.exports = { - - /** - * 查询节点上的骨骼资源 - * @param {*} event - * @param {string} uuid - * @returns - */ - 'query-skeleton'(event, uuid) { - // 获取节点 - const node = cc.engine.getInstanceById(uuid); - if (!node) { - event.reply(null, null); - return; - } - // 获取节点上的骨骼组件 - const spine = node.getComponent('sp.Skeleton'); - if (!spine) { - event.reply(null, null); - return; - } - // 获取骨骼数据 - const skeletonData = spine.skeletonData; - if (!skeletonData) { - event.reply(null, null); - return; - } - // 返回资源的 uuid - event.reply(null, skeletonData._uuid); - }, - -}; diff --git a/src/renderer/settings/index.html b/src/renderer/settings/index.html index d99ff8c..1a943a9 100644 --- a/src/renderer/settings/index.html +++ b/src/renderer/settings/index.html @@ -40,11 +40,21 @@ + +
+
+ {{ t('alwaysOnTop') }} + {{ t('alwaysOnTopTooltip') }} +
+
+ +
+
- {{ t('autoCheck') }} - {{ t('autoCheckTooltip') }} + {{ t('autoCheckUpdate') }} + {{ t('autoCheckUpdateTooltip') }}
diff --git a/src/renderer/settings/index.js b/src/renderer/settings/index.js index 6606ff6..346d683 100644 --- a/src/renderer/settings/index.js +++ b/src/renderer/settings/index.js @@ -39,6 +39,8 @@ const App = { const selectKey = ref(''); // 自定义 const customKey = ref(''); + // 窗口置顶 + const alwaysOnTop = ref(true); // 自动检查更新 const autoCheckUpdate = ref(false); @@ -66,27 +68,31 @@ const App = { */ function getConfig() { const config = ConfigManager.get(); - if (!config) return; + if (!config) { + return; + } + // 窗口置顶 + alwaysOnTop.value = config.alwaysOnTop; // 自动检查更新 autoCheckUpdate.value = config.autoCheckUpdate; // 快捷键 - const hotkey = config.hotkey; - if (!hotkey || hotkey === '') { + const shortcutKey = config.shortcutKey; + if (!shortcutKey || shortcutKey === '') { selectKey.value = ''; customKey.value = ''; return; } // 预设快捷键 - for (let i = 0, l = presets.length; i < l; i++) { - if (presets[i].key === hotkey) { - selectKey.value = hotkey; + for (let i = 0, l = presets.value.length; i < l; i++) { + if (presets.value[i].key === shortcutKey) { + selectKey.value = shortcutKey; customKey.value = ''; return; } } // 自定义快捷键 selectKey.value = 'custom'; - customKey.value = hotkey; + customKey.value = shortcutKey; } /** @@ -94,8 +100,9 @@ const App = { */ function setConfig() { const config = { + alwaysOnTop: alwaysOnTop.value, autoCheckUpdate: autoCheckUpdate.value, - hotkey: null, + shortcutKey: null, }; if (selectKey.value === 'custom') { // 自定义输入是否有效 @@ -109,12 +116,14 @@ const App = { EditorRendererKit.print('warn', translate('quoteError')); return; } - config.hotkey = customKey.value; + config.shortcutKey = customKey.value; } else { - config.hotkey = selectKey.value; + config.shortcutKey = selectKey.value; } // 保存到本地 ConfigManager.set(config); + // 重新加载扩展 + RendererEvent.send('reload'); } /** @@ -164,6 +173,7 @@ const App = { presets, selectKey, customKey, + alwaysOnTop, autoCheckUpdate, repositoryUrl, packageName, diff --git a/src/renderer/view/entry.js b/src/renderer/view/entry.js index fd969b4..c49c9c6 100644 --- a/src/renderer/view/entry.js +++ b/src/renderer/view/entry.js @@ -1,57 +1,70 @@ +'use strict'; + const { readFileSync } = require('fs'); const { join } = require('path'); -// ⚠️ 2.4.5 以上版本可以直接导入 -// 但是 2.4.5 以下版本无法正常访问 __dirname -// const Vue = require('../../../lib/vue.global.prod'); -// const App = require('./index'); +// 插件内置的 Vue +const Vue = require('../../../lib/vue.global.prod'); +// 面板代码 +const App = require('./index'); -// ⚠️ 2.4.5 以下版本只能通过 Editor.url 来获取插件路径 -const PACKAGE_NAME = 'ccc-skeleton-viewer'; -const PACKAGE_PATH = Editor.url(`packages://${PACKAGE_NAME}/`); -const DIR_PATH = join(PACKAGE_PATH, 'src/renderer/view/'); +// 面板 Vue 实例 +let app = null; -const Vue = require(join(PACKAGE_PATH, 'lib/vue.global.prod')); -const App = require(join(DIR_PATH, 'index')); +// html 文本 +exports.template = readFileSync(join(__dirname, 'index.html'), 'utf8'); -// 创建面板 -Editor.Panel.extend({ +// 样式文本 +exports.style = ''; - /** HTML */ - // template: readFileSync(join(__dirname, 'index.html'), 'utf8'), - template: readFileSync(join(DIR_PATH, 'index.html'), 'utf8'), +// 渲染后 html 选择器 +exports.$ = { + app: '#app', +}; - /** - * 面板渲染成功 - */ - ready() { - const root = this.shadowRoot; - // 加载样式表 - // loadCss(root, join(__dirname, '../../eazax/css/cocos-tag.css')); - // loadCss(root, join(__dirname, '../../eazax/css/cocos-class.css')); - // loadCss(root, join(__dirname, 'index.css')); - loadCSS(root, join(PACKAGE_PATH, 'src/eazax/css/cocos-tag.css')); - loadCSS(root, join(PACKAGE_PATH, 'src/eazax/css/cocos-class.css')); - loadCSS(root, join(DIR_PATH, 'index.css')); - // 先替换掉编辑器内置的 Vue - const oldVue = window.Vue; - window.Vue = Vue; - // 创建实例 - const app = Vue.createApp(App); - // 挂载 - app.mount(root); - // 把编辑器的 Vue 换回去 - window.Vue = oldVue; - }, +// 面板上的方法 +exports.methods = {}; -}); +// 面板上触发的事件 +exports.listeners = {}; + +// 当面板渲染成功后触发 +exports.ready = async function () { + const root = this.$.app.parentNode; + + // 加载样式表 + loadCss(root, join(__dirname, '../../eazax/css/cocos-tag.css')); + loadCss(root, join(__dirname, '../../eazax/css/cocos-class.css')); + loadCss(root, join(__dirname, 'index.css')); + + // 先替换掉编辑器内置的 Vue(理论上 3.x 编辑器不内置 Vue) + const builtinVue = window.Vue; + window.Vue = Vue; + + // 创建 Vue 实例 + app = Vue.createApp(App); + // 挂载 Vue 实例 + app.mount(root); + + // 把编辑器的 Vue 换回去 + window.Vue = builtinVue; +}; + +// 尝试关闭面板的时候触发 +exports.beforeClose = async function () { + // 卸载 Vue 实例 + app.unmount(); +}; + +// 当面板实际关闭后触发 +exports.close = async function () { }; /** * 加载样式表 * @param {HTMLElement} root 根元素 * @param {string} path CSS 文件路径 */ -function loadCSS(root, path) { +function loadCss(root, path) { const link = document.createElement('link'); link.rel = 'stylesheet'; link.type = 'text/css'; diff --git a/src/renderer/view/index.css b/src/renderer/view/index.css index 1fe7250..8ecb406 100644 --- a/src/renderer/view/index.css +++ b/src/renderer/view/index.css @@ -1,10 +1,18 @@ +#app { + --background-color: #474747; + --content-color: #2f2f2f; + --font-color: #bdbdbd; + --hover-color: #09f; + --active-color: #fd942b; +} + #app { width: 100%; height: 100%; - background-color: #454545; + background-color: var(--background-color); box-sizing: border-box; padding: 10px; - color: #bdbdbd; + color: var(--font-color); animation: fade-in 0.3s; animation-fill-mode: forwards; } @@ -31,7 +39,7 @@ .view { width: 100%; min-height: 0; - background-color: #4c4c4c; + background-color: var(--content-color); position: relative; flex: 1; } @@ -40,7 +48,7 @@ .canvas { width: 100%; height: 100%; - background-color: #4c4c4c; + background-color: var(--content-color); } /* 版本号 */ @@ -52,7 +60,8 @@ right: 6px; line-height: 16px; font-size: 11px; - color: #bdbdbd; + color: var(--font-color); + user-select: none; } /* 按钮容器 */ @@ -76,7 +85,7 @@ .button { width: 16px; height: 16px; - color: #bdbdbd; + color: var(--font-color); display: flex; align-items: center; justify-content: center; @@ -86,12 +95,12 @@ /* 信息,按钮:虚指 */ .info:hover, .button:hover { - color: #09f; + color: var(--hover-color); } /* 按钮:激活 */ .button:active { - color: #fd942b; + color: var(--active-color); } /* 信息,按钮 图标 */ @@ -117,6 +126,7 @@ margin-right: 2px; line-height: 12px; font-size: 11px; + user-select: none; } /* 位置 按钮 */ diff --git a/src/renderer/view/index.html b/src/renderer/view/index.html index 1f49dfe..95d4987 100644 --- a/src/renderer/view/index.html +++ b/src/renderer/view/index.html @@ -1,159 +1,148 @@ - - - - - - - - -
- -
- -
- - - -
- -
- - - - -
- -
- - - - -
- -
- - - - -
+
+ +
+ +
+ + + +
+ +
+ + + +
- -
- {{ t('spineRuntime') }}: {{ version }} + +
+ + + +
- -
- - -
- -
- -
{{ offset }}
- -
- - - - -
+ +
+ + + +
- -
- -
-
- {{ t('viewScale') }} -
-
- -
-
- -
-
- {{ t('skin') }} -
-
- -
-
- -
-
- {{ t('animation') }} -
-
- -
-
- -
-
- {{ t('loop') }} -
-
- -
-
- -
-
- {{ t('premultipliedAlpha') }} -
-
- -
-
- -
-
- {{ t('timeScale') }} -
-
- -
-
- -
-
- {{ t('drawBones') }} -
-
- -
-
- -
-
- {{ t('drawBoundingBoxes') }} -
-
- -
-
- -
-
- {{ t('drawMeshTriangles') }} -
-
- -
+ +
+ {{ t('spineRuntime') }}: {{ version }} +
+ +
+ + +
+ +
+ +
{{ offset }}
+ +
+ + + + +
+
+
+ +
+ +
+
+ {{ t('viewScale') }} +
+
+ +
+
+ +
+
+ {{ t('skin') }} +
+
+ +
+
+ +
+
+ {{ t('animation') }} +
+
+ +
+
+ +
+
+ {{ t('loop') }} +
+
+ +
+
+ +
+
+ {{ t('premultipliedAlpha') }} +
+
+ +
+
+ +
+
+ {{ t('timeScale') }} +
+
+ +
+
+ +
+
+ {{ t('drawBones') }} +
+
+ +
+
+ +
+
+ {{ t('drawBoundingBoxes') }} +
+
+ +
+
+ +
+
+ {{ t('drawMeshTriangles') }} +
+
+
-
- - - \ No newline at end of file +
+
\ No newline at end of file diff --git a/src/renderer/view/index.js b/src/renderer/view/index.js index 629c296..41d8bd8 100644 --- a/src/renderer/view/index.js +++ b/src/renderer/view/index.js @@ -1,923 +1,914 @@ -const { shell } = require('electron'); -const Path = require('path'); -const Fs = require('fs'); -const I18n = require('../../eazax/i18n'); -const RendererEvent = require('../../eazax/renderer-event'); -const EditorRendererKit = require('../../eazax/editor-renderer-kit'); -const { hexToRGB } = require('../../eazax/color-util'); -const SpineRuntime = require('../../common/spine-runtime'); +module.exports = (function () { + 'use strict'; -/** 当前语言 */ -const LANG = Editor.lang || Editor.I18n.getLanguage(); + const { shell } = require('electron'); + const Path = require('path'); + const Fs = require('fs'); + const I18n = require('../../eazax/i18n'); + const RendererEvent = require('../../eazax/renderer-event'); + const EditorRendererKit = require('../../eazax/editor-renderer-kit'); + const { hexToRGB } = require('../../eazax/color-util'); + const SpineRuntime = require('../../common/spine-runtime'); + const EditorAdapter = require('../../common/editor-adapter'); -/** - * i18n - * @param {string} key - * @returns {string} - */ -const translate = (key) => I18n.translate(LANG, key); - -// 环境 -let canvas = null, - gl = null, - shader = null, - batcher = null, - mvp = null, - skeletonRenderer = null; -// 调试 -let debugRenderer = null, - debugShader = null, - shapeRenderer = null; -// 骨骼数据 -let skeleton = null, - bounds = null; -// 上一帧时间 -let lastFrameTime = null; - -// 拖动 -let isDragging = false, - clickOffset = [0, 0]; - -// 布局 -let layout = null, - resizeObserver = null, - resizeHandler = null; - -// 构建 Vue 应用 -const App = { + /** 当前语言 */ + const LANG = EditorAdapter.getLanguage(); /** - * 数据 + * i18n + * @param {string} key + * @returns {string} */ - data() { - return { - // 资源信息 - assets: { - dir: null, - json: null, - skel: null, - atlas: null, - png: null, - }, - // 选项 - viewScale: 1.0, - skin: '', - animation: '', - timeScale: 1, - loop: true, - premultipliedAlpha: false, - drawBones: false, - drawBoundingBoxes: false, - drawMeshTriangles: false, - drawPaths: false, - // 当前运行时版本 - version: 'unknown', - // 画布颜色 - canvasColor: '#4c4c4c', - clearColor: [0.3, 0.3, 0.3], - // 环境 - assetManager: null, - // 骨骼数据 - skeletonData: null, - animationState: null, - // 拖动 - dragOffset: [0, 0], - }; - }, + const translate = (key) => I18n.translate(LANG, key); - /** - * 计算属性 - */ - computed: { + // 元素 + let canvas = null, + layout = null, + properties = null; + // 元素观察者 + let resizeObserver = null; + // 环境 + let gl = null, + shader = null, + batcher = null, + mvp = null, + skeletonRenderer = null; + // 调试 + let debugRenderer = null, + debugShader = null, + shapeRenderer = null; + // 骨骼数据 + let skeleton = null, + bounds = null; + // 上一帧时间 + let lastFrameTime = null; + // 拖动 + let isDragging = false, + clickOffset = [0, 0]; + + // 构建 Vue 应用 + const App = { /** - * 皮肤列表 + * 数据 */ - skins() { - if (!this.skeletonData || !this.skeletonData.skins) { - return []; - } - return this.skeletonData.skins.map(v => v.name); - }, - - /** - * 动画列表 - */ - animations() { - if (!this.skeletonData || !this.skeletonData.animations) { - return []; - } - return this.skeletonData.animations.map(v => v.name); - }, - - /** - * 调试 - */ - debug() { - return ( - this.drawBones || - this.drawBoundingBoxes || - this.drawMeshTriangles || - this.drawPaths - ); - }, - - /** - * 动画时长 - */ - duration() { - if (!this.animationState) { - return 0; - } - return this.animationState.getCurrent(0).animation.duration; - }, - - /** - * 资源信息 - */ - assetsInfo() { - if (!this.assetManager) { - return `💡 ${translate('noAssets')}`; + data() { + return { + // 资源信息 + assets: { + dir: null, + json: null, + skel: null, + atlas: null, + png: null, + }, + // 选项 + viewScale: 1.0, + skin: '', + animation: '', + timeScale: 1, + loop: true, + premultipliedAlpha: false, + drawBones: false, + drawBoundingBoxes: false, + drawMeshTriangles: false, + drawPaths: false, + // 当前运行时版本 + version: 'unknown', + // 画布颜色 + canvasColor: '#4c4c4c', + clearColor: [0.3, 0.3, 0.3], + // 环境 + assetManager: null, + // 骨骼数据 + skeletonData: null, + animationState: null, + // 拖动 + dragOffset: [0, 0], }; - let skeletonPath = '', - texturePath = '', - atlasPath = ''; - for (const path in this.assetManager.assets) { - switch (Path.extname(path)) { - case '.json': - case '.skel': { - skeletonPath = path; - break; - } - case '.png': { - texturePath = path; - break; - } - case '.atlas': { - atlasPath = path; - break; + }, + + /** + * 计算属性 + */ + computed: { + + /** + * 皮肤列表 + */ + skins() { + if (!this.skeletonData || !this.skeletonData.skins) { + return []; + } + return this.skeletonData.skins.map(v => v.name); + }, + + /** + * 动画列表 + */ + animations() { + if (!this.skeletonData || !this.skeletonData.animations) { + return []; + } + return this.skeletonData.animations.map(v => v.name); + }, + + /** + * 调试 + */ + debug() { + return ( + this.drawBones || + this.drawBoundingBoxes || + this.drawMeshTriangles || + this.drawPaths + ); + }, + + /** + * 动画时长 + */ + duration() { + if (!this.animationState) { + return 0; + } + return this.animationState.getCurrent(0).animation.duration; + }, + + /** + * 资源信息 + */ + assetsInfo() { + if (!this.assetManager) { + return `💡 ${translate('noAssets')}`; + }; + let skeletonPath = '', + texturePath = '', + atlasPath = ''; + for (const path in this.assetManager.assets) { + switch (Path.extname(path)) { + case '.json': + case '.skel': { + skeletonPath = path; + break; + } + case '.png': { + texturePath = path; + break; + } + case '.atlas': { + atlasPath = path; + break; + } } } - } - return `💀 [Skeleton]\n· ${skeletonPath}\n\n🖼 [Texture]\n· ${texturePath}\n\n🗺 [Atlas]\n· ${atlasPath}`; + return `💀 [Skeleton]\n· ${skeletonPath}\n\n🖼 [Texture]\n· ${texturePath}\n\n🗺 [Atlas]\n· ${atlasPath}`; + }, + + /** + * 偏移 + */ + offset() { + return `(${this.dragOffset[0]}, ${-this.dragOffset[1]})`; + }, + }, /** - * 偏移 + * 监听属性 */ - offset() { - return `(${this.dragOffset[0]}, ${-this.dragOffset[1]})`; - }, + watch: { - }, + /** + * 当前皮肤 + * @param {string} value + */ + skin(value) { + // 设置皮肤 + this.setSkin(value); + }, - /** - * 监听属性 - */ - watch: { + /** + * 当前动画 + * @param {string} value + */ + animation(value) { + // 播放动画 + this.playAnimation(value); + }, + + /** + * 时间缩放 + * @param {number} value + */ + timeScale(value) { + value = parseFloat(value) || 0; + this.setTimeScale(value); + }, + + /** + * 循环 + * @param {boolean} value + */ + loop(value) { + // 重新播放 + this.playAnimation(this.animation); + }, + + /** + * 画布颜色 + * @param {string} value + */ + canvasColor(value) { + // 更新画布颜色 + canvas.style.backgroundColor = value; + // 获取 RGB 格式 + const { r, g, b } = hexToRGB(value); + // 保存颜色值 + this.clearColor = [r / 255, g / 255, b / 255]; + // 更新 gl 颜色 + if (gl) { + gl.clearColor(r / 255, g / 255, b / 255, 1); + gl.clear(gl.COLOR_BUFFER_BIT); + } + }, - /** - * 当前皮肤 - * @param {string} value - */ - skin(value) { - // 设置皮肤 - this.setSkin(value); }, /** - * 当前动画 - * @param {string} value + * 实例函数 */ - animation(value) { - // 播放动画 - this.playAnimation(value); - }, + methods: { - /** - * 时间缩放 - * @param {number} value - */ - timeScale(value) { - value = parseFloat(value) || 0; - this.setTimeScale(value); - }, + /** + * 重置 + */ + reset() { + // 资源信息 + this.assets = null; + // 选项 + this.viewScale = 1; + this.skin = ''; + this.animation = ''; + this.timeScale = 1; + this.loop = true; + this.premultipliedAlpha = false; + this.drawBones = false; + this.drawBoundingBoxes = false; + this.drawMeshTriangles = false; + this.drawPaths = false; + // 当前运行时版本 + this.version = 'unknown'; + // 恢复默认画布颜色 + this.canvasColor = '#2f2f2f'; + // 骨骼数据 + skeleton = null; + bounds = null; + this.skeletonData = null; + this.animationState = null; + // 清空画布 + gl && gl.clear(gl.COLOR_BUFFER_BIT); + // 环境 + shader = null; + batcher = null; + mvp = null; + skeletonRenderer = null; + this.assetManager = null; + // 调试 + debugRenderer = null; + debugShader = null; + shapeRenderer = null; + // 上一帧时间 + lastFrameTime = null; + // 拖动 + isDragging = false; + clickOffset = [0, 0]; + this.dragOffset = [0, 0]; + }, - /** - * 循环 - * @param {boolean} value - */ - loop(value) { - // 重新播放 - this.playAnimation(this.animation); - }, + /** + * 翻译 + * @param {string} key + */ + t(key) { + return translate(key); + }, - /** - * 画布颜色 - * @param {string} value - */ - canvasColor(value) { - // 更新画布颜色 - canvas.style.backgroundColor = value; - // 获取 RGB 格式 - const { r, g, b } = hexToRGB(value); - // 保存颜色值 - this.clearColor = [r / 255, g / 255, b / 255]; - // 更新 gl 颜色 - if (gl) { - gl.clearColor(r / 255, g / 255, b / 255, 1); + /** + * 资源信息按钮点击回调 + */ + onInfoBtnClick() { + if (!this.assets || !this.assets.dir) { + return; + } + const { dir, json, skel } = this.assets, + skeletonPath = Path.join(dir, (json || skel)); + // 在资源管理器中展示 spine 文件 + shell.showItemInFolder(skeletonPath) + }, + + /** + * 选择资源按钮点击回调 + */ + onSelectBtnClick() { + // (主进程)选择资源 + RendererEvent.send('select'); + }, + + /** + * 重置按钮点击回调 + */ + onResetBtnClick() { + this.reset(); + }, + + /** + * 复位按钮点击回调 + */ + onRepositionBtnClick() { + isDragging = false; + clickOffset = [0, 0]; + this.dragOffset = [0, 0]; + }, + + /** + * 获取 Spine 运行时 + */ + getRuntime() { + // 资源对应的 Spine 运行时版本 + let version = this.getAssetSpineVersion(this.assets.json || this.assets.skel); + if (!version) { + // RendererUtil.print('warn', translate('noVersion')); + // return false; + console.warn('Unable to identify Spine version of asset!'); + // 默认使用 3.8 的 Runtime + version = "3.8"; + } + // 处理版本号(保留前两个分量) + version = version.split('.').slice(0, 2).map(v => parseInt(v)).join('.'); + // 获取目标版本的 Spine 运行时对象 + const spine = SpineRuntime.get(version); + if (!spine) { + const content = `${translate('noSpineRuntime')} | ${translate('version')}: ${version}`; + EditorRendererKit.print('warn', content); + return false; + } + window.spine = spine; + this.version = spine.version; + return true; + }, + + /** + * 获取资源对应的 Spine 运行时版本 + * @param {string} path 文件路径 + * @returns {string} + */ + getAssetSpineVersion(path) { + const fullPath = Path.join((this.assets.dir || ''), path); + if (!Fs.existsSync(fullPath)) { + return null; + } + const extname = Path.extname(path); + if (extname === '.json') { + const data = JSON.parse(Fs.readFileSync(fullPath, 'utf-8')); + if (data.skeleton) { + return data.skeleton.spine; + } + } else if (extname === '.skel') { + return '3.8'; + } + return null; + }, + + /** + * 初始化 Spine 运行时 + */ + initRuntime() { + // 获取画布 + if (!canvas) { + canvas = this.$refs.canvas; + } + // WebGL + if (!gl) { + const config = { alpha: false }; + gl = canvas.getContext("webgl", config); + if (!gl) { + EditorRendererKit.print('warn', translate('noWebGL')); + return; + } + const color = this.clearColor; + gl.clearColor(color[0], color[1], color[2], 1); + } + + // Shader + shader = spine.webgl.Shader.newTwoColoredTextured(gl); + // 处理器 + batcher = new spine.webgl.PolygonBatcher(gl); + // MVP 变换矩阵 + mvp = new spine.webgl.Matrix4(); + mvp.ortho2d(0, 0, canvas.width - 1, canvas.height - 1); + // 骨骼渲染器 + skeletonRenderer = new spine.webgl.SkeletonRenderer(gl); + + // 用于调试的 debugRenderer、debugShader 和 shapeRenderer + debugRenderer = new spine.webgl.SkeletonDebugRenderer(gl); + debugShader = spine.webgl.Shader.newColored(gl); + shapeRenderer = new spine.webgl.ShapeRenderer(gl); + + // 资源管理器 + this.assetManager = new spine.webgl.AssetManager(gl); + }, + + /** + * 加载资源 + */ + loadAssets() { + const assetManager = this.assetManager; + if (!assetManager) { + return; + } + const assets = this.assets; + // 指定资源目录前缀 + if (assets.dir) { + assetManager.pathPrefix = assets.dir; + } + // 骨骼数据 + if (assets.json) { + // JSON + assetManager.loadText(assets.json); + } else if (assets.skel) { + // skel(二进制) + assetManager.loadBinary(assets.skel); + } else { + EditorRendererKit.print('warn', translate('noSkeletonData')); + return; + } + // 图集和纹理 + if (assetManager.loadTextureAtlas) { + // spine runtime 3.6+ + // loadTextureAtlas 内部会自动加载纹理 + assetManager.loadTextureAtlas(assets.atlas); + } else { + // spine runtime 3.5 + assetManager.loadText(assets.atlas); + assetManager.loadTexture(assets.png); + } + // 是否开启纹理预乘 + if (Path.basename(assets.png).includes('pma') || + Path.basename(assets.atlas).includes('pma')) { + this.premultipliedAlpha = true; + } + // 等待加载 + requestAnimationFrame(this.loading); + }, + + /** + * 等待加载 + */ + loading() { + if (!this.assetManager) { + return; + } + // 文件是否已加载完成 + if (this.assetManager.isLoadingComplete()) { + // 加载骨骼数据 + const result = this.loadSkeleton(); + if (!result) { + this.reset(); + return; + } + // 设置皮肤 + if (this.skins && this.skins[0]) { + // this.skeletonData.defaultSkin.name + this.setSkin(this.skins[0]); + } + // 播放动画 + if (this.animations && this.animations[0]) { + this.playAnimation(this.animations[0]); + } + // 记录当前帧时间 + lastFrameTime = Date.now() / 1000; + // 下一帧开始渲染 + requestAnimationFrame(this.render); + } else { + // 继续等待加载 + requestAnimationFrame(this.loading); + } + }, + + /** + * 加载骨骼数据 + */ + loadSkeleton() { + const assetManager = this.assetManager, + assets = this.assets; + + // 图集数据 + let atlas = assetManager.get(assets.atlas); + if (spine.version === '3.5') { + atlas = new spine.TextureAtlas(atlas); + } + // 创建 AtlasAttachmentLoader 对象用于处理部位、网格、包围盒和路径 + const atlasLoader = new spine.AtlasAttachmentLoader(atlas); + + try { + // 骨骼数据 + if (assets.json) { + // 创建 skeletonJson 对象用于解析 json 文件 + const skeletonJson = new spine.SkeletonJson(atlasLoader); + this.skeletonData = skeletonJson.readSkeletonData(assetManager.get(assets.json)); + } else if (assets.skel) { + // 创建 SkeletonBinary 对象用于解析 skel 文件 + const skeletonBinary = new spine.SkeletonBinary(atlasLoader); + this.skeletonData = skeletonBinary.readSkeletonData(assetManager.get(assets.skel)); + } + } catch (error) { + console.error(error); + EditorRendererKit.print('warn', translate('dataMismatch')); + return false; + } + + // 创建骨骼对象 + skeleton = new spine.Skeleton(this.skeletonData); + + // 计算边界 + bounds = this.calculateBounds(); + + // 创建 AnimationState 对象用于动画控制 + const animationStateData = new spine.AnimationStateData(skeleton.data); + this.animationState = new spine.AnimationState(animationStateData); + + // Done + return true; + }, + + /** + * 设置皮肤 + * @param {string} name + */ + setSkin(name) { + if (!skeleton) { + return; + } + this.skin = name; + // 设置皮肤 + skeleton.setSkinByName(name); + // 重置姿势 + skeleton.setSlotsToSetupPose(); + }, + + /** + * 播放动画 + * @param {string} name + */ + playAnimation(name) { + if (!skeleton) { + return; + } + this.animation = name; + // 重置姿势 + skeleton.setToSetupPose(); + // 播放动画 + this.animationState.setAnimation(0, name, this.loop); + }, + + /** + * 设置时间缩放 + * @param {number} value + */ + setTimeScale(value) { + if (!skeleton) { + return; + } + this.animationState.timeScale = value; + }, + + /** + * 计算边界 + * @returns {{ offset: { x: number, y: number }, size: { x: number, y: number } }} + */ + calculateBounds() { + skeleton.setToSetupPose(); + skeleton.updateWorldTransform(); + const offset = new spine.Vector2(), + size = new spine.Vector2(); + skeleton.getBounds(offset, size, []); + return { offset, size }; + }, + + /** + * 渲染骨骼 + */ + render() { + if (!skeleton) { + return; + } + // 计算帧时间差 + const now = Date.now() / 1000, + delta = now - lastFrameTime; + // 记录当前帧时间 + lastFrameTime = now; + + // 更新 mvp 来适配画布尺寸 + this.resizeView(); + + // 清空画布 gl.clear(gl.COLOR_BUFFER_BIT); - } + + // 应用动画并根据时间差值更新动画时间 + this.animationState.update(delta); + this.animationState.apply(skeleton); + // 更新骨骼 Transform + skeleton.updateWorldTransform(); + + // 渲染 + // 绑定 shader + shader.bind(); + // 传递属性 + shader.setUniformi(spine.webgl.Shader.SAMPLER, 0); + shader.setUniform4x4f(spine.webgl.Shader.MVP_MATRIX, mvp.values); + // 渲染骨骼 + batcher.begin(shader); + // 设置 skeletonRenderer 属性 + skeletonRenderer.premultipliedAlpha = this.premultipliedAlpha; + // 渲染 + skeletonRenderer.draw(batcher, skeleton); + batcher.end(); + // 解除 shader 绑定 + shader.unbind(); + + // 调试 + if (this.debug) { + // 绑定 shader + debugShader.bind(); + // 传递属性 + debugShader.setUniform4x4f(spine.webgl.Shader.MVP_MATRIX, mvp.values); + // 设置 debugRenderer 属性 + debugRenderer.premultipliedAlpha = this.premultipliedAlpha; + debugRenderer.drawBones = this.drawBones; + debugRenderer.drawBoundingBoxes = this.drawBoundingBoxes; + debugRenderer.drawRegionAttachments = this.drawBoundingBoxes; + debugRenderer.drawMeshHull = this.drawMeshTriangles; + debugRenderer.drawMeshTriangles = this.drawMeshTriangles; + debugRenderer.drawPaths = this.drawPaths; + debugRenderer.drawSkeletonXY = this.drawBones; + // 开始渲染 + shapeRenderer.begin(debugShader); + // 渲染 + debugRenderer.draw(shapeRenderer, skeleton); + shapeRenderer.end(); + // 解除 shader 绑定 + debugShader.unbind(); + } + + // 持续渲染 + requestAnimationFrame(this.render); + }, + + /** + * 更新视口尺寸 + */ + resizeView() { + // 更新画布尺寸 + const { clientWidth, clientHeight } = canvas; + if (canvas.width !== clientWidth || canvas.height !== clientHeight) { + canvas.width = clientWidth; + canvas.height = clientHeight; + } + // 骨骼位置以及缩放 + const canvasWidth = canvas.width, + canvasHeight = canvas.height; + // 计算中心点 + const centerX = (bounds.offset.x + (bounds.size.x / 2)) || 0, + centerY = (bounds.offset.y + (bounds.size.y / 2)) || 0; + // 计算缩放比例 + const ratioX = bounds.size.x / canvasWidth, + ratioY = bounds.size.y / canvasHeight; + let scale = Math.max(ratioX, ratioY) * 1.2; + if (scale < 1) scale = 1; + // 自定义缩放 + scale /= this.viewScale; + // 最终宽高 + const width = canvasWidth * scale, + height = canvasHeight * scale; + // 更新矩阵 + const x = (centerX - (width / 2)) - (this.dragOffset[0] * scale), + y = (centerY - (height / 2)) + (this.dragOffset[1] * scale); + mvp.ortho2d(x, y, width, height); + // 更新视口 + gl.viewport(0, 0, canvasWidth, canvasHeight); + }, + + /** + * (主进程)资源旋转回调 + * @param {Electron.ipcRendererEvent} event + * @param {{ dir?: string, json?: string, skel?: string, atlas: string, png: string }} assets 资源 + */ + onAssetsSelectedEvent(event, assets) { + // 重置 + if (this.assets) { + this.reset(); + } + // 未选中资源 + if (!assets) return; + // 储存 + this.assets = assets; + // 处理路径 + this.processAssetPaths(); + // 获取运行时 + const result = this.getRuntime(); + if (!result) return; + // 初始化运行时 + this.initRuntime(); + // 开始加载资源 + this.loadAssets(); + }, + + /** + * 处理资源路径 + */ + processAssetPaths() { + // ⚠️ Spine Runtime 在 Windows 平台下的问题 + // 使用 loadTextureAtlas 加载图集时会自动加载纹理 + // 但是 loadTextureAtlas 内部调用 loadTexture 时传递的 path 是文件名而不是完整路径 + // 如果没有指定 pathPrefix 属性,loadTexture 就会无法正常加载 + // 所以干脆都改为需要指定 pathPrefix 属性 + const assets = this.assets, + { dir, json, skel, png, atlas } = assets; + if (!dir) { + assets.dir = Path.dirname(json || skel); + } + if (!assets.dir.endsWith(Path.sep)) { + assets.dir += Path.sep; + } + if (json) { + assets.json = Path.basename(json); + } else if (skel) { + assets.skel = Path.basename(skel); + } + assets.atlas = Path.basename(atlas); + assets.png = Path.basename(png); + }, + + /** + * 画布鼠标滚轮事件回调 + * @param {WheelEvent} event + */ + onCanvasMouseWheel(event) { + if (!this.assets) { + return; + } + // 当前缩放 + let scale = this.viewScale; + // 缩放步长 + const step = Math.abs(scale) >= 1 ? 0.1 : 0.05; + // 方向 + if (event.wheelDelta > 0) { + // 向上(放大) + scale += step; + } else { + // 向下(缩小) + scale -= step; + } + // 处理精度 + scale = Math.round(scale * 100) / 100; + // 设置缩放 + this.viewScale = scale; + }, + + /** + * 画布鼠标点击事件回调 + * @param {MouseEvent} event + */ + onCanvasMouseDown(event) { + if (!this.assets) { + return; + } + isDragging = true; + const x = event.offsetX - this.dragOffset[0], + y = event.offsetY - this.dragOffset[1]; + clickOffset = [x, y]; + }, + + /** + * 画布鼠标移动事件回调 + * @param {MouseEvent} event + */ + onCanvasMouseMove(event) { + if (!isDragging) { + return; + } + const x = event.offsetX - clickOffset[0], + y = event.offsetY - clickOffset[1]; + this.dragOffset = [x, y]; + }, + + /** + * 画布鼠标松开事件回调 + * @param {MouseEvent} event + */ + onCanvasMouseUp(event) { + isDragging = false; + clickOffset = [0, 0]; + }, + + /** + * 画布鼠标离开事件回调 + * @param {MouseEvent} event + */ + onCanvasMouseLeave(event) { + isDragging = false; + clickOffset = [0, 0]; + }, + + /** + * 布局尺寸变化回调 + */ + onLayoutResize() { + const layoutStyle = layout.style, + propertiesStyle = properties.style; + if (layout.clientWidth >= 800 || layout.clientHeight < 330) { + if (layout.clientWidth >= 350) { + // 水平布局 + layoutStyle.flexDirection = 'row'; + propertiesStyle.width = '265px'; + propertiesStyle.marginTop = '0'; + propertiesStyle.marginLeft = '5px'; + propertiesStyle.display = 'flex'; + } else { + // 隐藏选项 + propertiesStyle.display = 'none'; + } + } else { + // 垂直布局 + layoutStyle.flexDirection = 'column'; + propertiesStyle.width = '100%'; + propertiesStyle.marginTop = '5px'; + propertiesStyle.marginLeft = '0'; + propertiesStyle.display = 'flex'; + } + }, + }, - }, - - /** - * 实例函数 - */ - methods: { + /** + * 生命周期:挂载后 + */ + mounted() { + // 收集元素 + canvas = this.$refs.canvas; + layout = this.$refs.layout; + properties = this.$refs.properties; + // 监听画布事件 + canvas.addEventListener('mousewheel', this.onCanvasMouseWheel); // 监听画布鼠标滚轮 + canvas.addEventListener('mousedown', this.onCanvasMouseDown); // 监听画布鼠标点击 + canvas.addEventListener('mousemove', this.onCanvasMouseMove); // 监听画布鼠标移动 + canvas.addEventListener('mouseup', this.onCanvasMouseUp); // 监听画布鼠标松开 + canvas.addEventListener('mouseleave', this.onCanvasMouseLeave); // 监听画布鼠标离开 + // (主进程)监听资源选择事件 + RendererEvent.on('assets-selected', this.onAssetsSelectedEvent); + // (下一帧)发送事件给主进程 + this.$nextTick(() => { + RendererEvent.send('ready'); // (主进程)已就绪 + RendererEvent.send('check-update', false); // (主进程)检查更新 + }); + // 主动触发布局尺寸变化 + this.onLayoutResize(); + // 监听布局尺寸变化 + resizeObserver = new ResizeObserver(entries => { + this.onLayoutResize(); + }); + resizeObserver.observe(layout); + }, /** - * 重置 + * 生命周期:卸载前 */ - reset() { - // 资源信息 - this.assets = null; - // 选项 - this.viewScale = 1; - this.skin = ''; - this.animation = ''; - this.timeScale = 1; - this.loop = true; - this.premultipliedAlpha = false; - this.drawBones = false; - this.drawBoundingBoxes = false; - this.drawMeshTriangles = false; - this.drawPaths = false; - // 当前运行时版本 - this.version = 'unknown'; - // 恢复默认画布颜色 - this.canvasColor = '#4c4c4c'; - // 骨骼数据 - skeleton = null; - bounds = null; - this.skeletonData = null; - this.animationState = null; - // 清空画布 - gl && gl.clear(gl.COLOR_BUFFER_BIT); - // 环境 + beforeUnmount() { + // 清理案发现场 + canvas = null; + layout = null; + properties = null; + gl = null; shader = null; batcher = null; mvp = null; skeletonRenderer = null; - this.assetManager = null; - // 调试 debugRenderer = null; debugShader = null; shapeRenderer = null; - // 上一帧时间 - lastFrameTime = null; - // 拖动 - isDragging = false; - clickOffset = [0, 0]; - this.dragOffset = [0, 0]; - }, - - /** - * 翻译 - * @param {string} key - */ - t(key) { - return translate(key); - }, - - /** - * 资源信息按钮点击回调 - */ - onInfoBtnClick() { - if (!this.assets || !this.assets.dir) { - return; - } - const { dir, json, skel } = this.assets, - spinePath = Path.join(dir, (json || skel)); - // 在资源管理器中展示 spine 文件 - shell.showItemInFolder(spinePath) - }, - - /** - * 选择资源按钮点击回调 - */ - onSelectBtnClick() { - // (主进程)选择资源 - RendererEvent.send('select'); - }, - - /** - * 重置按钮点击回调 - */ - onResetBtnClick() { - this.reset(); - }, - - /** - * 复位按钮点击回调 - */ - onRepositionBtnClick() { - isDragging = false; - clickOffset = [0, 0]; - this.dragOffset = [0, 0]; - }, - - /** - * 获取 Spine 运行时 - */ - getRuntime() { - console.log('[methods]', 'getRuntime'); - // 资源对应的 Spine 运行时版本 - let version = this.getAssetSpineVersion(this.assets.json || this.assets.skel); - if (!version) { - // RendererUtil.print('warn', translate('noVersion')); - // return false; - console.warn('Unable to identify Spine version of asset!'); - // 默认使用 3.8 的 Runtime - version = "3.8"; - } - console.log('Skeleton spine version', version); - // 处理版本号(保留前两个分量) - version = version.split('.').slice(0, 2).map(v => parseInt(v)).join('.'); - // 获取目标版本的 Spine 运行时对象 - const spine = SpineRuntime.get(version); - if (!spine) { - const content = `${translate('noSpineRuntime')} | ${translate('version')}: ${version}`; - EditorRendererKit.print('warn', content); - return false; - } - window.spine = spine; - this.version = spine.version; - console.log('Spine runtime version', spine.version); - return true; - }, - - /** - * 获取资源对应的 Spine 运行时版本 - * @param {string} path 文件路径 - * @returns {string} - */ - getAssetSpineVersion(path) { - const fullPath = Path.join((this.assets.dir || ''), path); - if (!Fs.existsSync(fullPath)) { - return null; - } - const extname = Path.extname(path); - if (extname === '.json') { - const data = JSON.parse(Fs.readFileSync(fullPath, 'utf-8')); - if (data.skeleton) { - return data.skeleton.spine; - } - } else if (extname === '.skel') { - return '3.8'; - } - return null; - }, - - /** - * 初始化 Spine 运行时 - */ - initRuntime() { - console.log('[methods]', 'initRuntime'); - // 获取画布 - if (!canvas) { - canvas = this.$refs.canvas; - } - // WebGL - if (!gl) { - const config = { alpha: false }; - gl = canvas.getContext("webgl", config); - if (!gl) { - EditorRendererKit.print('warn', translate('noWebGL')); - return; - } - const color = this.clearColor; - gl.clearColor(color[0], color[1], color[2], 1); - } - - // Shader - shader = spine.webgl.Shader.newTwoColoredTextured(gl); - // 处理器 - batcher = new spine.webgl.PolygonBatcher(gl); - // MVP 变换矩阵 - mvp = new spine.webgl.Matrix4(); - mvp.ortho2d(0, 0, canvas.width - 1, canvas.height - 1); - // 骨骼渲染器 - skeletonRenderer = new spine.webgl.SkeletonRenderer(gl); - - // 用于调试的 debugRenderer、debugShader 和 shapeRenderer - debugRenderer = new spine.webgl.SkeletonDebugRenderer(gl); - debugShader = spine.webgl.Shader.newColored(gl); - shapeRenderer = new spine.webgl.ShapeRenderer(gl); - - // 资源管理器 - this.assetManager = new spine.webgl.AssetManager(gl); - }, - - /** - * 加载资源 - */ - loadAssets() { - console.log('[methods]', 'loadAssets'); - const assetManager = this.assetManager; - if (!assetManager) { - return; - } - const assets = this.assets; - // 指定资源目录前缀 - if (assets.dir) { - assetManager.pathPrefix = assets.dir; - } - // 骨骼数据 - if (assets.json) { - // JSON - assetManager.loadText(assets.json); - } else if (assets.skel) { - // skel(二进制) - assetManager.loadBinary(assets.skel); - } else { - EditorRendererKit.print('warn', translate('noSkeletonData')); - return; - } - // 图集和纹理 - if (assetManager.loadTextureAtlas) { - // spine runtime 3.6+ - // loadTextureAtlas 内部会自动加载纹理 - assetManager.loadTextureAtlas(assets.atlas); - } else { - // spine runtime 3.5 - assetManager.loadText(assets.atlas); - assetManager.loadTexture(assets.png); - } - // 是否开启纹理预乘 - if (Path.basename(assets.png).includes('pma') || - Path.basename(assets.atlas).includes('pma')) { - this.premultipliedAlpha = true; - } - // 等待加载 - requestAnimationFrame(this.loading); - }, - - /** - * 等待加载 - */ - loading() { - if (!this.assetManager) { - return; - } - // 文件是否已加载完成 - if (this.assetManager.isLoadingComplete()) { - // 加载骨骼数据 - const result = this.loadSkeleton(); - if (!result) { - this.reset(); - return; - } - // 设置皮肤 - if (this.skins && this.skins[0]) { - // this.skeletonData.defaultSkin.name - this.setSkin(this.skins[0]); - } - // 播放动画 - if (this.animations && this.animations[0]) { - this.playAnimation(this.animations[0]); - } - // 记录当前帧时间 - lastFrameTime = Date.now() / 1000; - // 下一帧开始渲染 - requestAnimationFrame(this.render); - } else { - // 继续等待加载 - requestAnimationFrame(this.loading); - } - }, - - /** - * 加载骨骼数据 - */ - loadSkeleton() { - console.log('[methods]', 'loadSkeleton'); - const assetManager = this.assetManager, - assets = this.assets; - - // 图集数据 - let atlas = assetManager.get(assets.atlas); - if (spine.version === '3.5') { - atlas = new spine.TextureAtlas(atlas); - } - // 创建 AtlasAttachmentLoader 对象用于处理部位、网格、包围盒和路径 - const atlasLoader = new spine.AtlasAttachmentLoader(atlas); - - try { - // 骨骼数据 - if (assets.json) { - // 创建 skeletonJson 对象用于解析 json 文件 - const skeletonJson = new spine.SkeletonJson(atlasLoader); - this.skeletonData = skeletonJson.readSkeletonData(assetManager.get(assets.json)); - } else if (assets.skel) { - // 创建 SkeletonBinary 对象用于解析 skel 文件 - const skeletonBinary = new spine.SkeletonBinary(atlasLoader); - this.skeletonData = skeletonBinary.readSkeletonData(assetManager.get(assets.skel)); - } - } catch (error) { - console.error(error); - EditorRendererKit.print('warn', translate('dataMismatch')); - return false; - } - - // 创建骨骼对象 - skeleton = new spine.Skeleton(this.skeletonData); - - // 计算边界 - bounds = this.calculateBounds(); - - // 创建 AnimationState 对象用于动画控制 - const animationStateData = new spine.AnimationStateData(skeleton.data); - this.animationState = new spine.AnimationState(animationStateData); - - // Done - return true; - }, - - /** - * 设置皮肤 - * @param {string} name - */ - setSkin(name) { - if (!skeleton) { - return; - } - this.skin = name; - // 设置皮肤 - skeleton.setSkinByName(name); - // 重置姿势 - skeleton.setSlotsToSetupPose(); - }, - - /** - * 播放动画 - * @param {string} name - */ - playAnimation(name) { - if (!skeleton) { - return; - } - this.animation = name; - // 重置姿势 - skeleton.setToSetupPose(); - // 播放动画 - this.animationState.setAnimation(0, name, this.loop); - }, - - /** - * 设置时间缩放 - * @param {number} value - */ - setTimeScale(value) { - if (!skeleton) { - return; - } - this.animationState.timeScale = value; - }, - - /** - * 计算边界 - * @returns {{ offset: { x: number, y: number }, size: { x: number, y: number } }} - */ - calculateBounds() { - skeleton.setToSetupPose(); - skeleton.updateWorldTransform(); - const offset = new spine.Vector2(), - size = new spine.Vector2(); - skeleton.getBounds(offset, size, []); - return { offset, size }; - }, - - /** - * 渲染骨骼 - */ - render() { - if (!skeleton) { - return; - } - // 计算帧时间差 - const now = Date.now() / 1000, - delta = now - lastFrameTime; - // 记录当前帧时间 - lastFrameTime = now; - - // 更新 mvp 来适配画布尺寸 - this.resizeView(); - - // 清空画布 - gl.clear(gl.COLOR_BUFFER_BIT); - - // 应用动画并根据时间差值更新动画时间 - this.animationState.update(delta); - this.animationState.apply(skeleton); - // 更新骨骼 Transform - skeleton.updateWorldTransform(); - - // 渲染 - // 绑定 shader - shader.bind(); - // 传递属性 - shader.setUniformi(spine.webgl.Shader.SAMPLER, 0); - shader.setUniform4x4f(spine.webgl.Shader.MVP_MATRIX, mvp.values); - // 渲染骨骼 - batcher.begin(shader); - // 设置 skeletonRenderer 属性 - skeletonRenderer.premultipliedAlpha = this.premultipliedAlpha; - // 渲染 - skeletonRenderer.draw(batcher, skeleton); - batcher.end(); - // 解除 shader 绑定 - shader.unbind(); - - // 调试 - if (this.debug) { - // 绑定 shader - debugShader.bind(); - // 传递属性 - debugShader.setUniform4x4f(spine.webgl.Shader.MVP_MATRIX, mvp.values); - // 设置 debugRenderer 属性 - debugRenderer.premultipliedAlpha = this.premultipliedAlpha; - debugRenderer.drawBones = this.drawBones; - debugRenderer.drawBoundingBoxes = this.drawBoundingBoxes; - debugRenderer.drawRegionAttachments = this.drawBoundingBoxes; - debugRenderer.drawMeshHull = this.drawMeshTriangles; - debugRenderer.drawMeshTriangles = this.drawMeshTriangles; - debugRenderer.drawPaths = this.drawPaths; - debugRenderer.drawSkeletonXY = this.drawBones; - // 开始渲染 - shapeRenderer.begin(debugShader); - // 渲染 - debugRenderer.draw(shapeRenderer, skeleton); - shapeRenderer.end(); - // 解除 shader 绑定 - debugShader.unbind(); - } - - // 持续渲染 - requestAnimationFrame(this.render); - }, - - /** - * 更新视口尺寸 - */ - resizeView() { - // 更新画布尺寸 - const { clientWidth, clientHeight } = canvas; - if (canvas.width !== clientWidth || canvas.height !== clientHeight) { - canvas.width = clientWidth; - canvas.height = clientHeight; - } - // 骨骼位置以及缩放 - const canvasWidth = canvas.width, - canvasHeight = canvas.height; - // 计算中心点 - const centerX = (bounds.offset.x + (bounds.size.x / 2)) || 0, - centerY = (bounds.offset.y + (bounds.size.y / 2)) || 0; - // 计算缩放比例 - const ratioX = bounds.size.x / canvasWidth, - ratioY = bounds.size.y / canvasHeight; - let scale = Math.max(ratioX, ratioY) * 1.2; - if (scale < 1) scale = 1; - // 自定义缩放 - scale /= this.viewScale; - // 最终宽高 - const width = canvasWidth * scale, - height = canvasHeight * scale; - // 更新矩阵 - const x = (centerX - (width / 2)) - (this.dragOffset[0] * scale), - y = (centerY - (height / 2)) + (this.dragOffset[1] * scale); - mvp.ortho2d(x, y, width, height); - // 更新视口 - gl.viewport(0, 0, canvasWidth, canvasHeight); - }, - - /** - * (主进程)资源旋转回调 - * @param {Electron.ipcRendererEvent} event - * @param {{ dir?: string, json?: string, skel?: string, atlas: string, png: string }} assets 资源 - */ - onAssetsSelectedEvent(event, assets) { - console.log('[methods]', 'onAssetsSelectedEvent', assets); - // 重置 - if (this.assets) { - this.reset(); - } - // 未选中资源 - if (!assets) return; - // 储存 - this.assets = assets; - // 处理路径 - this.processAssetPaths(); - // 获取运行时 - const result = this.getRuntime(); - if (!result) return; - // 初始化运行时 - this.initRuntime(); - // 开始加载资源 - this.loadAssets(); - }, - - /** - * 处理资源路径 - */ - processAssetPaths() { - // ⚠️ Spine Runtime 在 Windows 平台下的问题 - // 使用 loadTextureAtlas 加载图集时会自动加载纹理 - // 但是 loadTextureAtlas 内部调用 loadTexture 时传递的 path 是文件名而不是完整路径 - // 如果没有指定 pathPrefix 属性,loadTexture 就会无法正常加载 - // 所以干脆都改为需要指定 pathPrefix 属性 - const assets = this.assets, - { dir, json, skel, png, atlas } = assets; - if (!dir) { - assets.dir = Path.dirname(json || skel); - } - if (!assets.dir.endsWith(Path.sep)) { - assets.dir += Path.sep; - } - if (json) { - assets.json = Path.basename(json); - } else if (skel) { - assets.skel = Path.basename(skel); - } - assets.atlas = Path.basename(atlas); - assets.png = Path.basename(png); - console.log('[methods]', 'processAssetPaths', this.assets); - }, - - /** - * 布局尺寸变化回调 - */ - onLayoutResize() { - console.log('[methods]', 'onLayoutResize'); - const layoutStyle = layout.style, - propertiesStyle = this.$refs.properties.style; - if (layout.clientWidth >= 800 || layout.clientHeight < 330) { - if (layout.clientWidth >= 350) { - // 水平布局 - layoutStyle.flexDirection = 'row'; - propertiesStyle.width = '265px'; - propertiesStyle.marginTop = '0'; - propertiesStyle.marginLeft = '5px'; - propertiesStyle.display = 'flex'; - } else { - // 隐藏选项 - propertiesStyle.display = 'none'; - } - } else { - // 垂直布局 - layoutStyle.flexDirection = 'column'; - propertiesStyle.width = '100%'; - propertiesStyle.marginTop = '5px'; - propertiesStyle.marginLeft = '0'; - propertiesStyle.display = 'flex'; - } - }, - - /** - * 画布鼠标滚轮事件回调 - * @param {WheelEvent} event - */ - onCanvasMouseWheel(event) { - if (!this.assets) { - return; - } - // 当前缩放 - let scale = this.viewScale; - // 缩放步长 - const step = Math.abs(scale) >= 1 ? 0.1 : 0.05; - // 方向 - if (event.wheelDelta > 0) { - // 向上(放大) - scale += step; - } else { - // 向下(缩小) - scale -= step; - } - // 处理精度 - scale = Math.round(scale * 100) / 100; - // 设置缩放 - this.viewScale = scale; - }, - - /** - * 画布鼠标点击事件回调 - * @param {MouseEvent} event - */ - onCanvasMouseDown(event) { - if (!this.assets) { - return; - } - isDragging = true; - const x = event.offsetX - this.dragOffset[0], - y = event.offsetY - this.dragOffset[1]; - clickOffset = [x, y]; - }, - - /** - * 画布鼠标移动事件回调 - * @param {MouseEvent} event - */ - onCanvasMouseMove(event) { - if (!isDragging) { - return; - } - const x = event.offsetX - clickOffset[0], - y = event.offsetY - clickOffset[1]; - this.dragOffset = [x, y]; - }, - - /** - * 画布鼠标松开事件回调 - * @param {MouseEvent} event - */ - onCanvasMouseUp(event) { - isDragging = false; - clickOffset = [0, 0]; - }, - - /** - * 画布鼠标离开事件回调 - * @param {MouseEvent} event - */ - onCanvasMouseLeave(event) { - isDragging = false; - clickOffset = [0, 0]; - }, - - }, - - /** - * 生命周期:挂载后 - */ - mounted() { - console.log('mounted', this); - // 收集元素 - canvas = this.$refs.canvas; - layout = this.$refs.layout; - // 监听画布事件 - canvas.addEventListener('mousewheel', this.onCanvasMouseWheel); // 监听画布鼠标滚轮 - canvas.addEventListener('mousedown', this.onCanvasMouseDown); // 监听画布鼠标点击 - canvas.addEventListener('mousemove', this.onCanvasMouseMove); // 监听画布鼠标移动 - canvas.addEventListener('mouseup', this.onCanvasMouseUp); // 监听画布鼠标松开 - canvas.addEventListener('mouseleave', this.onCanvasMouseLeave); // 监听画布鼠标离开 - // 监听(主进程)资源选择事件 - RendererEvent.on('assets-selected', this.onAssetsSelectedEvent); - // (下一帧)发送事件给主进程 - this.$nextTick(() => { - RendererEvent.send('ready'); // (主进程)已就绪 - RendererEvent.send('check-update', false); // (主进程)检查更新 - }); - // 主动触发布局尺寸变化 - this.onLayoutResize(); - // 监听布局尺寸变化 - setTimeout(() => { - if (window.ResizeObserver) { - resizeObserver = new ResizeObserver(entries => { - this.onLayoutResize(); - }); - resizeObserver.observe(layout); - } else { - let lastWidth = layout.clientWidth, - lastHeight = layout.clientHeight; - resizeHandler = setInterval(() => { - if (layout.clientWidth !== lastWidth || - layout.clientHeight !== lastHeight) { - this.onLayoutResize(); - lastWidth = layout.clientWidth; - lastHeight = layout.clientHeight; - } - }, 500); - } - }, 500); - }, - - /** - * 生命周期:卸载前 - */ - beforeUnmount() { - // 取消事件监听 - RendererEvent.removeAllListeners('assets-selected'); - // 取消监听布局尺寸变化 - if (resizeObserver) { + skeleton = null; + bounds = null; + // 取消监听布局尺寸变化 resizeObserver.disconnect(); resizeObserver = null; - } else { - clearInterval(resizeHandler); - resizeHandler = null; - } - }, + // 取消事件监听 + RendererEvent.removeAllListeners('assets-selected'); + // 发送事件给主进程 + RendererEvent.send('close'); + }, -}; + }; + return App; -module.exports = App; +}()); diff --git a/typings/cocos/editor.d.ts b/typings/cocos/editor.d.ts deleted file mode 100644 index 93278df..0000000 --- a/typings/cocos/editor.d.ts +++ /dev/null @@ -1,1053 +0,0 @@ -/** - * Cocos Creator 编辑器模块 - * @author 陈皮皮(ifaswind) - * @version 20210312 - * @see https://gitee.com/ifaswind/eazax-ccc/blob/master/declarations/editor.d.ts - */ -declare module Editor { - - /** - * Log the normal message and show on the console. The method will send ipc message editor:console-log to all windows. - * @param args Whatever arguments the message needs - */ - function log(...args: any): void; - - /** - * Log the normal message and show on the console. The method will send ipc message editor:console-log to all windows. - * @param args Whatever arguments the message needs - */ - function info(...args: any): void; - - /** - * Log the warnning message and show on the console, it also shows the call stack start from the function call it. The method will send ipc message editor:console-warn to all windows. - * @param args Whatever arguments the message needs - */ - function warn(...args: any): void; - - /** - * Log the error message and show on the console, it also shows the call stack start from the function call it. The method will sends ipc message editor:console-error to all windows. - * @param args Whatever arguments the message needs - */ - function error(...args: any): void; - - /** - * Log the success message and show on the console The method will send ipc message editor:console-success to all windows. - * @param args Whatever arguments the message needs - */ - function success(...args: any): void; - - /** - * Require the module by Editor.url. This is good for module exists in package, since the absolute path of package may be variant in different machine. - * @param url - */ - function require(url: string): any; - - /** - * Returns the file path (if it is registered in custom protocol) or url (if it is a known public protocol). - * @param url - * @param encode - */ - function url(url: string, encode?: string): string; - - function T(key: string): string; - -} - -declare module Editor { - readonly let appPath: string; - readonly let frameworkPath: string; - readonly let importPath: string; - readonly let isWin32: boolean; - readonly let isDarwin: boolean; - readonly let lang: string; - readonly let libraryPath: string; - readonly let sceneScripts: { [packageName: string]: string }; -} - -declare module Editor { - - /** - * 渲染进程 - */ - module RendererProcess { - - /** - * AssetDB singleton class in renderer process, you can access the instance with `Editor.assetdb`. - */ - class AssetDB { - - /** - * The remote AssetDB instance of main process, same as `Editor.remote.assetdb`. - */ - readonly remote: Remote; - - /** - * The library path. - */ - readonly library: string; - - /** - * Reveal given url in native file system. - * @param url - */ - explore(url: string): string; - - /** - * Reveal given url's library file in native file system. - * @param url - */ - exploreLib(url: string): string; - - /** - * Get native file path by url. - * @param url - * @param cb The callback function. - */ - queryPathByUrl(url: string, cb?: (err: any, path: any) => void): void; - - /** - * Get uuid by url. - * @param url - * @param cb The callback function. - */ - queryUuidByUrl(url: string, cb?: (err: any, uuid: any) => void): void; - - /** - * Get native file path by uuid. - * @param uuid - * @param cb The callback function. - */ - queryPathByUuid(uuid: string, cb?: (err: any, path: any) => void): void; - - /** - * Get asset url by uuid. - * @param uuid - * @param cb The callback function. - */ - queryUrlByUuid(uuid: string, cb?: (err: any, url: any) => void): void; - - /** - * Get asset info by uuid. - * @param uuid - * @param cb The callback function. - */ - queryInfoByUuid(uuid: string, cb?: (err: any, info: any) => void): void; - - /** - * Get meta info by uuid. - * @param uuid - * @param cb The callback function. - */ - queryMetaInfoByUuid(uuid: string, cb?: (err: any, info: any) => void): void; - - /** - * Query all assets from asset-db. - * @param cb The callback function. - */ - deepQuery(cb?: (err: any, results: any[]) => void): void; - - /** - * Query assets by url pattern and asset-type. - * @param pattern The url pattern. - * @param assetTypes The asset type(s). - * @param cb The callback function. - */ - queryAssets(pattern: string, assetTypes: string | string[], cb?: (err: any, results: any[]) => void): void; - - /** - * Import files outside asset-db to specific url folder. - * @param rawfiles Rawfile path list. - * @param destUrl The url of dest folder. - * @param showProgress Show progress or not. - * @param cb The callbak function. - */ - import(rawfiles: string[], destUrl: string, showProgress?: boolean, cb?: (err: any, result: any) => void): void; - - /** - * Create asset in specific url by sending string data to it. - * @param uuid - * @param metaJson - * @param cb the callback function. - */ - create(url: string, data: string, cb?: (err: any, result: any) => void): void; - - /** - * Move asset from src to dest. - * @param srcUrl - * @param destUrl - * @param showMessageBox - */ - move(srcUrl: string, destUrl: string, showMessageBox?: boolean): void; - - /** - * Delete assets by url list. - * @param urls - */ - delete(urls: string[]): void; - - /** - * Save specific asset by sending string data. - * @param url - * @param data - * @param cb the callback function. - */ - saveExists(url: string, data: string, cb?: (err: any, result: any) => void): void; - - /** - * Create or save assets by sending string data. If the url is already existed, it will be changed with new data. The behavior is same with method saveExists. Otherwise, a new asset will be created. The behavior is same with method create. - * @param url - * @param data - * @param cb the callback function. - */ - createOrSave(url: string, data: string, cb?: (err: any, result: any) => void): void; - - /** - * Save specific meta by sending meta's json string. - * @param uuid - * @param metaJson - * @param cb the callback function. - */ - saveMeta(uuid: string, metaJson: string, cb?: (err: any, result: any) => void): void; - - /** - * Refresh the assets in url, and return the results. - * @param url - * @param cb - */ - refresh(url: string, cb?: (err: any, results: any[]) => void): void; - - } - - } - - /** - * 主进程 - */ - module MainProcess { - - /** - * AssetDB singleton class in main process, you can access the instance with `Editor.assetdb`. - */ - class AssetDB { - - /** - * Return uuid by url. If uuid not found, it will return null. - * @param url - */ - urlToUuid(url: string): string; - - /** - * Return uuid by file path. If uuid not found, it will return null. - * @param fspath - */ - fspathToUuid(fspath: string): string; - - /** - * Return file path by uuid. If file path not found, it will return null. - * @param url - */ - uuidToFspath(url: string): string; - - /** - * Return url by uuid. If url not found, it will return null. - * @param uuid - */ - uuidToUrl(uuid: string): string; - - /** - * Return url by file path. If file path not found, it will return null. - * @param fspath - */ - fspathToUrl(fspath: string): string; - - /** - * Return file path by url. If url not found, it will return null. - * @param url - */ - urlToFspath(url: string): string; - - /** - * Check existance by url. - * @param url - */ - exists(url: string): string; - - /** - * Check existance by uuid. - * @param uuid - */ - existsByUuid(uuid: string): string; - - /** - * Check existance by path. - * @param fspath - */ - existsByPath(fspath: string): string; - - /** - * Check whether asset for a given url is a sub asset. - * @param url - */ - isSubAsset(url: string): boolean; - - /** - * Check whether asset for a given uuid is a sub asset. - * @param uuid - */ - isSubAssetByUuid(uuid: string): boolean; - - /** - * Check whether asset for a given path is a sub asset. - * @param fspath - */ - isSubAssetByPath(fspath: string): boolean; - - /** - * Check whether asset contains sub assets for a given url. - * @param url - */ - containsSubAssets(url: string): boolean; - - /** - * Check whether asset contains sub assets for a given uuid. - * @param uuid - */ - containsSubAssetsByUuid(uuid: string): boolean; - - /** - * Check whether asset contains sub assets for a given path. - * @param fspath - */ - containsSubAssetsByPath(fspath: string): boolean; - - /** - * Return asset info by a given url. - * @param url - */ - assetInfo(url: string): AssetInfo; - - /** - * Return asset info by a given uuid. - * @param uuid - */ - assetInfoByUuid(uuid: string): AssetInfo; - - /** - * Return asset info by a given file path. - * @param fspath - */ - assetInfoByPath(fspath: string): AssetInfo; - - /** - * Return all sub assets info by url if the url contains sub assets. - * @param url - */ - subAssetInfos(url: string): AssetInfo[]; - - /** - * Return all sub assets info by uuid if the uuid contains sub assets. - * @param uuid - */ - subAssetInfosByUuid(uuid: string): AssetInfo[]; - - /** - * Return all sub assets info by path if the path contains sub assets. - * @param fspath - */ - subAssetInfosByPath(fspath: string): AssetInfo[]; - - /** - * Return meta instance by a given url. - * @param url - */ - loadMeta(url: string): MetaBase; - - /** - * Return meta instance by a given uuid. - * @param uuid - */ - loadMetaByUuid(uuid: string): MetaBase; - - /** - * Return meta instance by a given path. - * @param fspath - */ - loadMetaByPath(fspath: string): MetaBase; - - /** - * Return whether a given url is reference to a mount. - * @param url - */ - isMount(url: string): boolean; - - /** - * Return whether a given path is reference to a mount. - * @param fspath - */ - isMountByPath(fspath: string): boolean; - - /** - * Return whether a given uuid is reference to a mount. - * @param uuid - */ - isMountByUuid(uuid: string): boolean; - - /** - * Return mount info by url. - * @param url - */ - mountInfo(url: string): MountInfo; - - /** - * Return mount info by uuid. - * @param uuid - */ - mountInfoByUuid(uuid: string): MountInfo; - - /** - * Return mount info by path. - * @param fspath - */ - mountInfoByPath(fspath: string): MountInfo; - - /** - * Mount a directory to assetdb, and give it a name. If you don't provide a name, it will mount to root. - * @param path file system path. - * @param mountPath the mount path (relative path). - * @param opts options. - * @param opts.hide if the mount hide in assets browser. - * @param opts.virtual if this is a virtual mount point. - * @param opts.icon icon for the mount. - * @param cb a callback function. - * @example Editor.assetdb.mount('path/to/mount', 'assets', function (err) { - // mounted, do something ... - }); - */ - mount(path: string, mountPath: string, opts: { hide: object, vitural: object, icon: object }, cb?: (err: any) => void): void; - - /** - * Attach the specified mount path. - * @param mountPath the mount path (relative path). - * @param cb a callback function. - * @example Editor.assetdb.attachMountPath('assets', function (err, results) { - // mount path attached, do something ... - // results are the assets created - }); - */ - attachMountPath(mountPath: string, cb?: (err: any, results: any[]) => void): void; - - /** - * Unattach the specified mount path. - * @param mountPath the mount path (relative path). - * @param cb a callback function. - * @example Editor.assetdb.unattachMountPath('assets', function (err, results) { - // mount path unattached, do something ... - // results are the assets deleted - }); - */ - unattachMountPath(mountPath: string, cb?: (err: any, results: any[]) => void): void; - - /** - * Unmount by name. - * @param mountPath the mount path. - * @param cb a callback function. - * @example Editor.assetdb.unmount('assets', function (err) { - // unmounted, do something ... - }); - */ - unmount(mountPath: string, cb?: (err: any) => void): void; - - /** - * Init assetdb, it will scan the mounted directories, and import unimported assets. - * @param cb a callback function. - * @example Editor.assetdb.init(function (err, results) { - // assets that imported during init - results.forEach(function (result) { - // result.uuid - // result.parentUuid - // result.url - // result.path - // result.type - }); - }); - */ - init(cb?: (err: any, results: any[]) => void): void; - - /** - * Refresh the assets in url, and return the results. - * @param url - * @param cb - */ - refresh(url: string, cb?: Function): void; - - /** - * deepQuery - * @param cb - * @example Editor.assetdb.deepQuery(function (err, results) { - results.forEach(function (result) { - // result.name - // result.extname - // result.uuid - // result.type - // result.isSubAsset - // result.children - the array of children result - }); - }); - */ - deepQuery(cb?: Function): void; - - /** - * queryAssets - * @param pattern The url pattern. - * @param assetTypes The asset type(s). - * @param cb The callback function. - */ - queryAssets(pattern: string, assetTypes: string | string[], cb?: (err: Error, results: any[]) => void): void; - - /** - * queryMetas - * @param pattern The url pattern. - * @param type The asset type. - * @param cb The callback function. - */ - queryMetas(pattern: string, type: string, cb?: (err: Error, results: any[]) => void): void; - - /** - * move - * @param srcUrl The url pattern. - * @param destUrl The asset type. - * @param cb The callback function. - */ - move(srcUrl: string, destUrl: string, cb?: (err: Error, results: any[]) => void): void; - - /** - * delete - * @param urls - * @param cb - */ - delete(urls: string[], cb?: (err: Error, results: any[]) => void): void; - - /** - * Create asset at url with data. - * @param url - * @param data - * @param cb - */ - create(url: string, data: string, cb?: (err: Error, results: any[]) => void): void; - - /** - * Save data to the exists asset at url. - * @param url - * @param data - * @param cb - */ - saveExists(url: string, data: string, cb?: (err: Error, meta: any) => void): void; - - /** - * Import raw files to url - * @param rawfiles - * @param url - * @param cb - */ - import(rawfiles: string[], url: string, cb?: (err: Error, results: any[]) => void): void; - - /** - * Overwrite the meta by loading it through uuid. - * @param uuid - * @param jsonString - * @param cb - */ - saveMeta(uuid: string, jsonString: string, cb?: (err: Error, meta: any) => void): void; - - /** - * Exchange uuid for two assets. - * @param urlA - * @param urlB - * @param cb - */ - exchangeUuid(urlA: string, urlB: string, cb?: (err: Error, results: any[]) => void): void; - - /** - * Clear imports. - * @param url - * @param cb - */ - clearImports(url: string, cb?: (err: Error, results: any[]) => void): void; - - /** - * Register meta type. - * @param extname - * @param folder Whether it's a folder type. - * @param metaCtor - */ - register(extname: string, folder: boolean, metaCtor: object): void; - - /** - * Unregister meta type. - * @param metaCtor - */ - unregister(metaCtor: object): void; - - /** - * Get the relative path from mount path to the asset by fspath. - * @param fspath - */ - getRelativePath(fspath: string): string; - - /** - * Get the backup file path of asset file. - * @param filePath - */ - getAssetBackupPath(filePath: string): string; - - } - - } - - interface MetaBase { - ver: string; - uuid: string; - } - - interface MountInfo { - path: string; - name: string; - type: string; - } - - interface Metas { - asset: string[]; - folder: string[]; - mount: string[]; - 'custom-asset': string[]; - 'native-asset': string[]; - 'animation-clip': string[]; - 'audio-clip': string[]; - 'bitmap-font': string[]; - } - - interface App { - readonly home: string; - readonly name: string; - readonly path: string; - readonly version: string; - } - - class Remote { - readonly App: App; - readonly isClosing: boolean; - readonly lang: string; - readonly isNode: boolean; - readonly isElectron: boolean; - readonly isNative: boolean; - readonly isPureWeb: boolean; - readonly isRendererProcess: boolean; - readonly isMainProcess: boolean; - readonly isDarwin: boolean; - readonly isWin32: boolean; - readonly isRetina: boolean; - readonly frameworkPath: string; - readonly dev: boolean; - readonly logfile: string; - readonly themePaths: string[]; - readonly theme: string; - readonly showInternalMount: boolean; - readonly metas: Metas; - readonly metaBackupPath: string; - readonly assetBackupPath: string; - readonly libraryPath: string; - readonly importPath: string; - readonly externalMounts: any; - readonly mountsWritable: string; - readonly assetdb: MainProcess.AssetDB; - readonly assetdbInited: boolean; - readonly sceneList: string[]; - readonly versions: { - 'asset-db': string; - CocosCreator: string; - cocos2d: string; - 'editor-framework': string; - } - } - - /** Remote 实例 */ - const remote: Remote; - - /** AssetDB 实例 */ - const assetdb: MainProcess.AssetDB; - -} - -interface AssetInfo { - uuid?: string; - path?: string; - url?: string; - type?: string; - isSubAsset?: boolean; - assetType?: string; - id?: string; - name?: string; - subAssetTypes?: string; -} - -declare module Editor.Project { - readonly let id: string; - readonly let name: string; - /** Absolute path for current open project. */ - readonly let path: string; -} - -declare module Editor.Builder { - - /** - * - * @param eventName The name of the event - * @param callback The event callback - */ - function on(eventName: string, callback: (options: BuildOptions, cb: Function) => void): void; - - /** - * - * @param eventName The name of the event - * @param callback The event callback - */ - function once(eventName: string, callback: (options: BuildOptions, cb: Function) => void): void; - - /** - * - * @param eventName The name of the event - * @param callback The event callback - */ - function removeListener(eventName: string, callback: Function): void; - -} - -declare module Editor.Scene { - - /** - * - * @param packageName - * @param method - * @param cb - */ - function callSceneScript(packageName: string, method: string, cb: (err: Error, msg: any) => void): void; - -} - -declare module Editor.Panel { - - /** - * Open a panel via panelID. - * @param panelID The panel ID - * @param argv - */ - function open(panelID: string, argv?: object): void; - - /** - * Close a panel via panelID. - * @param panelID The panel ID - */ - function close(panelID: string): void; - - /** - * Find panel frame via panelID. - * @param panelID The panel ID - */ - function find(panelID: string): void; - - /** - * Extends a panel. - * @param proto - */ - function extend(proto: object): void; - -} - -declare module Editor.Selection { - - /** - * Select item with its id. - * @param type - * @param id - * @param unselectOthers - * @param confirm - */ - function select(type: string, id: string, unselectOthers?: boolean, confirm?: boolean): void; - - /** - * Unselect item with its id. - * @param type - * @param id - * @param confirm - */ - function unselect(type: string, id: string, confirm?: boolean): void; - - /** - * Hover item with its id. If id is null, it means hover out. - * @param type - * @param id - */ - function hover(type: string, id: string): string; - - /** - * - * @param type - */ - function clear(type: string): void; - - /** - * - * @param type - */ - function curActivate(type: string): string[]; - - /** - * - * @param type - */ - function curGlobalActivate(type: string): string[]; - - /** - * - * @param type - */ - function curSelection(type: string): string[]; - - /** - * - * @param items - * @param mode 'top-level', 'deep' and 'name' - * @param func - */ - function filter(items: string[], mode: string, func: Function): string[]; - -} - -declare module Editor.Ipc { - - /** - * Send message with ...args to main process asynchronously. It is possible to add a callback as the last or the 2nd last argument to receive replies from the IPC receiver. - * @param message Ipc message. - * @param args Whatever arguments the message needs. - * @param callback You can specify a callback function to receive IPC reply at the last or the 2nd last argument. - * @param timeout You can specify a timeout for the callback at the last argument. If no timeout specified, it will be 5000ms. - */ - function sendToMain(message: string, ...args?: any, callback?: Function, timeout?: number): void; - - /** - * Send message with ...args to panel defined in renderer process asynchronously. It is possible to add a callback as the last or the 2nd last argument to receive replies from the IPC receiver. - * @param panelID Panel ID. - * @param message Ipc message. - * @param args Whatever arguments the message needs. - * @param callback You can specify a callback function to receive IPC reply at the last or the 2nd last argument. - * @param timeout You can specify a timeout for the callback at the last argument. If no timeout specified, it will be 5000ms. - */ - function sendToPanel(panelID: string, message: string, ...args?: any, callback?: Function, timeout?: number): void; - - /** - * Send message with ...args to all opened window and to main process asynchronously. - * @param message Ipc message. - * @param args Whatever arguments the message needs. - * @param option You can indicate the last argument as an IPC option by Editor.Ipc.option({...}). - */ - function sendToAll(message: string, ...args?: any, option?: object): void; - - /** - * Send message with ...args to main process synchronized and return a result which is responded from main process. - * @param message Ipc message. - * @param args Whatever arguments the message needs. - */ - function sendToMainSync(message: string, ...args?: any): void; - - /** - * Send message with ...args to main process by package name and the short name of the message. - * @param pkgName Package name. - * @param message Ipc message. - * @param args Whatever arguments the message needs. - */ - function sendToPackage(pkgName: string, message: string, ...args?: any): void; - -} - -declare module Editor.UI { - - module Setting { - - /** - * Control the default step for float point input element. Default is 0.1. - * @param value - */ - function stepFloat(value: number): void; - - /** - * Control the default step for integer input element. Default is 1. - * @param value - */ - function stepInt(value: number): void; - - /** - * Control the step when shift key press down. Default is 10. - * @param value - */ - function shiftStep(value: number): void; - - } - - module DragDrop { - - readonly let dragging: boolean; - - function start(e: any, t: any): void; - - function end(): void; - - function updateDropEffect(e: any, t: any); - - function type(e: any); - - function filterFiles(e: any); - - function items(dataTransfer: DataTransfer): AssetInfo[]; - - function getDragIcon(e: any); - - function options(e: any); - - function getLength(e: any): number; - - } - -} - -declare module Editor.GizmosUtils { - - function addMoveHandles(e, n, t); - - function getCenter(e); - - function getCenterWorldPos(n); - - function getCenterWorldPos3D(e); - - function getRecursiveNodes(e, t); - - function getRecursiveWorldBounds3D(e); - - function getWorldBounds3D(n); - - function snapPixel(e); - - function snapPixelWihVec2(e); - -} - -declare module Editor.Utils { - - /** - * Uuid 工具 - */ - module UuidUtils { - - /** - * 压缩后的 uuid 可以减小保存时的尺寸,但不能做为文件名(因为无法区分大小写并且包含非法字符)。 - * 默认将 uuid 的后面 27 位压缩成 18 位,前 5 位保留下来,方便调试。 - * 如果启用 min 则将 uuid 的后面 30 位压缩成 20 位,前 2 位保留不变。 - * @param uuid - * @param min - */ - function compressUuid(uuid: string, min?: boolean): string; - - function compressHex(hexString: string, reservedHeadLength?: number): string; - - function decompressUuid(str: string): string; - - function isUuid(str: string): boolean; - - function uuid(): string; - - } - -} - -declare interface BuildOptions { - actualPlatform: string; - android: { packageName: string }; - 'android-instant': { - REMOTE_SERVER_ROOT: string; - host: string; - packageName: string; - pathPattern: string; - recordPath: string; - scheme: string; - skipRecord: boolean; - } - apiLevel: string; - appABIs: string[]; - appBundle: boolean; - buildPath: string; - buildScriptsOnly: boolean; - debug: string; - dest: string; - embedWebDebugger: boolean; - encryptJs: boolean; - excludeScenes: string[]; - excludedModules: string[]; - 'fb-instant-games': object; - inlineSpriteFrames: boolean; - inlineSpriteFrames_native: boolean; - ios: { packageName: string }; - mac: { packageName: string }; - md5Cache: boolean; - mergeStartScene: boolean; - optimizeHotUpdate: boolean; - orientation: { - landscapeLeft: boolean; - landscapeRight: boolean; - portrait: boolean; - upsideDown: boolean; - }; - packageName: string; - platform: string; - previewHeight: number; - previewWidth: number; - scenes: string[]; - sourceMaps: boolean; - startScene: string; - template: string; - title: string; - useDebugKeystore: boolean; - vsVersion: string; - webOrientation: boolean; - win32: object; - xxteaKey: string; - zipCompressJs: string; - project: string; - projectName: string; - debugBuildWorker: boolean; - bundles: bundle[]; -} - -interface bundle { - /** bundle 的根目录 */ - root: string; - /** bundle 的输出目录 */ - dest: string; - /** 脚本的输出目录 */ - scriptDest: string; - /** bundle 的名称 */ - name: string; - /** bundle 的优先级 */ - priority: number; - /** bundle 中包含的场景 */ - scenes: string[]; - /** bundle 的压缩类型 */ - compressionType: 'subpackage' | 'normal' | 'none' | 'merge_all_json' | 'zip'; - /** bundle 所构建出来的所有资源 */ - buildResults: BuildResults; - /** bundle 的版本信息,由 config 生成 */ - version: string; - /** bundle 的 config.json 文件 */ - config: any; - /** bundle 是否是远程包 */ - isRemote: boolean; -}