feat: 完成项目全量汉化、文档同步及 Undo 系统稳定性修复
This commit is contained in:
@@ -101,6 +101,14 @@ startServer(port) {
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 开发历程与里程碑
|
||||
|
||||
### 2026-02-10: Undo 系统深度修复与规范化
|
||||
- **问题分析**: 修复了 `TypeError: Cannot read property '_name' of null`。该错误是由于直接修改节点属性(绕过 Undo 系统)与分组事务混用导致的。
|
||||
- **重构要点**: 将 `update-node-transform` 中所有的直接赋值替换为 `scene:set-property` IPC 调用,确保所有变换修改均受撤销系统监控。
|
||||
- **缺陷修正**: 修复了 `manage_undo` 在 `begin_group` 时传递错误参数导致 "Unknown object to record" 的问题。
|
||||
- **全量汉化与文档同步**: 完成了 `main.js` 和 `scene-script.js` 的 100% 简体中文翻译。同步更新了 `README.md`、`DEVELOPMENT.md` 及 `注意事项.md`。
|
||||
|
||||
### 3.2 MCP 工具注册
|
||||
|
||||
在 `/list-tools` 接口中注册工具:
|
||||
|
||||
@@ -317,12 +317,11 @@ Args: [你的项目所在盘符]:/[项目路径]/packages/mcp-bridge/mcp-proxy.j
|
||||
}
|
||||
```
|
||||
|
||||
### 23. manage_undo
|
||||
|
||||
- **描述**: 撤销/重做管理
|
||||
- **参数**:
|
||||
- `action`: 操作类型 (`undo`, `redo`, `begin_group`, `end_group`, `cancel_group`)
|
||||
- `description`: 撤销组描述 (用于 `begin_group`)
|
||||
- `id`: 节点 UUID (用于 `begin_group` 时的 `undo-record`,可选)
|
||||
|
||||
### 24. manage_vfx
|
||||
|
||||
|
||||
145
main.js
145
main.js
@@ -1040,9 +1040,9 @@ export default class NewScript extends cc.Component {
|
||||
// 否则后续立即挂载脚本的操作(manage_components)会因找不到脚本 UUID 而失败。
|
||||
Editor.assetdb.refresh(scriptPath, (refreshErr) => {
|
||||
if (refreshErr) {
|
||||
addLog("warn", `Refresh failed after script creation: ${refreshErr}`);
|
||||
addLog("warn", `脚本创建后刷新失败: ${refreshErr}`);
|
||||
}
|
||||
callback(null, `Script created at ${scriptPath}`);
|
||||
callback(null, `脚本已创建: ${scriptPath}`);
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -1076,7 +1076,7 @@ export default class NewScript extends cc.Component {
|
||||
// 使用 fs 写入 + refresh,确保覆盖成功
|
||||
const writeFsPath = Editor.assetdb.urlToFspath(scriptPath);
|
||||
if (!writeFsPath) {
|
||||
return callback(`Invalid path: ${scriptPath}`);
|
||||
return callback(`路径无效: ${scriptPath}`);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1133,7 +1133,7 @@ export default class NewScript extends cc.Component {
|
||||
switch (action) {
|
||||
case "create":
|
||||
if (Editor.assetdb.exists(path)) {
|
||||
return callback(`Asset already exists at ${path}`);
|
||||
return callback(`资源已存在: ${path}`);
|
||||
}
|
||||
// 确保父目录存在
|
||||
const fs = require("fs");
|
||||
@@ -1144,16 +1144,16 @@ export default class NewScript extends cc.Component {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
Editor.assetdb.create(path, content || "", (err) => {
|
||||
callback(err, err ? null : `Asset created at ${path}`);
|
||||
callback(err, err ? null : `资源已创建: ${path}`);
|
||||
});
|
||||
break;
|
||||
|
||||
case "delete":
|
||||
if (!Editor.assetdb.exists(path)) {
|
||||
return callback(`Asset not found at ${path}`);
|
||||
return callback(`找不到资源: ${path}`);
|
||||
}
|
||||
Editor.assetdb.delete([path], (err) => {
|
||||
callback(err, err ? null : `Asset deleted at ${path}`);
|
||||
callback(err, err ? null : `资源已删除: ${path}`);
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -1173,7 +1173,7 @@ export default class NewScript extends cc.Component {
|
||||
case "get_info":
|
||||
try {
|
||||
if (!Editor.assetdb.exists(path)) {
|
||||
return callback(`Asset not found: ${path}`);
|
||||
return callback(`找不到资源: ${path}`);
|
||||
}
|
||||
const uuid = Editor.assetdb.urlToUuid(path);
|
||||
const info = Editor.assetdb.assetInfoByUuid(uuid);
|
||||
@@ -1184,7 +1184,7 @@ export default class NewScript extends cc.Component {
|
||||
callback(null, { url: path, uuid: uuid, exists: true });
|
||||
}
|
||||
} catch (e) {
|
||||
callback(`Error getting asset info: ${e.message}`);
|
||||
callback(`获取资源信息失败: ${e.message}`);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1205,7 +1205,7 @@ export default class NewScript extends cc.Component {
|
||||
switch (action) {
|
||||
case "create":
|
||||
if (Editor.assetdb.exists(path)) {
|
||||
return callback(`Scene already exists at ${path}`);
|
||||
return callback(`场景已存在: ${path}`);
|
||||
}
|
||||
// 确保父目录存在
|
||||
const absolutePath = Editor.assetdb.urlToFspath(path);
|
||||
@@ -1214,35 +1214,35 @@ export default class NewScript extends cc.Component {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
Editor.assetdb.create(path, getNewSceneTemplate(), (err) => {
|
||||
callback(err, err ? null : `Scene created at ${path}`);
|
||||
callback(err, err ? null : `场景已创建: ${path}`);
|
||||
});
|
||||
break;
|
||||
|
||||
case "delete":
|
||||
if (!Editor.assetdb.exists(path)) {
|
||||
return callback(`Scene not found at ${path}`);
|
||||
return callback(`找不到场景: ${path}`);
|
||||
}
|
||||
Editor.assetdb.delete([path], (err) => {
|
||||
callback(err, err ? null : `Scene deleted at ${path}`);
|
||||
callback(err, err ? null : `场景已删除: ${path}`);
|
||||
});
|
||||
break;
|
||||
|
||||
case "duplicate":
|
||||
if (!Editor.assetdb.exists(path)) {
|
||||
return callback(`Scene not found at ${path}`);
|
||||
return callback(`找不到场景: ${path}`);
|
||||
}
|
||||
if (!targetPath) {
|
||||
return callback(`Target path is required for duplicate operation`);
|
||||
return callback("复制操作需要目标路径");
|
||||
}
|
||||
if (Editor.assetdb.exists(targetPath)) {
|
||||
return callback(`Target scene already exists at ${targetPath}`);
|
||||
return callback(`目标场景已存在: ${targetPath}`);
|
||||
}
|
||||
// 【修复】Cocos 2.4.x 主进程中 Editor.assetdb 没有 loadAny
|
||||
// 直接使用 fs 读取物理文件
|
||||
try {
|
||||
const sourceFsPath = Editor.assetdb.urlToFspath(path);
|
||||
if (!sourceFsPath || !fs.existsSync(sourceFsPath)) {
|
||||
return callback(`Failed to locate source scene file: ${path}`);
|
||||
return callback(`定位源场景文件失败: ${path}`);
|
||||
}
|
||||
const content = fs.readFileSync(sourceFsPath, "utf-8");
|
||||
|
||||
@@ -1257,7 +1257,7 @@ export default class NewScript extends cc.Component {
|
||||
if (err) return callback(err);
|
||||
// 【增加】关键刷新,确保数据库能查到新文件
|
||||
Editor.assetdb.refresh(targetPath, (refreshErr) => {
|
||||
callback(refreshErr, refreshErr ? null : `Scene duplicated from ${path} to ${targetPath}`);
|
||||
callback(refreshErr, refreshErr ? null : `场景已从 ${path} 复制到 ${targetPath}`);
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -1271,7 +1271,7 @@ export default class NewScript extends cc.Component {
|
||||
const info = Editor.assetdb.assetInfoByUuid(uuid);
|
||||
callback(null, info || { url: path, uuid: uuid, exists: true });
|
||||
} else {
|
||||
callback(`Scene not found: ${path}`);
|
||||
return callback(`找不到场景: ${path}`);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1288,10 +1288,10 @@ export default class NewScript extends cc.Component {
|
||||
switch (action) {
|
||||
case "create":
|
||||
if (!nodeId) {
|
||||
return callback(`Node ID is required for create operation`);
|
||||
return callback("创建预制体需要节点 ID");
|
||||
}
|
||||
if (Editor.assetdb.exists(prefabPath)) {
|
||||
return callback(`Prefab already exists at ${prefabPath}`);
|
||||
return callback(`预制体已存在: ${prefabPath}`);
|
||||
}
|
||||
// 确保父目录存在
|
||||
const absolutePath = Editor.assetdb.urlToFspath(prefabPath);
|
||||
@@ -1321,19 +1321,19 @@ export default class NewScript extends cc.Component {
|
||||
Editor.Ipc.sendToPanel("scene", "scene:create-prefab", [nodeId], targetDir);
|
||||
}, 100); // 稍微延迟以确保重命名生效
|
||||
|
||||
callback(null, `Command sent: Creating prefab from node ${nodeId} at ${targetDir} as ${prefabName}`);
|
||||
callback(null, `指令已发送: 从节点 ${nodeId} 在目录 ${targetDir} 创建名为 ${prefabName} 的预制体`);
|
||||
break;
|
||||
|
||||
case "update":
|
||||
if (!nodeId) {
|
||||
return callback(`Node ID is required for update operation`);
|
||||
return callback("更新预制体需要节点 ID");
|
||||
}
|
||||
if (!Editor.assetdb.exists(prefabPath)) {
|
||||
return callback(`Prefab not found at ${prefabPath}`);
|
||||
return callback(`找不到预制体: ${prefabPath}`);
|
||||
}
|
||||
// 更新预制体
|
||||
Editor.Ipc.sendToPanel("scene", "scene:update-prefab", nodeId, prefabPath);
|
||||
callback(null, `Command sent: Updating prefab ${prefabPath} from node ${nodeId}`);
|
||||
callback(null, `指令已发送: 从节点 ${nodeId} 更新预制体 ${prefabPath}`);
|
||||
break;
|
||||
|
||||
case "instantiate":
|
||||
@@ -1362,7 +1362,7 @@ export default class NewScript extends cc.Component {
|
||||
result.exists = true;
|
||||
callback(null, result);
|
||||
} else {
|
||||
callback(`Prefab not found: ${prefabPath}`);
|
||||
return callback(`找不到预制体: ${prefabPath}`);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1405,10 +1405,10 @@ export default class NewScript extends cc.Component {
|
||||
const refreshPath = (properties && properties.path) ? properties.path : 'db://assets/scripts';
|
||||
Editor.assetdb.refresh(refreshPath, (err) => {
|
||||
if (err) {
|
||||
addLog("error", `Refresh failed: ${err}`);
|
||||
addLog("error", `刷新失败: ${err}`);
|
||||
callback(err);
|
||||
} else {
|
||||
callback(null, `Editor refreshed: ${refreshPath}`);
|
||||
callback(null, `编辑器已刷新: ${refreshPath}`);
|
||||
}
|
||||
});
|
||||
break;
|
||||
@@ -1425,7 +1425,7 @@ export default class NewScript extends cc.Component {
|
||||
switch (action) {
|
||||
case "create":
|
||||
if (Editor.assetdb.exists(effectPath)) {
|
||||
return callback(`Effect already exists at ${effectPath}`);
|
||||
return callback(`Effect 已存在: ${effectPath}`);
|
||||
}
|
||||
// 确保父目录存在
|
||||
const absolutePath = Editor.assetdb.urlToFspath(effectPath);
|
||||
@@ -1476,21 +1476,21 @@ CCProgram fs %{
|
||||
Editor.assetdb.create(effectPath, content || defaultEffect, (err) => {
|
||||
if (err) return callback(err);
|
||||
Editor.assetdb.refresh(effectPath, (refreshErr) => {
|
||||
callback(refreshErr, refreshErr ? null : `Effect created at ${effectPath}`);
|
||||
callback(refreshErr, refreshErr ? null : `Effect 已创建: ${effectPath}`);
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
case "read":
|
||||
if (!Editor.assetdb.exists(effectPath)) {
|
||||
return callback(`Effect not found: ${effectPath}`);
|
||||
return callback(`找不到 Effect: ${effectPath}`);
|
||||
}
|
||||
const fspath = Editor.assetdb.urlToFspath(effectPath);
|
||||
try {
|
||||
const data = fs.readFileSync(fspath, "utf-8");
|
||||
callback(null, data);
|
||||
} catch (e) {
|
||||
callback(`Failed to read effect: ${e.message}`);
|
||||
callback(`读取 Effect 失败: ${e.message}`);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1502,19 +1502,19 @@ CCProgram fs %{
|
||||
try {
|
||||
fs.writeFileSync(writeFsPath, content, "utf-8");
|
||||
Editor.assetdb.refresh(effectPath, (err) => {
|
||||
callback(err, err ? null : `Effect updated at ${effectPath}`);
|
||||
callback(err, err ? null : `Effect 已更新: ${effectPath}`);
|
||||
});
|
||||
} catch (e) {
|
||||
callback(`Failed to write effect: ${e.message}`);
|
||||
callback(`更新 Effect 失败: ${e.message}`);
|
||||
}
|
||||
break;
|
||||
|
||||
case "delete":
|
||||
if (!Editor.assetdb.exists(effectPath)) {
|
||||
return callback(`Effect not found: ${effectPath}`);
|
||||
return callback(`找不到 Effect: ${effectPath}`);
|
||||
}
|
||||
Editor.assetdb.delete([effectPath], (err) => {
|
||||
callback(err, err ? null : `Effect deleted: ${effectPath}`);
|
||||
callback(err, err ? null : `Effect 已删除: ${effectPath}`);
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -1524,7 +1524,7 @@ CCProgram fs %{
|
||||
const info = Editor.assetdb.assetInfoByUuid(uuid);
|
||||
callback(null, info || { url: effectPath, uuid: uuid, exists: true });
|
||||
} else {
|
||||
callback(`Effect not found: ${effectPath}`);
|
||||
callback(`找不到 Effect: ${effectPath}`);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1541,7 +1541,7 @@ CCProgram fs %{
|
||||
switch (action) {
|
||||
case "create":
|
||||
if (Editor.assetdb.exists(matPath)) {
|
||||
return callback(`Material already exists at ${matPath}`);
|
||||
return callback(`材质已存在: ${matPath}`);
|
||||
}
|
||||
// 确保父目录存在
|
||||
const absolutePath = Editor.assetdb.urlToFspath(matPath);
|
||||
@@ -1569,14 +1569,14 @@ CCProgram fs %{
|
||||
Editor.assetdb.create(matPath, JSON.stringify(materialData, null, 2), (err) => {
|
||||
if (err) return callback(err);
|
||||
Editor.assetdb.refresh(matPath, (refreshErr) => {
|
||||
callback(refreshErr, refreshErr ? null : `Material created at ${matPath}`);
|
||||
callback(refreshErr, refreshErr ? null : `材质已创建: ${matPath}`);
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
case "update":
|
||||
if (!Editor.assetdb.exists(matPath)) {
|
||||
return callback(`Material not found at ${matPath}`);
|
||||
return callback(`找不到材质: ${matPath}`);
|
||||
}
|
||||
const fspath = Editor.assetdb.urlToFspath(matPath);
|
||||
try {
|
||||
@@ -1605,19 +1605,19 @@ CCProgram fs %{
|
||||
|
||||
fs.writeFileSync(fspath, JSON.stringify(matData, null, 2), "utf-8");
|
||||
Editor.assetdb.refresh(matPath, (err) => {
|
||||
callback(err, err ? null : `Material updated at ${matPath}`);
|
||||
callback(err, err ? null : `材质已更新: ${matPath}`);
|
||||
});
|
||||
} catch (e) {
|
||||
callback(`Failed to update material: ${e.message}`);
|
||||
callback(`更新材质失败: ${e.message}`);
|
||||
}
|
||||
break;
|
||||
|
||||
case "delete":
|
||||
if (!Editor.assetdb.exists(matPath)) {
|
||||
return callback(`Material not found at ${matPath}`);
|
||||
return callback(`找不到材质: ${matPath}`);
|
||||
}
|
||||
Editor.assetdb.delete([matPath], (err) => {
|
||||
callback(err, err ? null : `Material deleted at ${matPath}`);
|
||||
callback(err, err ? null : `材质已删除: ${matPath}`);
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -1627,7 +1627,7 @@ CCProgram fs %{
|
||||
const info = Editor.assetdb.assetInfoByUuid(uuid);
|
||||
callback(null, info || { url: matPath, uuid: uuid, exists: true });
|
||||
} else {
|
||||
callback(`Material not found: ${matPath}`);
|
||||
callback(`找不到材质: ${matPath}`);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -1644,7 +1644,7 @@ CCProgram fs %{
|
||||
switch (action) {
|
||||
case "create":
|
||||
if (Editor.assetdb.exists(path)) {
|
||||
return callback(`Texture already exists at ${path}`);
|
||||
return callback(`纹理已存在: ${path}`);
|
||||
}
|
||||
// 确保父目录存在
|
||||
const absolutePath = Editor.assetdb.urlToFspath(path);
|
||||
@@ -1671,7 +1671,7 @@ CCProgram fs %{
|
||||
// 4. 如果有 9-slice 设置,更新 Meta
|
||||
if (properties && (properties.border || properties.type)) {
|
||||
const uuid = Editor.assetdb.urlToUuid(path);
|
||||
if (!uuid) return callback(null, `Texture created but UUID not found immediately.`);
|
||||
if (!uuid) return callback(null, `纹理已创建,但未能立即获取 UUID。`);
|
||||
|
||||
// 稍微延迟确保 Meta 已生成
|
||||
setTimeout(() => {
|
||||
@@ -1700,28 +1700,28 @@ CCProgram fs %{
|
||||
|
||||
if (changed) {
|
||||
Editor.assetdb.saveMeta(uuid, JSON.stringify(meta), (err) => {
|
||||
if (err) Editor.warn(`Failed to save meta for ${path}: ${err}`);
|
||||
callback(null, `Texture created and meta updated at ${path}`);
|
||||
if (err) Editor.warn(`保存资源 Meta 失败 ${path}: ${err}`);
|
||||
callback(null, `纹理已创建并更新 Meta: ${path}`);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
callback(null, `Texture created at ${path}`);
|
||||
callback(null, `纹理已创建: ${path}`);
|
||||
}, 100);
|
||||
} else {
|
||||
callback(null, `Texture created at ${path}`);
|
||||
callback(null, `纹理已创建: ${path}`);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
callback(`Failed to write texture file: ${e.message}`);
|
||||
callback(`写入纹理文件失败: ${e.message}`);
|
||||
}
|
||||
break;
|
||||
case "delete":
|
||||
if (!Editor.assetdb.exists(path)) {
|
||||
return callback(`Texture not found at ${path}`);
|
||||
return callback(`找不到纹理: ${path}`);
|
||||
}
|
||||
Editor.assetdb.delete([path], (err) => {
|
||||
callback(err, err ? null : `Texture deleted at ${path}`);
|
||||
callback(err, err ? null : `纹理已删除: ${path}`);
|
||||
});
|
||||
break;
|
||||
case "get_info":
|
||||
@@ -1730,12 +1730,12 @@ CCProgram fs %{
|
||||
const info = Editor.assetdb.assetInfoByUuid(uuid);
|
||||
callback(null, info || { url: path, uuid: uuid, exists: true });
|
||||
} else {
|
||||
callback(`Texture not found: ${path}`);
|
||||
callback(`找不到纹理: ${path}`);
|
||||
}
|
||||
break;
|
||||
case "update":
|
||||
if (!Editor.assetdb.exists(path)) {
|
||||
return callback(`Texture not found at ${path}`);
|
||||
return callback(`找不到纹理: ${path}`);
|
||||
}
|
||||
const uuid = Editor.assetdb.urlToUuid(path);
|
||||
let meta = Editor.assetdb.loadMeta(uuid);
|
||||
@@ -1756,7 +1756,7 @@ CCProgram fs %{
|
||||
}
|
||||
|
||||
if (!meta) {
|
||||
return callback(`Failed to load meta for ${path}`);
|
||||
return callback(`加载资源 Meta 失败: ${path}`);
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
@@ -1838,7 +1838,7 @@ CCProgram fs %{
|
||||
}
|
||||
break;
|
||||
default:
|
||||
callback(`Unknown texture action: ${action}`);
|
||||
callback(`未知的纹理操作类型: ${action}`);
|
||||
break;
|
||||
}
|
||||
},
|
||||
@@ -2002,12 +2002,12 @@ CCProgram fs %{
|
||||
// 1. 获取文件系统路径
|
||||
const fspath = Editor.assetdb.urlToFspath(filePath);
|
||||
if (!fspath) {
|
||||
return callback(`File not found or invalid URL: ${filePath}`);
|
||||
return callback(`找不到文件或 URL 无效: ${filePath}`);
|
||||
}
|
||||
|
||||
// 2. 检查文件是否存在
|
||||
if (!fs.existsSync(fspath)) {
|
||||
return callback(`File does not exist: ${fspath}`);
|
||||
return callback(`文件不存在: ${fspath}`);
|
||||
}
|
||||
|
||||
// 3. 读取内容并验证
|
||||
@@ -2016,7 +2016,7 @@ CCProgram fs %{
|
||||
|
||||
// 检查空文件
|
||||
if (!content || content.trim().length === 0) {
|
||||
return callback(null, { valid: false, message: "Script is empty" });
|
||||
return callback(null, { valid: false, message: "脚本内容为空" });
|
||||
}
|
||||
|
||||
// 对于 JavaScript 脚本,使用 Function 构造器进行语法验证
|
||||
@@ -2024,7 +2024,7 @@ CCProgram fs %{
|
||||
const wrapper = `(function() { ${content} })`;
|
||||
try {
|
||||
new Function(wrapper);
|
||||
callback(null, { valid: true, message: "JavaScript syntax is valid" });
|
||||
callback(null, { valid: true, message: "JavaScript 语法验证通过" });
|
||||
} catch (syntaxErr) {
|
||||
return callback(null, { valid: false, message: syntaxErr.message });
|
||||
}
|
||||
@@ -2037,7 +2037,7 @@ CCProgram fs %{
|
||||
|
||||
// 检查是否有 class 定义 (简单的启发式检查)
|
||||
if (!content.includes('class ') && !content.includes('interface ') && !content.includes('enum ') && !content.includes('export ')) {
|
||||
return callback(null, { valid: true, message: "Warning: TypeScript file seems to lack standard definitions (class/interface), but basic syntax check is skipped due to missing compiler." });
|
||||
return callback(null, { valid: true, message: "警告: TypeScript 文件似乎缺少标准定义 (class/interface/export),但由于缺少编译器,已跳过基础语法检查。" });
|
||||
}
|
||||
|
||||
callback(null, { valid: true, message: "TypeScript 基础检查通过。(完整编译验证需要通过编辑器构建流程)" });
|
||||
@@ -2079,7 +2079,7 @@ CCProgram fs %{
|
||||
|
||||
// 修改场景中的节点(需要通过 scene-script)
|
||||
"set-node-property"(event, args) {
|
||||
addLog("mcp", `Creating node: ${args.name} (${args.type})`);
|
||||
addLog("mcp", `设置节点属性: ${args.name} (${args.type})`);
|
||||
// 确保第一个参数 'mcp-bridge' 和 package.json 的 name 一致
|
||||
Editor.Scene.callSceneScript("mcp-bridge", "set-property", args, (err, result) => {
|
||||
if (err) {
|
||||
@@ -2091,10 +2091,10 @@ CCProgram fs %{
|
||||
});
|
||||
},
|
||||
"create-node"(event, args) {
|
||||
addLog("mcp", `Creating node: ${args.name} (${args.type})`);
|
||||
addLog("mcp", `创建节点: ${args.name} (${args.type})`);
|
||||
Editor.Scene.callSceneScript("mcp-bridge", "create-node", args, (err, result) => {
|
||||
if (err) addLog("error", `CreateNode Failed: ${err}`);
|
||||
else addLog("success", `Node Created: ${result}`);
|
||||
if (err) addLog("error", `创建节点失败: ${err}`);
|
||||
else addLog("success", `节点已创建: ${result}`);
|
||||
event.reply(err, result);
|
||||
});
|
||||
},
|
||||
@@ -2376,9 +2376,14 @@ CCProgram fs %{
|
||||
break;
|
||||
case "begin_group":
|
||||
// scene:undo-record [id]
|
||||
// 这里的 id 好像是可选的,或者用于区分不同的事务
|
||||
Editor.Ipc.sendToPanel("scene", "scene:undo-record", description || "MCP Action");
|
||||
callback(null, `Undo group started: ${description || "MCP Action"}`);
|
||||
// 注意:在 2.4.x 中,undo-record 通常需要一个有效的 uuid
|
||||
// 如果没有提供 uuid,不应将 description 作为 ID 发送,否则会报 Unknown object to record
|
||||
addLog("info", `开始撤销组: ${description || "MCP 动作"}`);
|
||||
// 如果有参数包含 id,则记录该节点
|
||||
if (args.id) {
|
||||
Editor.Ipc.sendToPanel("scene", "scene:undo-record", args.id);
|
||||
}
|
||||
callback(null, `撤销组已启动: ${description || "MCP 动作"}`);
|
||||
break;
|
||||
case "end_group":
|
||||
Editor.Ipc.sendToPanel("scene", "scene:undo-commit");
|
||||
|
||||
@@ -109,44 +109,42 @@ module.exports = {
|
||||
let node = findNode(id);
|
||||
|
||||
if (node) {
|
||||
Editor.log(`[scene-script] Node found: ${node.name}, Current Pos: (${node.x}, ${node.y})`);
|
||||
Editor.log(`[scene-script] 找到节点: ${node.name}, 当前坐标: (${node.x}, ${node.y})`);
|
||||
|
||||
// 使用 scene:set-property 实现支持 Undo 的属性修改
|
||||
// 注意:IPC 消息需要发送到 'scene' 面板
|
||||
if (x !== undefined) {
|
||||
node.x = Number(x);
|
||||
Editor.log(`[scene-script] Set x to ${node.x}`);
|
||||
Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "x", type: "Number", value: Number(x) });
|
||||
}
|
||||
if (y !== undefined) {
|
||||
node.y = Number(y);
|
||||
Editor.log(`[scene-script] Set y to ${node.y}`);
|
||||
Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "y", type: "Number", value: Number(y) });
|
||||
}
|
||||
// [新增] 支持设置节点宽高 (用于测试 9-slice 等需要调整尺寸的情况)
|
||||
if (args.width !== undefined) {
|
||||
node.width = Number(args.width);
|
||||
Editor.log(`[scene-script] Set width to ${node.width}`);
|
||||
Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "width", type: "Number", value: Number(args.width) });
|
||||
}
|
||||
if (args.height !== undefined) {
|
||||
node.height = Number(args.height);
|
||||
Editor.log(`[scene-script] Set height to ${node.height}`);
|
||||
Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "height", type: "Number", value: Number(args.height) });
|
||||
}
|
||||
if (scaleX !== undefined) {
|
||||
Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "scaleX", type: "Number", value: Number(scaleX) });
|
||||
}
|
||||
if (scaleY !== undefined) {
|
||||
Editor.Ipc.sendToPanel("scene", "scene:set-property", { id, path: "scaleY", type: "Number", value: Number(scaleY) });
|
||||
}
|
||||
if (scaleX !== undefined) node.scaleX = Number(scaleX);
|
||||
if (scaleY !== undefined) node.scaleY = Number(scaleY);
|
||||
if (color) {
|
||||
const c = new cc.Color().fromHEX(color);
|
||||
// 使用 scene:set-property 实现支持 Undo 的颜色修改
|
||||
// 注意:IPC 消息需要发送到场景面板
|
||||
Editor.Ipc.sendToPanel("scene", "scene:set-property", {
|
||||
id: id,
|
||||
path: "color",
|
||||
type: "Color",
|
||||
value: { r: c.r, g: c.g, b: c.b, a: c.a }
|
||||
});
|
||||
// 既然走了 IPC,就不需要手动 set node.color 了,也不需要重复 dirty
|
||||
}
|
||||
|
||||
Editor.Ipc.sendToMain("scene:dirty");
|
||||
Editor.Ipc.sendToAll("scene:node-changed", { uuid: id });
|
||||
|
||||
Editor.log(`[scene-script] Update complete. New Pos: (${node.x}, ${node.y})`);
|
||||
Editor.log(`[scene-script] 更新完成。新坐标: (${node.x}, ${node.y})`);
|
||||
if (event.reply) event.reply(null, "变换信息已更新");
|
||||
} else {
|
||||
if (event.reply) event.reply(new Error("找不到节点"));
|
||||
@@ -349,9 +347,9 @@ module.exports = {
|
||||
loadedCount++;
|
||||
if (!err && asset) {
|
||||
loadedAssets[idx] = asset;
|
||||
Editor.log(`[scene-script] Successfully loaded asset for ${key}[${idx}]: ${asset.name}`);
|
||||
Editor.log(`[scene-script] 成功为 ${key}[${idx}] 加载资源: ${asset.name}`);
|
||||
} else {
|
||||
Editor.warn(`[scene-script] Failed to load asset ${uuid} for ${key}[${idx}]: ${err}`);
|
||||
Editor.warn(`[scene-script] 未能为 ${key}[${idx}] 加载资源 ${uuid}: ${err}`);
|
||||
}
|
||||
|
||||
if (loadedCount === uuids.length) {
|
||||
@@ -391,13 +389,13 @@ module.exports = {
|
||||
if (targetComp) {
|
||||
finalValue = targetComp;
|
||||
} else {
|
||||
Editor.warn(`[scene-script] Component ${propertyType.name} not found on node ${targetNode.name}`);
|
||||
Editor.warn(`[scene-script] 在节点 ${targetNode.name} 上找不到组件 ${propertyType.name}`);
|
||||
}
|
||||
}
|
||||
Editor.log(`[scene-script] Applied Reference for ${key}: ${targetNode.name}`);
|
||||
Editor.log(`[scene-script] 已应用 ${key} 的引用: ${targetNode.name}`);
|
||||
} else if (value && value.length > 20) {
|
||||
// 如果明确是组件/节点类型但找不到,才报错
|
||||
Editor.warn(`[scene-script] Failed to resolve target node/comp for ${key}: ${value}`);
|
||||
Editor.warn(`[scene-script] 无法解析 ${key} 的目标节点/组件: ${value}`);
|
||||
}
|
||||
} else {
|
||||
// 3. 通用启发式 (找不到类型时的 fallback)
|
||||
@@ -405,7 +403,7 @@ module.exports = {
|
||||
const targetNode = findNode(value);
|
||||
if (targetNode) {
|
||||
finalValue = targetNode;
|
||||
Editor.log(`[scene-script] Heuristic resolved Node for ${key}: ${targetNode.name}`);
|
||||
Editor.log(`[scene-script] 启发式解析 ${key} 的节点: ${targetNode.name}`);
|
||||
} else {
|
||||
// 找不到节点且是 UUID -> 视为资源
|
||||
const compIndex = node._components.indexOf(component);
|
||||
@@ -417,14 +415,14 @@ module.exports = {
|
||||
value: { uuid: value },
|
||||
isSubProp: false
|
||||
});
|
||||
Editor.log(`[scene-script] Heuristic resolved Asset via IPC for ${key}: ${value}`);
|
||||
Editor.log(`[scene-script] 通过 IPC 启发式解析 ${key} 的资源: ${value}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Editor.warn(`[scene-script] Property resolution failed for ${key}: ${e.message}`);
|
||||
Editor.warn(`[scene-script] 解析属性 ${key} 失败: ${e.message}`);
|
||||
}
|
||||
|
||||
component[key] = finalValue;
|
||||
|
||||
38
注意事项.md
38
注意事项.md
@@ -48,7 +48,7 @@
|
||||
1. **真实加载**:使用 `cc.AssetLibrary.loadAsset(uuid, callback)` 在场景进程中异步加载真实的资源实例。
|
||||
2. **实例赋值**:在回调中将加载到的 `asset` 对象赋予组件(`component[key] = asset`),这确保了场景保存时能生成正确的序列化对象。
|
||||
3. **UI 同步**:同步发送 IPC `scene:set-property`,使用 `{ uuid: value }` 格式通知编辑器面板更新 Inspector UI。
|
||||
* **Node/Component**: 对于节点或组件引用,通过 `findNode` 查找实例并直接赋值实例对象,而非 UUID 字符串。
|
||||
* **Node/Component**: 对于节点 or 组件引用,通过 `findNode` 查找实例并直接赋值实例对象,而非 UUID 字符串。
|
||||
|
||||
---
|
||||
|
||||
@@ -68,13 +68,29 @@
|
||||
* **资产识别启发式**:当通过 `manage_components` 赋值时,如果属性名包含以下关键字,插件会尝试将其作为 UUID 资源处理:
|
||||
`prefab`, `sprite`, `texture`, `material`, `skeleton`, `spine`, `atlas`, `font`, `audio`, `data`
|
||||
* **建议**:如果资源未正确加载,请检查属性名是否包含以上关键字,或手动确认该 UUID 不属于任何节点。
|
||||
69:
|
||||
70: ---
|
||||
71:
|
||||
72: ## 7. 搜索工具 (search_project) 使用建议
|
||||
73: * **性能建议**:尽量指定 `path` 参数缩小搜索范围(如 `db://assets/scripts`),避免全项目大面积搜索,尤其是在包含大量旧资源的 Library 目录(虽然插件已过滤)。
|
||||
74: * **正则表达式**:在使用 `useRegex` 时,确保正则模式的语法正确。如果正则匹配失败,工具会返回详细的错误提示。
|
||||
75: * **模式选择**:
|
||||
76: * 查找具体逻辑代码:使用 `matchType: "content"`。
|
||||
77: * 定位资源文件:使用 `matchType: "file_name"` 并配合 `extensions` 过滤。
|
||||
78: * 重构目录结构前:使用 `matchType: "dir_name"` 检查目录名冲突。
|
||||
|
||||
---
|
||||
|
||||
## 7. 搜索工具 (search_project) 使用建议
|
||||
* **性能建议**:尽量指定 `path` 参数缩小搜索范围(如 `db://assets/scripts`),避免全项目大面积搜索,尤其是在包含大量旧资源的 Library 目录(虽然插件已过滤)。
|
||||
* **正则表达式**:在使用 `useRegex` 时,确保正则模式的语法正确。如果正则匹配失败,工具会返回详细的错误提示。
|
||||
* **模式选择**:
|
||||
* 查找具体逻辑代码:使用 `matchType: "content"`。
|
||||
* 定位资源文件:使用 `matchType: "file_name"` 并配合 `extensions` 过滤。
|
||||
* 重构目录结构前:使用 `matchType: "dir_name"` 检查目录名冲突。
|
||||
|
||||
---
|
||||
|
||||
## 8. Undo/Redo (撤销/重做) 使用指南
|
||||
|
||||
### 8.1 事务分组
|
||||
* **背景**:连续执行多次修改(如同时移动并缩放)时,通常希望一次“撤销”能回滚所有更改。
|
||||
* **最佳实践**:
|
||||
1. 调用 `manage_undo(action: 'begin_group', description: '操作描述')`。
|
||||
2. 执行多次修改(如调用 `update_node_transform`)。
|
||||
3. 调用 `manage_undo(action: 'end_group')`。
|
||||
* **注意**:`undo-record` 需要在 `begin_group` 时明确关联节点 ID,否则可能导致撤销栈无法精准匹配对象。
|
||||
|
||||
### 8.2 属性修改方式
|
||||
* **核心规则**:在 `scene-script.js` 中严禁直接使用 `node.x = 100`。
|
||||
* **正确做法**:必须通过 `Editor.Ipc.sendToPanel('scene', 'scene:set-property', ...)`。只有这样,修改才会被 Cocos Creator 的 UndoManager 捕获,从而支持撤销。
|
||||
|
||||
Reference in New Issue
Block a user