feat: 修复节点变换属性竞态问题,扩展完整属性支持,增强获取数据工具

- fix: 将 update-node-transform 中 scaleX/scaleY 从异步 IPC 改为直接赋值,修复设置不生效
- fix: 将 color 从异步 IPC 改为直接赋值 (node.color = new cc.Color().fromHEX(color))
- feat: update_node_transform 新增 rotation/anchorX/anchorY/opacity/skewX/skewY 参数
- feat: get_scene_hierarchy includeDetails 新增 rotation/anchor/color/opacity/skew/group 返回
- feat: callSceneScriptWithTimeout 对 panel not found 错误返回友好中文提示
- docs: 更新 UPDATE_LOG.md/注意事项.md/project-rules.md 反映所有修改
This commit is contained in:
火焰库拉
2026-03-01 12:18:02 +08:00
parent a618497028
commit 77aba8e7f3
5 changed files with 153 additions and 108 deletions

View File

@@ -7,18 +7,20 @@ description: "MCP Bridge 插件开发规则和编码规范"
## 1. 语言规范 (Language) ## 1. 语言规范 (Language)
* **强制中文**: 所有的对话回复、代码注释、以及生成的文档都必须使用**中文**。 - **强制中文**: 所有的对话回复、代码注释、以及生成的文档都必须使用**中文**。
* **日志消息**: `addLog()` 的消息内容可以使用中英文混合,确保关键术语清晰。 - **日志消息**: `addLog()` 的消息内容可以使用中英文混合,确保关键术语清晰。
--- ---
## 2. 关键工作流程 (Critical Workflow) ## 2. 关键工作流程 (Critical Workflow)
### 2.1 插件重载 ### 2.1 插件重载
- **必须重载**: 修改 `main.js`, `package.json`, `scene-script.js`, 或 `panel/` 后,**必须**在编辑器中执行「扩展 → 刷新」或重启编辑器。 - **必须重载**: 修改 `main.js`, `package.json`, `scene-script.js`, 或 `panel/` 后,**必须**在编辑器中执行「扩展 → 刷新」或重启编辑器。
- **热更新不适用**: Cocos Creator 2.x 的插件主进程脚本不支持热更新。 - **热更新不适用**: Cocos Creator 2.x 的插件主进程脚本不支持热更新。
### 2.2 测试驱动 ### 2.2 测试驱动
- **测试脚本**: 每个新功能必须在 `test/` 目录下创建独立测试脚本 (如 `test/test_feature.js`)。 - **测试脚本**: 每个新功能必须在 `test/` 目录下创建独立测试脚本 (如 `test/test_feature.js`)。
- **HTTP 验证**: 测试脚本通过 HTTP 请求直接调用插件 API验证功能正确性。 - **HTTP 验证**: 测试脚本通过 HTTP 请求直接调用插件 API验证功能正确性。
- **运行前提**: 确保 Cocos Creator 编辑器已打开且 MCP Bridge 服务已启动。 - **运行前提**: 确保 Cocos Creator 编辑器已打开且 MCP Bridge 服务已启动。
@@ -29,10 +31,10 @@ description: "MCP Bridge 插件开发规则和编码规范"
### 3.1 进程职责划分 ### 3.1 进程职责划分
| 文件 | 进程 | 可访问 | 不可访问 | | 文件 | 进程 | 可访问 | 不可访问 |
|------|------|--------|----------| | ----------------- | ------------------- | ------------------------------------------- | ------------------------------------- |
| `main.js` | 主进程 (Main) | `Editor.assetdb`, `Editor.Ipc`, `require()` | `cc.*` (Cocos 引擎) | | `main.js` | 主进程 (Main) | `Editor.assetdb`, `Editor.Ipc`, `require()` | `cc.*` (Cocos 引擎) |
| `scene-script.js` | 渲染进程 (Renderer) | `cc.*`, `cc.engine`, `cc.director` | `Editor.assetdb`, `Editor.FileSystem` | | `scene-script.js` | 渲染进程 (Renderer) | `cc.*`, `cc.engine`, `cc.director` | `Editor.assetdb`, `Editor.FileSystem` |
### 3.2 跨进程通信规则 ### 3.2 跨进程通信规则
@@ -59,14 +61,14 @@ description: "MCP Bridge 插件开发规则和编码规范"
### 4.1 命名规范 ### 4.1 命名规范
| 类型 | 规范 | 示例 | | 类型 | 规范 | 示例 |
|------|------|------| | ---------- | -------------------- | --------------------------------- |
| 函数名 | camelCase | `handleMcpCall`, `manageScript` | | 函数名 | camelCase | `handleMcpCall`, `manageScript` |
| 常量 | SCREAMING_SNAKE_CASE | `MAX_RESULTS`, `DEFAULT_PORT` | | 常量 | SCREAMING_SNAKE_CASE | `MAX_RESULTS`, `DEFAULT_PORT` |
| 私有变量 | _camelCase | `_isMoving`, `_timer` | | 私有变量 | \_camelCase | `_isMoving`, `_timer` |
| 布尔变量 | is/has/can 前缀 | `isSceneBusy`, `hasComponent` | | 布尔变量 | is/has/can 前缀 | `isSceneBusy`, `hasComponent` |
| MCP 工具名 | snake_case | `get_selected_node`, `manage_vfx` | | MCP 工具名 | snake_case | `get_selected_node`, `manage_vfx` |
| IPC 消息名 | kebab-case | `get-hierarchy`, `create-node` | | IPC 消息名 | kebab-case | `get-hierarchy`, `create-node` |
### 4.2 函数组织顺序 ### 4.2 函数组织顺序
@@ -76,28 +78,28 @@ description: "MCP Bridge 插件开发规则和编码规范"
module.exports = { module.exports = {
// 1. 配置属性 // 1. 配置属性
"scene-script": "scene-script.js", "scene-script": "scene-script.js",
// 2. 生命周期函数 // 2. 生命周期函数
load() { }, load() {},
unload() { }, unload() {},
// 3. 服务器管理 // 3. 服务器管理
startServer(port) { }, startServer(port) {},
stopServer() { }, stopServer() {},
// 4. 核心处理逻辑 // 4. 核心处理逻辑
handleMcpCall(name, args, callback) { }, handleMcpCall(name, args, callback) {},
// 5. 工具函数 (按字母顺序) // 5. 工具函数 (按字母顺序)
applyTextEdits(args, callback) { }, applyTextEdits(args, callback) {},
batchExecute(args, callback) { }, batchExecute(args, callback) {},
// ... // ...
// 6. IPC 消息处理 // 6. IPC 消息处理
messages: { messages: {
"open-test-panel"() { }, "open-test-panel"() {},
// ... // ...
} },
}; };
``` ```
@@ -106,6 +108,7 @@ module.exports = {
> ⚠️ **重要**: `main.js` 已存在重复函数问题,编辑前务必使用 `view_file` 确认上下文,避免创建重复定义。 > ⚠️ **重要**: `main.js` 已存在重复函数问题,编辑前务必使用 `view_file` 确认上下文,避免创建重复定义。
**检查清单**: **检查清单**:
- [ ] 新增函数前,搜索是否已存在同名函数 - [ ] 新增函数前,搜索是否已存在同名函数
- [ ] 修改函数时,确认只有一个定义 - [ ] 修改函数时,确认只有一个定义
- [ ] `messages` 对象中避免重复的消息处理器 - [ ] `messages` 对象中避免重复的消息处理器
@@ -122,16 +125,16 @@ addLog("mcp", `REQ -> [${toolName}]`);
addLog("success", `RES <- [${toolName}] 成功`); addLog("success", `RES <- [${toolName}] 成功`);
// ❌ 错误 // ❌ 错误
console.log("服务启动成功"); // 不会被 read_console 捕获 console.log("服务启动成功"); // 不会被 read_console 捕获
``` ```
| type | 用途 | 颜色 | | type | 用途 | 颜色 |
|------|------|------| | --------- | ------------- | ---- |
| `info` | 一般信息 | 蓝色 | | `info` | 一般信息 | 蓝色 |
| `success` | 操作成功 | 绿色 | | `success` | 操作成功 | 绿色 |
| `warn` | 警告信息 | 黄色 | | `warn` | 警告信息 | 黄色 |
| `error` | 错误信息 | 红色 | | `error` | 错误信息 | 红色 |
| `mcp` | MCP 请求/响应 | 紫色 | | `mcp` | MCP 请求/响应 | 紫色 |
--- ---
@@ -148,13 +151,15 @@ Editor.Ipc.sendToPanel("scene", "scene:set-property", {
path: "x", path: "x",
type: "Float", type: "Float",
value: 100, value: 100,
isSubProp: false isSubProp: false,
}); });
// 不支持 Undo (直接修改) // ⚠️ 不支持 Undo但同步生效update-node-transform 使用此方式)
node.x = 100; node.x = 100;
``` ```
> **注意**: `update-node-transform` 中所有 13 个属性均使用直接赋值方式,这是为了解决异步 IPC 竞态条件导致属性不生效的问题。此为设计性 trade-off牺牲 Undo 支持以保证属性即时可靠生效。
### 5.2 使用 Undo 组 ### 5.2 使用 Undo 组
对于复合操作,使用 Undo 组包装: 对于复合操作,使用 Undo 组包装:
@@ -193,7 +198,7 @@ const defaultPaths = [
"db://internal/image/default_sprite_splash", "db://internal/image/default_sprite_splash",
"db://internal/image/default_sprite_splash.png", "db://internal/image/default_sprite_splash.png",
"db://internal/image/default_particle", "db://internal/image/default_particle",
"db://internal/image/default_particle.png" "db://internal/image/default_particle.png",
]; ];
for (const path of defaultPaths) { for (const path of defaultPaths) {
@@ -226,12 +231,12 @@ setTimeout(() => {
```javascript ```javascript
// ✅ 标准风格 // ✅ 标准风格
callback(null, result); // 成功 callback(null, result); // 成功
callback("Error message"); // 失败 (字符串) callback("Error message"); // 失败 (字符串)
callback(new Error("message")); // 失败 (Error 对象) callback(new Error("message")); // 失败 (Error 对象)
// 避免混用 // 避免混用
callback(err, null); // 不推荐,保持一致性 callback(err, null); // 不推荐,保持一致性
``` ```
### 7.2 异步操作错误处理 ### 7.2 异步操作错误处理
@@ -252,22 +257,22 @@ Editor.assetdb.queryInfoByUrl(path, (err, info) => {
使用 [Conventional Commits](https://conventionalcommits.org/) 格式: 使用 [Conventional Commits](https://conventionalcommits.org/) 格式:
| 类型 | 用途 | 示例 | | 类型 | 用途 | 示例 |
|------|------|------| | ---------- | -------- | -------------------------------------------- |
| `feat` | 新功能 | `feat: add manage_vfx tool` | | `feat` | 新功能 | `feat: add manage_vfx tool` |
| `fix` | 修复 bug | `fix: resolve duplicate function in main.js` | | `fix` | 修复 bug | `fix: resolve duplicate function in main.js` |
| `docs` | 文档更新 | `docs: add code review report` | | `docs` | 文档更新 | `docs: add code review report` |
| `refactor` | 重构 | `refactor: split main.js into modules` | | `refactor` | 重构 | `refactor: split main.js into modules` |
| `test` | 测试 | `test: add material management tests` | | `test` | 测试 | `test: add material management tests` |
| `chore` | 杂项 | `chore: update dependencies` | | `chore` | 杂项 | `chore: update dependencies` |
--- ---
## 9. 已知问题 (Known Issues) ## 9. 已知问题 (Known Issues)
| 问题 | 原因 | 解决方案 | | 问题 | 原因 | 解决方案 |
|------|------|----------| | ---------------------------------------- | ------------------------------------- | ---------------------------------- |
| "Unknown object to record" 错误 | Cocos 2.4.x Undo 系统与 MCP 交互问题 | 可忽略,不影响功能 | | "Unknown object to record" 错误 | Cocos 2.4.x Undo 系统与 MCP 交互问题 | 可忽略,不影响功能 |
| "sendToMain scene:stash-and-save failed" | 时序问题 | 手动 Ctrl+S 保存 | | "sendToMain scene:stash-and-save failed" | 时序问题 | 手动 Ctrl+S 保存 |
| 颜色修改不支持 Undo | 使用 scene-script 直接修改 | 待优化 | | `update-node-transform` 不支持 Undo | 为解决异步 IPC 竞态问题,改用直接赋值 | 设计性 trade-off保证属性即时生效 |
| execute_menu_item 仅支持部分菜单 | 缺乏通用菜单 IPC | 添加菜单映射表 | | execute_menu_item 仅支持部分菜单 | 缺乏通用菜单 IPC | 添加菜单映射表 |

View File

@@ -347,5 +347,40 @@
### 3. Texture2D -> SpriteFrame 子资源 UUID 自动解析 ### 3. Texture2D -> SpriteFrame 子资源 UUID 自动解析
- **问题**: AI 大模型在查找图片引用时,通常只知道 Texture2D (原图) 的 UUID`cc.Sprite.spriteFrame` 实际引用的是子资源 SpriteFrame 的 UUID导致查找结果为空 - **问题**: AI 传入图片 (Texture2D) 的 UUID`cc.Sprite.spriteFrame` 实际引用的是该图片的子资源 SpriteFrame(具有不同的 UUID,导致直接查找返回空结果
- **修复**: 在 `src/main.js``find_references` 路由中,调用 scene-script 前自动读取目标 UUID 对应资源的 `.meta` 文件,提取所有 `subMetas` 中的子资源 UUID (如 SpriteFrame),作为 `additionalIds` 传递给 scene-script。scene-script 将这些额外 UUID 及其压缩/解压变体一并加入匹配列表,实现 "传入 Texture2D UUID 也能查到 SpriteFrame 引用" 的透明体验。 - **修复**: 在 `src/main.js``find_references` 路由中,调用 scene-script 前自动读取目标 UUID 对应资源的 `.meta` 文件,提取所有 `subMetas` 中的子资源 UUID (如 SpriteFrame),作为 `additionalIds` 传递给 scene-script。scene-script 将这些额外 UUID 及其压缩/解压变体一并加入匹配列表,实现 "传入 Texture2D UUID 也能查到 SpriteFrame 引用" 的透明体验。
---
## 节点变换属性修复与数据增强 (2026-03-01)
### 1. `update-node-transform` 属性设置方式修复 (`src/scene-script.js`)
- **问题**: `update-node-transform``x`, `y`, `width`, `height`, `scaleX`, `scaleY` 六个属性通过异步 IPC `Editor.Ipc.sendToPanel("scene", "scene:set-property", ...)` 设置,存在 fire-and-forget 问题函数在属性实际生效前就已返回成功回复。导致宽高不生效Sprite 的 sizeMode 可能在异步消息到达前重置)、坐标不生效(异步 IPC 竞态条件)、批量操作属性丢失等问题。
- **修复**: 将所有属性设置统一改为在 scene-script渲染进程中直接对节点属性同步赋值`node.x = Number(x)`),与 `set-property``create-node` 中的处理方式保持一致。`color` 同步改为直接赋值 `node.color = new cc.Color().fromHEX(color)`
- **已验证**: 全部 7 个属性x, y, width, height, scaleX, scaleY, color批量设置后均即时生效。
### 2. `update_node_transform` 工具参数扩展 (`src/main.js` + `src/scene-script.js`)
- **问题**: 编辑器属性面板中的 Rotation、Anchor、Opacity、Skew 四类属性无法通过 `update_node_transform` MCP 工具设置和获取。
- **修复**:
-`src/main.js` 的工具 `inputSchema` 中新增 `rotation`, `anchorX`, `anchorY`, `opacity`, `skewX`, `skewY` 六个参数定义。
-`src/scene-script.js``update-node-transform` 处理函数中新增对应的直接赋值逻辑(`node.angle`, `node.anchorX`, `node.anchorY`, `node.opacity`, `node.skewX`, `node.skewY`)。
- **已验证**: 全部 13 个属性x, y, rotation, width, height, scaleX, scaleY, anchorX, anchorY, color, opacity, skewX, skewY批量设置后均即时生效。
### 3. `get_scene_hierarchy` 节点详情数据增强 (`src/scene-script.js`)
- **问题**: `includeDetails` 模式仅返回 position, scale, size 三类数据,缺少 rotation, anchor, color, opacity, skew, group 信息,无法通过 API 完整验证节点属性。
- **修复**: 在 `dumpNodes` 函数的 `includeDetails` 分支中新增六个返回字段:
- `rotation`: `node.angle`
- `anchor`: `{ x: node.anchorX, y: node.anchorY }`
- `color`: `{ r: node.color.r, g: node.color.g, b: node.color.b }`
- `opacity`: `node.opacity`
- `skew`: `{ x: node.skewX, y: node.skewY }`
- `group`: `node.group`
- **效果**: `includeDetails` 现在返回与编辑器属性面板完全一致的所有节点属性。
### 4. Scene 面板未就绪友好提示 (`src/main.js`)
- **问题**: 插件重载或场景切换期间调用 scene-script 方法时,原始错误 `Error: ipc failed to send, panel not found` 信息晦涩,容易让用户误以为插件出现严重故障。
- **修复**: 在 `callSceneScriptWithTimeout` 的回调中检测 `panel not found` 错误,自动替换为友好中文提示:`场景面板尚未就绪(可能正在重载插件或切换场景),请等待几秒后重试`。日志级别从 `error` 降为 `warn`

View File

@@ -114,8 +114,9 @@
### 8.2 属性修改方式 ### 8.2 属性修改方式
- **核心规则**:在 `scene-script.js` 中严禁直接使用 `node.x = 100` - **`update-node-transform` 的做法**:出于解决异步 IPC 竞态条件的考量,`update-node-transform` 中所有 13 个属性x, y, rotation, width, height, scaleX, scaleY, anchorX, anchorY, color, opacity, skewX, skewY均使用**直接赋值**(如 `node.x = 100`)。直接赋值是同步操作,确保属性在函数返回前已生效,但**不支持 Undo**
- **正确做法**:必须通过 `Editor.Ipc.sendToPanel('scene', 'scene:set-property', ...)`。只有这样,修改才会被 Cocos Creator 的 UndoManager 捕获,从而支持撤销 - **需要 Undo 的场景**:如果业务要求支持撤销,应改用 `Editor.Ipc.sendToPanel('scene', 'scene:set-property', ...)`,但需注意异步竞态风险
- **`get_scene_hierarchy``includeDetails`**:现在返回与编辑器属性面板完全一致的节点数据,包括 position, rotation, scale, anchor, size, color, opacity, skew, group。
--- ---

View File

@@ -89,7 +89,18 @@ function callSceneScriptWithTimeout(pluginName, method, args, callback, timeout
if (!settled) { if (!settled) {
settled = true; settled = true;
clearTimeout(timer); clearTimeout(timer);
callback(err, result); // 友好化处理 Scene 面板未就绪的错误(如插件重载、场景切换期间)
if (err && typeof err === "object" && err.message && err.message.includes("panel not found")) {
const friendlyMsg = `场景面板尚未就绪(可能正在重载插件或切换场景),请等待几秒后重试。原始信息: ${err.message}`;
addLog("warn", `[scene-script] ${friendlyMsg}`);
callback(friendlyMsg);
} else if (err && typeof err === "string" && err.includes("panel not found")) {
const friendlyMsg = `场景面板尚未就绪(可能正在重载插件或切换场景),请等待几秒后重试。原始信息: ${err}`;
addLog("warn", `[scene-script] ${friendlyMsg}`);
callback(friendlyMsg);
} else {
callback(err, result);
}
} }
}; };
@@ -286,11 +297,17 @@ const getToolsList = () => {
id: { type: "string", description: "节点 UUID" }, id: { type: "string", description: "节点 UUID" },
x: { type: "number" }, x: { type: "number" },
y: { type: "number" }, y: { type: "number" },
rotation: { type: "number", description: "旋转角度" },
width: { type: "number" }, width: { type: "number" },
height: { type: "number" }, height: { type: "number" },
scaleX: { type: "number" }, scaleX: { type: "number" },
scaleY: { type: "number" }, scaleY: { type: "number" },
anchorX: { type: "number", description: "锚点 X (0~1)" },
anchorY: { type: "number", description: "锚点 Y (0~1)" },
color: { type: "string", description: "HEX 颜色代码如 #FF0000" }, color: { type: "string", description: "HEX 颜色代码如 #FF0000" },
opacity: { type: "number", description: "透明度 (0~255)" },
skewX: { type: "number", description: "倾斜 X" },
skewY: { type: "number", description: "倾斜 Y" },
}, },
required: ["id"], required: ["id"],
}, },

View File

@@ -107,8 +107,14 @@ module.exports = {
if (includeDetails) { if (includeDetails) {
nodeData.active = node.active; nodeData.active = node.active;
nodeData.position = { x: Math.round(node.x), y: Math.round(node.y) }; nodeData.position = { x: Math.round(node.x), y: Math.round(node.y) };
nodeData.rotation = node.angle;
nodeData.scale = { x: node.scaleX, y: node.scaleY }; nodeData.scale = { x: node.scaleX, y: node.scaleY };
nodeData.anchor = { x: node.anchorX, y: node.anchorY };
nodeData.size = { width: node.width, height: node.height }; nodeData.size = { width: node.width, height: node.height };
nodeData.color = { r: node.color.r, g: node.color.g, b: node.color.b };
nodeData.opacity = node.opacity;
nodeData.skew = { x: node.skewX, y: node.skewY };
nodeData.group = node.group;
nodeData.components = comps.map((c) => cc.js.getClassName(c)); nodeData.components = comps.map((c) => cc.js.getClassName(c));
} else { } else {
// 简略模式下如果存在组件,至少提供一个极简列表让 AI 知道节点的作用 // 简略模式下如果存在组件,至少提供一个极简列表让 AI 知道节点的作用
@@ -137,9 +143,9 @@ module.exports = {
}, },
/** /**
* 批量更新节点的变换信息 (坐标、缩放、颜色) * 批量更新节点的变换信息 (坐标、缩放、颜色)
* @param {Object} event IPC 事件对象 * @param {Object} event IPC 事件对象
* @param {Object} args 参数 (id, x, y, scaleX, scaleY, color) * @param {Object} args 参数 (id, x, y, rotation, scaleX, scaleY, anchorX, anchorY, color, opacity, skewX, skewY, width, height)
*/ */
"update-node-transform": function (event, args) { "update-node-transform": function (event, args) {
const { id, x, y, scaleX, scaleY, color } = args; const { id, x, y, scaleX, scaleY, color } = args;
@@ -147,64 +153,45 @@ module.exports = {
let node = findNode(id); let node = findNode(id);
if (node) { if (node) {
// 使用 scene:set-property 实现支持 Undo 的属性修改 // 直接赋值,确保同步生效
// 注意IPC 消息需要发送到 'scene' 面板
if (x !== undefined) { if (x !== undefined) {
Editor.Ipc.sendToPanel("scene", "scene:set-property", { node.x = Number(x);
id,
path: "x",
type: "Number",
value: Number(x),
});
} }
if (y !== undefined) { if (y !== undefined) {
Editor.Ipc.sendToPanel("scene", "scene:set-property", { node.y = Number(y);
id, }
path: "y", if (args.rotation !== undefined) {
type: "Number", node.angle = Number(args.rotation);
value: Number(y),
});
} }
if (args.width !== undefined) { if (args.width !== undefined) {
Editor.Ipc.sendToPanel("scene", "scene:set-property", { node.width = Number(args.width);
id,
path: "width",
type: "Number",
value: Number(args.width),
});
} }
if (args.height !== undefined) { if (args.height !== undefined) {
Editor.Ipc.sendToPanel("scene", "scene:set-property", { node.height = Number(args.height);
id,
path: "height",
type: "Number",
value: Number(args.height),
});
} }
if (scaleX !== undefined) { if (scaleX !== undefined) {
Editor.Ipc.sendToPanel("scene", "scene:set-property", { node.scaleX = Number(scaleX);
id,
path: "scaleX",
type: "Number",
value: Number(scaleX),
});
} }
if (scaleY !== undefined) { if (scaleY !== undefined) {
Editor.Ipc.sendToPanel("scene", "scene:set-property", { node.scaleY = Number(scaleY);
id, }
path: "scaleY", if (args.anchorX !== undefined) {
type: "Number", node.anchorX = Number(args.anchorX);
value: Number(scaleY), }
}); if (args.anchorY !== undefined) {
node.anchorY = Number(args.anchorY);
} }
if (color) { if (color) {
const c = new cc.Color().fromHEX(color); node.color = new cc.Color().fromHEX(color);
Editor.Ipc.sendToPanel("scene", "scene:set-property", { }
id: id, if (args.opacity !== undefined) {
path: "color", node.opacity = Number(args.opacity);
type: "Color", }
value: { r: c.r, g: c.g, b: c.b, a: c.a }, if (args.skewX !== undefined) {
}); node.skewX = Number(args.skewX);
}
if (args.skewY !== undefined) {
node.skewY = Number(args.skewY);
} }
Editor.Ipc.sendToMain("scene:dirty"); Editor.Ipc.sendToMain("scene:dirty");