2026-01-29 13:47:38 +08:00
|
|
|
|
"use strict";
|
|
|
|
|
|
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 更加健壮的节点查找函数,支持解压后的 UUID
|
|
|
|
|
|
* @param {string} id 节点的 UUID (支持 22 位压缩格式)
|
|
|
|
|
|
* @returns {cc.Node | null} 找到的节点对象或 null
|
|
|
|
|
|
*/
|
|
|
|
|
|
const findNode = (id) => {
|
|
|
|
|
|
if (!id) return null;
|
|
|
|
|
|
let node = cc.engine.getInstanceById(id);
|
|
|
|
|
|
if (!node && typeof Editor !== 'undefined' && Editor.Utils && Editor.Utils.UuidUtils) {
|
|
|
|
|
|
// 如果直接查不到,尝试对可能是压缩格式的 ID 进行解压后再次查找
|
|
|
|
|
|
try {
|
|
|
|
|
|
const decompressed = Editor.Utils.UuidUtils.decompressUuid(id);
|
|
|
|
|
|
if (decompressed !== id) {
|
|
|
|
|
|
node = cc.engine.getInstanceById(decompressed);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// 忽略转换错误
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return node;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-01-29 13:47:38 +08:00
|
|
|
|
module.exports = {
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 修改节点的基础属性
|
|
|
|
|
|
* @param {Object} event IPC 事件对象
|
|
|
|
|
|
* @param {Object} args 参数 (id, path, value)
|
|
|
|
|
|
*/
|
2026-02-03 19:55:51 +08:00
|
|
|
|
"set-property": function (event, args) {
|
|
|
|
|
|
const { id, path, value } = args;
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 获取节点
|
2026-02-07 23:14:12 +08:00
|
|
|
|
let node = findNode(id);
|
2026-02-03 19:55:51 +08:00
|
|
|
|
|
|
|
|
|
|
if (node) {
|
|
|
|
|
|
// 2. 修改属性
|
|
|
|
|
|
if (path === "name") {
|
|
|
|
|
|
node.name = value;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
node[path] = value;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 【解决报错的关键】告诉编辑器场景变脏了(需要保存)
|
|
|
|
|
|
// 在场景进程中,我们发送 IPC 给主进程
|
|
|
|
|
|
Editor.Ipc.sendToMain("scene:dirty");
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 【额外补丁】通知层级管理器(Hierarchy)同步更新节点名称
|
|
|
|
|
|
// 否则你修改了名字,层级管理器可能还是显示旧名字
|
|
|
|
|
|
Editor.Ipc.sendToAll("scene:node-changed", {
|
|
|
|
|
|
uuid: id,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (event.reply) {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
event.reply(null, `节点 ${id} 已更新为 ${value}`);
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (event.reply) {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
event.reply(new Error("场景脚本:找不到节点 " + id));
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取当前场景的完整层级树
|
|
|
|
|
|
* @param {Object} event IPC 事件对象
|
|
|
|
|
|
*/
|
2026-02-03 19:55:51 +08:00
|
|
|
|
"get-hierarchy": function (event) {
|
|
|
|
|
|
const scene = cc.director.getScene();
|
|
|
|
|
|
|
|
|
|
|
|
function dumpNodes(node) {
|
|
|
|
|
|
// 【优化】跳过编辑器内部的私有节点,减少数据量
|
|
|
|
|
|
if (node.name.startsWith("Editor Scene") || node.name === "gizmoRoot") {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let nodeData = {
|
|
|
|
|
|
name: node.name,
|
|
|
|
|
|
uuid: node.uuid,
|
|
|
|
|
|
active: node.active,
|
|
|
|
|
|
position: { x: Math.round(node.x), y: Math.round(node.y) },
|
|
|
|
|
|
scale: { x: node.scaleX, y: node.scaleY },
|
|
|
|
|
|
size: { width: node.width, height: node.height },
|
|
|
|
|
|
// 记录组件类型,让 AI 知道这是个什么节点
|
|
|
|
|
|
components: node._components.map((c) => c.__typename),
|
|
|
|
|
|
children: [],
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < node.childrenCount; i++) {
|
|
|
|
|
|
let childData = dumpNodes(node.children[i]);
|
|
|
|
|
|
if (childData) nodeData.children.push(childData);
|
|
|
|
|
|
}
|
|
|
|
|
|
return nodeData;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const hierarchy = dumpNodes(scene);
|
|
|
|
|
|
if (event.reply) event.reply(null, hierarchy);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 批量更新节点的变换信息 (坐标、缩放、颜色)
|
|
|
|
|
|
* @param {Object} event IPC 事件对象
|
|
|
|
|
|
* @param {Object} args 参数 (id, x, y, scaleX, scaleY, color)
|
|
|
|
|
|
*/
|
2026-02-03 19:55:51 +08:00
|
|
|
|
"update-node-transform": function (event, args) {
|
|
|
|
|
|
const { id, x, y, scaleX, scaleY, color } = args;
|
|
|
|
|
|
Editor.log(`[scene-script] update-node-transform called for ${id} with args: ${JSON.stringify(args)}`);
|
|
|
|
|
|
|
2026-02-07 23:14:12 +08:00
|
|
|
|
let node = findNode(id);
|
2026-02-03 19:55:51 +08:00
|
|
|
|
|
|
|
|
|
|
if (node) {
|
|
|
|
|
|
Editor.log(`[scene-script] Node found: ${node.name}, Current Pos: (${node.x}, ${node.y})`);
|
|
|
|
|
|
|
|
|
|
|
|
if (x !== undefined) {
|
2026-02-10 14:00:02 +08:00
|
|
|
|
node.x = Number(x);
|
2026-02-03 19:55:51 +08:00
|
|
|
|
Editor.log(`[scene-script] Set x to ${node.x}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (y !== undefined) {
|
|
|
|
|
|
node.y = Number(y);
|
|
|
|
|
|
Editor.log(`[scene-script] Set y to ${node.y}`);
|
|
|
|
|
|
}
|
2026-02-10 14:00:02 +08:00
|
|
|
|
// [新增] 支持设置节点宽高 (用于测试 9-slice 等需要调整尺寸的情况)
|
|
|
|
|
|
if (args.width !== undefined) {
|
|
|
|
|
|
node.width = Number(args.width);
|
|
|
|
|
|
Editor.log(`[scene-script] Set width to ${node.width}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (args.height !== undefined) {
|
|
|
|
|
|
node.height = Number(args.height);
|
|
|
|
|
|
Editor.log(`[scene-script] Set height to ${node.height}`);
|
|
|
|
|
|
}
|
2026-02-03 19:55:51 +08:00
|
|
|
|
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})`);
|
2026-02-07 22:29:17 +08:00
|
|
|
|
if (event.reply) event.reply(null, "变换信息已更新");
|
2026-02-03 19:55:51 +08:00
|
|
|
|
} else {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
if (event.reply) event.reply(new Error("找不到节点"));
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 在场景中创建新节点
|
|
|
|
|
|
* @param {Object} event IPC 事件对象
|
|
|
|
|
|
* @param {Object} args 参数 (name, parentId, type)
|
|
|
|
|
|
*/
|
2026-02-03 19:55:51 +08:00
|
|
|
|
"create-node": function (event, args) {
|
|
|
|
|
|
const { name, parentId, type } = args;
|
|
|
|
|
|
const scene = cc.director.getScene();
|
|
|
|
|
|
if (!scene) {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
if (event.reply) event.reply(new Error("场景尚未准备好或正在加载。"));
|
2026-02-03 19:55:51 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let newNode = null;
|
|
|
|
|
|
|
|
|
|
|
|
// 特殊处理:如果是创建 Canvas,自动设置好适配
|
|
|
|
|
|
if (type === "canvas" || name === "Canvas") {
|
|
|
|
|
|
newNode = new cc.Node("Canvas");
|
|
|
|
|
|
let canvas = newNode.addComponent(cc.Canvas);
|
|
|
|
|
|
newNode.addComponent(cc.Widget);
|
|
|
|
|
|
// 设置默认设计分辨率
|
|
|
|
|
|
canvas.designResolution = cc.size(960, 640);
|
|
|
|
|
|
canvas.fitHeight = true;
|
|
|
|
|
|
// 自动在 Canvas 下创建一个 Camera
|
|
|
|
|
|
let camNode = new cc.Node("Main Camera");
|
|
|
|
|
|
camNode.addComponent(cc.Camera);
|
|
|
|
|
|
camNode.parent = newNode;
|
|
|
|
|
|
} else if (type === "sprite") {
|
2026-02-07 23:14:12 +08:00
|
|
|
|
newNode = new cc.Node(name || "新建精灵节点");
|
|
|
|
|
|
let sprite = newNode.addComponent(cc.Sprite);
|
|
|
|
|
|
// 设置为 CUSTOM 模式
|
|
|
|
|
|
sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM;
|
|
|
|
|
|
// 为精灵设置默认尺寸
|
|
|
|
|
|
newNode.width = 100;
|
|
|
|
|
|
newNode.height = 100;
|
|
|
|
|
|
|
|
|
|
|
|
// 加载引擎默认图做占位
|
|
|
|
|
|
if (args.defaultSpriteUuid) {
|
|
|
|
|
|
cc.assetManager.loadAny(args.defaultSpriteUuid, (err, asset) => {
|
|
|
|
|
|
if (!err && (asset instanceof cc.SpriteFrame || asset instanceof cc.Texture2D)) {
|
|
|
|
|
|
sprite.spriteFrame = asset instanceof cc.SpriteFrame ? asset : new cc.SpriteFrame(asset);
|
|
|
|
|
|
Editor.Ipc.sendToMain("scene:dirty");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (type === "button") {
|
|
|
|
|
|
newNode = new cc.Node(name || "新建按钮节点");
|
|
|
|
|
|
let sprite = newNode.addComponent(cc.Sprite);
|
|
|
|
|
|
newNode.addComponent(cc.Button);
|
|
|
|
|
|
|
|
|
|
|
|
// 设置为 CUSTOM 模式并应用按钮专用尺寸
|
|
|
|
|
|
sprite.sizeMode = cc.Sprite.SizeMode.CUSTOM;
|
|
|
|
|
|
newNode.width = 150;
|
|
|
|
|
|
newNode.height = 50;
|
|
|
|
|
|
|
|
|
|
|
|
// 设置稍暗的背景颜色 (#A0A0A0),以便于看清其上的文字
|
|
|
|
|
|
newNode.color = new cc.Color(160, 160, 160);
|
|
|
|
|
|
|
|
|
|
|
|
// 加载引擎默认图
|
|
|
|
|
|
if (args.defaultSpriteUuid) {
|
|
|
|
|
|
cc.assetManager.loadAny(args.defaultSpriteUuid, (err, asset) => {
|
|
|
|
|
|
if (!err && (asset instanceof cc.SpriteFrame || asset instanceof cc.Texture2D)) {
|
|
|
|
|
|
sprite.spriteFrame = asset instanceof cc.SpriteFrame ? asset : new cc.SpriteFrame(asset);
|
|
|
|
|
|
Editor.Ipc.sendToMain("scene:dirty");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-02-03 19:55:51 +08:00
|
|
|
|
} else if (type === "label") {
|
2026-02-07 23:14:12 +08:00
|
|
|
|
newNode = new cc.Node(name || "新建文本节点");
|
2026-02-03 19:55:51 +08:00
|
|
|
|
let l = newNode.addComponent(cc.Label);
|
2026-02-07 23:14:12 +08:00
|
|
|
|
l.string = "新文本";
|
|
|
|
|
|
newNode.width = 120;
|
|
|
|
|
|
newNode.height = 40;
|
2026-02-03 19:55:51 +08:00
|
|
|
|
} else {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
newNode = new cc.Node(name || "新建节点");
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置层级
|
2026-02-07 23:14:12 +08:00
|
|
|
|
let parent = parentId ? findNode(parentId) : scene;
|
2026-02-03 19:55:51 +08:00
|
|
|
|
if (parent) {
|
|
|
|
|
|
newNode.parent = parent;
|
|
|
|
|
|
|
|
|
|
|
|
// 【优化】通知主进程场景变脏
|
|
|
|
|
|
Editor.Ipc.sendToMain("scene:dirty");
|
|
|
|
|
|
|
|
|
|
|
|
// 【关键】使用 setTimeout 延迟通知 UI 刷新,让出主循环
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
Editor.Ipc.sendToAll("scene:node-created", {
|
|
|
|
|
|
uuid: newNode.uuid,
|
|
|
|
|
|
parentUuid: parent.uuid,
|
|
|
|
|
|
});
|
|
|
|
|
|
}, 10);
|
|
|
|
|
|
|
|
|
|
|
|
if (event.reply) event.reply(null, newNode.uuid);
|
2026-02-07 23:14:12 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
if (event.reply) event.reply(new Error(`无法创建节点:找不到父节点 ${parentId}`));
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 管理节点上的组件 (添加、移除、更新属性)
|
|
|
|
|
|
* @param {Object} event IPC 事件对象
|
|
|
|
|
|
* @param {Object} args 参数 (nodeId, action, componentType, componentId, properties)
|
|
|
|
|
|
*/
|
2026-02-03 19:55:51 +08:00
|
|
|
|
"manage-components": function (event, args) {
|
|
|
|
|
|
const { nodeId, action, componentType, componentId, properties } = args;
|
2026-02-07 23:14:12 +08:00
|
|
|
|
let node = findNode(nodeId);
|
2026-02-03 19:55:51 +08:00
|
|
|
|
|
|
|
|
|
|
// 辅助函数:应用属性并智能解析
|
|
|
|
|
|
const applyProperties = (component, props) => {
|
|
|
|
|
|
if (!props) return;
|
|
|
|
|
|
// 尝试获取组件类的属性定义
|
|
|
|
|
|
const compClass = component.constructor;
|
|
|
|
|
|
|
|
|
|
|
|
for (const [key, value] of Object.entries(props)) {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
// 【核心修复】专门处理各类事件属性 (ClickEvents, ScrollEvents 等)
|
|
|
|
|
|
const isEventProp = Array.isArray(value) && (key.toLowerCase().endsWith('events') || key === 'clickEvents');
|
|
|
|
|
|
|
|
|
|
|
|
if (isEventProp) {
|
|
|
|
|
|
const eventHandlers = [];
|
|
|
|
|
|
for (const item of value) {
|
|
|
|
|
|
if (typeof item === 'object' && (item.target || item.component || item.handler)) {
|
|
|
|
|
|
const handler = new cc.Component.EventHandler();
|
|
|
|
|
|
|
|
|
|
|
|
// 解析 Target Node
|
|
|
|
|
|
if (item.target) {
|
2026-02-07 23:14:12 +08:00
|
|
|
|
let targetNode = findNode(item.target);
|
|
|
|
|
|
if (!targetNode && item.target instanceof cc.Node) {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
targetNode = item.target;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (targetNode) {
|
|
|
|
|
|
handler.target = targetNode;
|
|
|
|
|
|
Editor.log(`[scene-script] Resolved event target: ${targetNode.name}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (item.component) handler.component = item.component;
|
|
|
|
|
|
if (item.handler) handler.handler = item.handler;
|
|
|
|
|
|
if (item.customEventData !== undefined) handler.customEventData = String(item.customEventData);
|
|
|
|
|
|
|
|
|
|
|
|
eventHandlers.push(handler);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 如果不是对象,原样保留
|
|
|
|
|
|
eventHandlers.push(item);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
component[key] = eventHandlers;
|
|
|
|
|
|
continue; // 处理完事件数组,跳出本次循环
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-03 19:55:51 +08:00
|
|
|
|
// 检查属性是否存在
|
|
|
|
|
|
if (component[key] !== undefined) {
|
|
|
|
|
|
let finalValue = value;
|
|
|
|
|
|
|
2026-02-10 00:38:38 +08:00
|
|
|
|
// 【核心逻辑】智能类型识别与赋值
|
2026-02-03 19:55:51 +08:00
|
|
|
|
try {
|
2026-02-10 00:38:38 +08:00
|
|
|
|
const attrs = (cc.Class.Attr.getClassAttrs && cc.Class.Attr.getClassAttrs(compClass)) || {};
|
|
|
|
|
|
let propertyType = attrs[key] ? attrs[key].type : null;
|
|
|
|
|
|
if (!propertyType && attrs[key + '$_$ctor']) {
|
|
|
|
|
|
propertyType = attrs[key + '$_$ctor'];
|
|
|
|
|
|
}
|
2026-02-03 19:55:51 +08:00
|
|
|
|
|
2026-02-10 00:38:38 +08:00
|
|
|
|
let isAsset = propertyType && (propertyType.prototype instanceof cc.Asset || propertyType === cc.Asset || propertyType === cc.Prefab || propertyType === cc.SpriteFrame);
|
2026-02-10 09:14:50 +08:00
|
|
|
|
let isAssetArray = Array.isArray(value) && (key === 'materials' || key.toLowerCase().includes('assets'));
|
2026-02-10 00:38:38 +08:00
|
|
|
|
|
|
|
|
|
|
// 启发式:如果属性名包含 prefab/sprite/texture 等,且值为 UUID 且不是节点
|
2026-02-10 09:14:50 +08:00
|
|
|
|
if (!isAsset && !isAssetArray && typeof value === 'string' && value.length > 20) {
|
2026-02-10 00:38:38 +08:00
|
|
|
|
const lowerKey = key.toLowerCase();
|
|
|
|
|
|
const assetKeywords = ['prefab', 'sprite', 'texture', 'material', 'skeleton', 'spine', 'atlas', 'font', 'audio', 'data'];
|
|
|
|
|
|
if (assetKeywords.some(k => lowerKey.includes(k))) {
|
|
|
|
|
|
if (!findNode(value)) {
|
|
|
|
|
|
isAsset = true;
|
|
|
|
|
|
}
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 09:14:50 +08:00
|
|
|
|
if (isAsset || isAssetArray) {
|
|
|
|
|
|
// 1. 处理资源引用 (单个或数组)
|
|
|
|
|
|
const uuids = isAssetArray ? value : [value];
|
|
|
|
|
|
const loadedAssets = [];
|
|
|
|
|
|
let loadedCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
if (uuids.length === 0) {
|
|
|
|
|
|
component[key] = [];
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
uuids.forEach((uuid, idx) => {
|
|
|
|
|
|
if (typeof uuid !== 'string' || uuid.length < 10) {
|
|
|
|
|
|
loadedCount++;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
cc.AssetLibrary.loadAsset(uuid, (err, asset) => {
|
|
|
|
|
|
loadedCount++;
|
2026-02-10 00:38:38 +08:00
|
|
|
|
if (!err && asset) {
|
2026-02-10 09:14:50 +08:00
|
|
|
|
loadedAssets[idx] = asset;
|
|
|
|
|
|
Editor.log(`[scene-script] Successfully loaded asset for ${key}[${idx}]: ${asset.name}`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Editor.warn(`[scene-script] Failed to load asset ${uuid} for ${key}[${idx}]: ${err}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (loadedCount === uuids.length) {
|
|
|
|
|
|
if (isAssetArray) {
|
|
|
|
|
|
// 过滤掉加载失败的
|
|
|
|
|
|
component[key] = loadedAssets.filter(a => !!a);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (loadedAssets[0]) component[key] = loadedAssets[0];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-10 00:38:38 +08:00
|
|
|
|
// 通知编辑器 UI 更新
|
|
|
|
|
|
const compIndex = node._components.indexOf(component);
|
|
|
|
|
|
if (compIndex !== -1) {
|
|
|
|
|
|
Editor.Ipc.sendToPanel('scene', 'scene:set-property', {
|
|
|
|
|
|
id: node.uuid,
|
|
|
|
|
|
path: `_components.${compIndex}.${key}`,
|
2026-02-10 09:14:50 +08:00
|
|
|
|
type: isAssetArray ? 'Array' : 'Object',
|
|
|
|
|
|
value: isAssetArray ? uuids.map(u => ({ uuid: u })) : { uuid: value },
|
2026-02-10 00:38:38 +08:00
|
|
|
|
isSubProp: false
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
Editor.Ipc.sendToMain("scene:dirty");
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
2026-02-10 00:38:38 +08:00
|
|
|
|
});
|
2026-02-10 09:14:50 +08:00
|
|
|
|
});
|
2026-02-10 14:00:02 +08:00
|
|
|
|
// 【重要修复】使用 continue 而不是 return,确保处理完 Asset 属性后
|
|
|
|
|
|
// 还能继续处理后续的普通属性 (如 type, sizeMode 等)
|
|
|
|
|
|
continue;
|
2026-02-10 00:38:38 +08:00
|
|
|
|
} else if (propertyType && (propertyType.prototype instanceof cc.Component || propertyType === cc.Component || propertyType === cc.Node)) {
|
|
|
|
|
|
// 2. 处理节点或组件引用
|
|
|
|
|
|
const targetNode = findNode(value);
|
|
|
|
|
|
if (targetNode) {
|
|
|
|
|
|
if (propertyType === cc.Node) {
|
|
|
|
|
|
finalValue = targetNode;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const targetComp = targetNode.getComponent(propertyType);
|
|
|
|
|
|
if (targetComp) {
|
|
|
|
|
|
finalValue = targetComp;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Editor.warn(`[scene-script] Component ${propertyType.name} not found on node ${targetNode.name}`);
|
|
|
|
|
|
}
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
2026-02-10 00:38:38 +08:00
|
|
|
|
Editor.log(`[scene-script] Applied Reference for ${key}: ${targetNode.name}`);
|
|
|
|
|
|
} else if (value && value.length > 20) {
|
|
|
|
|
|
// 如果明确是组件/节点类型但找不到,才报错
|
|
|
|
|
|
Editor.warn(`[scene-script] Failed to resolve target node/comp for ${key}: ${value}`);
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
2026-02-10 00:38:38 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
// 3. 通用启发式 (找不到类型时的 fallback)
|
|
|
|
|
|
if (typeof value === 'string' && value.length > 20) {
|
|
|
|
|
|
const targetNode = findNode(value);
|
|
|
|
|
|
if (targetNode) {
|
|
|
|
|
|
finalValue = targetNode;
|
|
|
|
|
|
Editor.log(`[scene-script] Heuristic resolved Node for ${key}: ${targetNode.name}`);
|
2026-02-03 19:55:51 +08:00
|
|
|
|
} else {
|
2026-02-10 00:38:38 +08:00
|
|
|
|
// 找不到节点且是 UUID -> 视为资源
|
|
|
|
|
|
const compIndex = node._components.indexOf(component);
|
|
|
|
|
|
if (compIndex !== -1) {
|
|
|
|
|
|
Editor.Ipc.sendToPanel('scene', 'scene:set-property', {
|
|
|
|
|
|
id: node.uuid,
|
|
|
|
|
|
path: `_components.${compIndex}.${key}`,
|
|
|
|
|
|
type: 'Object',
|
|
|
|
|
|
value: { uuid: value },
|
|
|
|
|
|
isSubProp: false
|
|
|
|
|
|
});
|
|
|
|
|
|
Editor.log(`[scene-script] Heuristic resolved Asset via IPC for ${key}: ${value}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
2026-02-10 00:38:38 +08:00
|
|
|
|
Editor.warn(`[scene-script] Property resolution failed for ${key}: ${e.message}`);
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
component[key] = finalValue;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (!node) {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
if (event.reply) event.reply(new Error("找不到节点"));
|
2026-02-03 19:55:51 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
switch (action) {
|
|
|
|
|
|
case "add":
|
|
|
|
|
|
if (!componentType) {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
if (event.reply) event.reply(new Error("必须提供组件类型"));
|
2026-02-03 19:55:51 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 解析组件类型
|
|
|
|
|
|
let compClass = null;
|
|
|
|
|
|
if (componentType.startsWith("cc.")) {
|
|
|
|
|
|
const className = componentType.replace("cc.", "");
|
|
|
|
|
|
compClass = cc[className];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 尝试获取自定义组件
|
|
|
|
|
|
compClass = cc.js.getClassByName(componentType);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!compClass) {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
if (event.reply) event.reply(new Error(`找不到组件类型: ${componentType}`));
|
2026-02-03 19:55:51 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加组件
|
|
|
|
|
|
const component = node.addComponent(compClass);
|
|
|
|
|
|
|
|
|
|
|
|
// 设置属性
|
|
|
|
|
|
if (properties) {
|
|
|
|
|
|
applyProperties(component, properties);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Editor.Ipc.sendToMain("scene:dirty");
|
|
|
|
|
|
Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId });
|
|
|
|
|
|
|
2026-02-07 22:29:17 +08:00
|
|
|
|
if (event.reply) event.reply(null, `组件 ${componentType} 已添加`);
|
2026-02-03 19:55:51 +08:00
|
|
|
|
} catch (err) {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
if (event.reply) event.reply(new Error(`添加组件失败: ${err.message}`));
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "remove":
|
|
|
|
|
|
if (!componentId) {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
if (event.reply) event.reply(new Error("必须提供组件 ID"));
|
2026-02-03 19:55:51 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 查找并移除组件
|
|
|
|
|
|
let component = null;
|
|
|
|
|
|
if (node._components) {
|
|
|
|
|
|
for (let i = 0; i < node._components.length; i++) {
|
|
|
|
|
|
if (node._components[i].uuid === componentId) {
|
|
|
|
|
|
component = node._components[i];
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (component) {
|
|
|
|
|
|
node.removeComponent(component);
|
|
|
|
|
|
Editor.Ipc.sendToMain("scene:dirty");
|
|
|
|
|
|
Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId });
|
2026-02-07 22:29:17 +08:00
|
|
|
|
if (event.reply) event.reply(null, "组件已移除");
|
2026-02-03 19:55:51 +08:00
|
|
|
|
} else {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
if (event.reply) event.reply(new Error("找不到组件"));
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
if (event.reply) event.reply(new Error(`移除组件失败: ${err.message}`));
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "update":
|
|
|
|
|
|
// 更新现有组件属性
|
|
|
|
|
|
if (!componentType) {
|
|
|
|
|
|
// 如果提供了 componentId,可以只用 componentId
|
|
|
|
|
|
// 但 Cocos 2.4 uuid 获取组件比较麻烦,最好还是有 type 或者遍历
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
let targetComp = null;
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 尝试通过 componentId 查找
|
|
|
|
|
|
if (componentId) {
|
|
|
|
|
|
if (node._components) {
|
|
|
|
|
|
for (let i = 0; i < node._components.length; i++) {
|
|
|
|
|
|
if (node._components[i].uuid === componentId) {
|
|
|
|
|
|
targetComp = node._components[i];
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 尝试通过 type 查找
|
|
|
|
|
|
if (!targetComp && componentType) {
|
|
|
|
|
|
let compClass = null;
|
|
|
|
|
|
if (componentType.startsWith("cc.")) {
|
|
|
|
|
|
const className = componentType.replace("cc.", "");
|
|
|
|
|
|
compClass = cc[className];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
compClass = cc.js.getClassByName(componentType);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (compClass) {
|
|
|
|
|
|
targetComp = node.getComponent(compClass);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (targetComp) {
|
|
|
|
|
|
if (properties) {
|
|
|
|
|
|
applyProperties(targetComp, properties);
|
|
|
|
|
|
|
|
|
|
|
|
Editor.Ipc.sendToMain("scene:dirty");
|
|
|
|
|
|
Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId });
|
2026-02-11 00:36:56 +08:00
|
|
|
|
if (event.reply) event.reply(null, "组件属性已更新");
|
2026-02-03 19:55:51 +08:00
|
|
|
|
} else {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
if (event.reply) event.reply(null, "没有需要更新的属性");
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
if (event.reply) event.reply(new Error(`找不到组件 (类型: ${componentType}, ID: ${componentId})`));
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (err) {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
if (event.reply) event.reply(new Error(`更新组件失败: ${err.message}`));
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "get":
|
|
|
|
|
|
try {
|
|
|
|
|
|
const components = node._components.map((c) => {
|
|
|
|
|
|
// 获取组件属性
|
|
|
|
|
|
const properties = {};
|
|
|
|
|
|
for (const key in c) {
|
|
|
|
|
|
if (typeof c[key] !== "function" && !key.startsWith("_") && c[key] !== undefined) {
|
|
|
|
|
|
try {
|
2026-02-03 20:04:45 +08:00
|
|
|
|
// 安全序列化检查
|
2026-02-03 19:55:51 +08:00
|
|
|
|
const val = c[key];
|
|
|
|
|
|
if (val === null || val === undefined) {
|
|
|
|
|
|
properties[key] = val;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-03 20:04:45 +08:00
|
|
|
|
// 基础类型是安全的
|
2026-02-03 19:55:51 +08:00
|
|
|
|
if (typeof val !== 'object') {
|
|
|
|
|
|
properties[key] = val;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-03 20:04:45 +08:00
|
|
|
|
// 特殊 Cocos 类型
|
2026-02-03 19:55:51 +08:00
|
|
|
|
if (val instanceof cc.ValueType) {
|
|
|
|
|
|
properties[key] = val.toString();
|
|
|
|
|
|
} else if (val instanceof cc.Asset) {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
properties[key] = `资源(${val.name})`;
|
2026-02-03 19:55:51 +08:00
|
|
|
|
} else if (val instanceof cc.Node) {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
properties[key] = `节点(${val.name})`;
|
2026-02-03 19:55:51 +08:00
|
|
|
|
} else if (val instanceof cc.Component) {
|
2026-02-11 00:36:56 +08:00
|
|
|
|
properties[key] = `组件(${val.name}<${val.__typename}>)`;
|
2026-02-03 19:55:51 +08:00
|
|
|
|
} else {
|
2026-02-03 20:04:45 +08:00
|
|
|
|
// 数组和普通对象
|
|
|
|
|
|
// 尝试转换为纯 JSON 数据以避免 IPC 错误(如包含原生对象/循环引用)
|
2026-02-03 19:55:51 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const jsonStr = JSON.stringify(val);
|
2026-02-03 20:04:45 +08:00
|
|
|
|
// 确保不传递原始对象引用
|
2026-02-03 19:55:51 +08:00
|
|
|
|
properties[key] = JSON.parse(jsonStr);
|
|
|
|
|
|
} catch (e) {
|
2026-02-03 20:04:45 +08:00
|
|
|
|
// 如果 JSON 失败(例如循环引用),格式化为字符串
|
2026-02-11 00:36:56 +08:00
|
|
|
|
properties[key] = `[复杂对象: ${val.constructor ? val.constructor.name : typeof val}]`;
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
properties[key] = "[Serialization Error]";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
type: cc.js.getClassName(c) || c.constructor.name || "Unknown",
|
|
|
|
|
|
uuid: c.uuid,
|
|
|
|
|
|
properties: properties,
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
if (event.reply) event.reply(null, components);
|
|
|
|
|
|
} catch (err) {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
if (event.reply) event.reply(new Error(`获取组件失败: ${err.message}`));
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
default:
|
2026-02-07 22:29:17 +08:00
|
|
|
|
if (event.reply) event.reply(new Error(`未知的组件操作类型: ${action}`));
|
2026-02-03 19:55:51 +08:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
"get-component-properties": function (component) {
|
|
|
|
|
|
const properties = {};
|
|
|
|
|
|
|
|
|
|
|
|
// 遍历组件属性
|
|
|
|
|
|
for (const key in component) {
|
|
|
|
|
|
if (typeof component[key] !== "function" && !key.startsWith("_") && component[key] !== undefined) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
properties[key] = component[key];
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
// 忽略无法序列化的属性
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return properties;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
"instantiate-prefab": function (event, args) {
|
|
|
|
|
|
const { prefabUuid, parentId } = args;
|
|
|
|
|
|
const scene = cc.director.getScene();
|
|
|
|
|
|
|
|
|
|
|
|
if (!scene) {
|
|
|
|
|
|
if (event.reply) event.reply(new Error("Scene not ready or loading."));
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!prefabUuid) {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
if (event.reply) event.reply(new Error("必须提供预制体 UUID。"));
|
2026-02-03 19:55:51 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 使用 cc.assetManager.loadAny 通过 UUID 加载 (Cocos 2.4+)
|
|
|
|
|
|
// 如果是旧版,可能需要 cc.loader.load({uuid: ...}),但在 2.4 环境下 assetManager 更推荐
|
|
|
|
|
|
cc.assetManager.loadAny(prefabUuid, (err, prefab) => {
|
|
|
|
|
|
if (err) {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
if (event.reply) event.reply(new Error(`加载预制体失败: ${err.message}`));
|
2026-02-03 19:55:51 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 实例化预制体
|
|
|
|
|
|
const instance = cc.instantiate(prefab);
|
|
|
|
|
|
if (!instance) {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
if (event.reply) event.reply(new Error("实例化预制体失败"));
|
2026-02-03 19:55:51 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置父节点
|
|
|
|
|
|
let parent = parentId ? cc.engine.getInstanceById(parentId) : scene;
|
|
|
|
|
|
if (parent) {
|
|
|
|
|
|
instance.parent = parent;
|
|
|
|
|
|
|
|
|
|
|
|
// 通知场景变脏
|
|
|
|
|
|
Editor.Ipc.sendToMain("scene:dirty");
|
|
|
|
|
|
|
|
|
|
|
|
// 通知 UI 刷新
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
Editor.Ipc.sendToAll("scene:node-created", {
|
|
|
|
|
|
uuid: instance.uuid,
|
|
|
|
|
|
parentUuid: parent.uuid,
|
|
|
|
|
|
});
|
|
|
|
|
|
}, 10);
|
|
|
|
|
|
|
2026-02-07 22:29:17 +08:00
|
|
|
|
if (event.reply) event.reply(null, `预制体实例化成功,UUID: ${instance.uuid}`);
|
2026-02-03 19:55:51 +08:00
|
|
|
|
} else {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
if (event.reply) event.reply(new Error("找不到父节点"));
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 根据特定条件在场景中搜索节点
|
|
|
|
|
|
* @param {Object} event IPC 事件对象
|
|
|
|
|
|
* @param {Object} args 参数 (conditions, recursive)
|
|
|
|
|
|
*/
|
2026-02-03 19:55:51 +08:00
|
|
|
|
"find-gameobjects": function (event, args) {
|
|
|
|
|
|
const { conditions, recursive = true } = args;
|
|
|
|
|
|
const result = [];
|
|
|
|
|
|
const scene = cc.director.getScene();
|
|
|
|
|
|
|
|
|
|
|
|
function searchNode(node) {
|
|
|
|
|
|
// 跳过编辑器内部的私有节点
|
|
|
|
|
|
if (node.name.startsWith("Editor Scene") || node.name === "gizmoRoot") {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 检查节点是否满足条件
|
|
|
|
|
|
let match = true;
|
|
|
|
|
|
|
|
|
|
|
|
if (conditions.name && !node.name.includes(conditions.name)) {
|
|
|
|
|
|
match = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (conditions.component) {
|
|
|
|
|
|
let hasComponent = false;
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (conditions.component.startsWith("cc.")) {
|
|
|
|
|
|
const className = conditions.component.replace("cc.", "");
|
|
|
|
|
|
hasComponent = node.getComponent(cc[className]) !== null;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
hasComponent = node.getComponent(conditions.component) !== null;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
hasComponent = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!hasComponent) {
|
|
|
|
|
|
match = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (conditions.active !== undefined && node.active !== conditions.active) {
|
|
|
|
|
|
match = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (match) {
|
|
|
|
|
|
result.push({
|
|
|
|
|
|
uuid: node.uuid,
|
|
|
|
|
|
name: node.name,
|
|
|
|
|
|
active: node.active,
|
|
|
|
|
|
position: { x: node.x, y: node.y },
|
|
|
|
|
|
scale: { x: node.scaleX, y: node.scaleY },
|
|
|
|
|
|
size: { width: node.width, height: node.height },
|
|
|
|
|
|
components: node._components.map((c) => c.__typename),
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 递归搜索子节点
|
|
|
|
|
|
if (recursive) {
|
|
|
|
|
|
for (let i = 0; i < node.childrenCount; i++) {
|
|
|
|
|
|
searchNode(node.children[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 从场景根节点开始搜索
|
|
|
|
|
|
if (scene) {
|
|
|
|
|
|
searchNode(scene);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (event.reply) {
|
|
|
|
|
|
event.reply(null, result);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 删除指定的场景节点
|
|
|
|
|
|
* @param {Object} event IPC 事件对象
|
|
|
|
|
|
* @param {Object} args 参数 (uuid)
|
|
|
|
|
|
*/
|
2026-02-03 20:04:45 +08:00
|
|
|
|
"delete-node": function (event, args) {
|
|
|
|
|
|
const { uuid } = args;
|
2026-02-07 23:14:12 +08:00
|
|
|
|
const node = findNode(uuid);
|
2026-02-03 20:04:45 +08:00
|
|
|
|
if (node) {
|
|
|
|
|
|
const parent = node.parent;
|
|
|
|
|
|
node.destroy();
|
|
|
|
|
|
Editor.Ipc.sendToMain("scene:dirty");
|
|
|
|
|
|
// 延迟通知以确保节点已被移除
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
if (parent) {
|
|
|
|
|
|
Editor.Ipc.sendToAll("scene:node-changed", { uuid: parent.uuid });
|
|
|
|
|
|
}
|
|
|
|
|
|
// 广播节点删除事件
|
|
|
|
|
|
Editor.Ipc.sendToAll("scene:node-deleted", { uuid: uuid });
|
|
|
|
|
|
}, 10);
|
|
|
|
|
|
|
2026-02-07 22:29:17 +08:00
|
|
|
|
if (event.reply) event.reply(null, `节点 ${uuid} 已删除`);
|
2026-02-03 20:04:45 +08:00
|
|
|
|
} else {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
if (event.reply) event.reply(new Error(`找不到节点: ${uuid}`));
|
2026-02-03 20:04:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 管理高效的全场景特效 (粒子系统)
|
|
|
|
|
|
* @param {Object} event IPC 事件对象
|
|
|
|
|
|
* @param {Object} args 参数 (action, nodeId, properties, name, parentId)
|
|
|
|
|
|
*/
|
2026-02-03 19:55:51 +08:00
|
|
|
|
"manage-vfx": function (event, args) {
|
|
|
|
|
|
const { action, nodeId, properties, name, parentId } = args;
|
|
|
|
|
|
const scene = cc.director.getScene();
|
|
|
|
|
|
|
|
|
|
|
|
const applyParticleProperties = (particleSystem, props) => {
|
|
|
|
|
|
if (!props) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (props.duration !== undefined) particleSystem.duration = props.duration;
|
|
|
|
|
|
if (props.emissionRate !== undefined) particleSystem.emissionRate = props.emissionRate;
|
|
|
|
|
|
if (props.life !== undefined) particleSystem.life = props.life;
|
|
|
|
|
|
if (props.lifeVar !== undefined) particleSystem.lifeVar = props.lifeVar;
|
|
|
|
|
|
|
|
|
|
|
|
// 【关键修复】启用自定义属性,否则属性修改可能不生效
|
|
|
|
|
|
particleSystem.custom = true;
|
|
|
|
|
|
|
|
|
|
|
|
if (props.startColor) particleSystem.startColor = new cc.Color().fromHEX(props.startColor);
|
|
|
|
|
|
if (props.endColor) particleSystem.endColor = new cc.Color().fromHEX(props.endColor);
|
|
|
|
|
|
|
|
|
|
|
|
if (props.startSize !== undefined) particleSystem.startSize = props.startSize;
|
|
|
|
|
|
if (props.endSize !== undefined) particleSystem.endSize = props.endSize;
|
|
|
|
|
|
|
|
|
|
|
|
if (props.speed !== undefined) particleSystem.speed = props.speed;
|
|
|
|
|
|
if (props.angle !== undefined) particleSystem.angle = props.angle;
|
|
|
|
|
|
|
|
|
|
|
|
if (props.gravity) {
|
|
|
|
|
|
if (props.gravity.x !== undefined) particleSystem.gravity.x = props.gravity.x;
|
|
|
|
|
|
if (props.gravity.y !== undefined) particleSystem.gravity.y = props.gravity.y;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理文件/纹理加载
|
|
|
|
|
|
if (props.file) {
|
|
|
|
|
|
// main.js 已经将 db:// 路径转换为 UUID
|
|
|
|
|
|
// 如果用户直接传递 URL (http/https) 或其他格式,cc.assetManager.loadAny 也能处理
|
|
|
|
|
|
const uuid = props.file;
|
|
|
|
|
|
cc.assetManager.loadAny(uuid, (err, asset) => {
|
|
|
|
|
|
if (!err) {
|
|
|
|
|
|
if (asset instanceof cc.ParticleAsset) {
|
|
|
|
|
|
particleSystem.file = asset;
|
|
|
|
|
|
} else if (asset instanceof cc.Texture2D || asset instanceof cc.SpriteFrame) {
|
|
|
|
|
|
particleSystem.texture = asset;
|
|
|
|
|
|
}
|
|
|
|
|
|
Editor.Ipc.sendToMain("scene:dirty");
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
} else if (!particleSystem.texture && !particleSystem.file && args.defaultSpriteUuid) {
|
|
|
|
|
|
// 【关键修复】如果没有纹理,加载默认纹理 (UUID 由 main.js 传入)
|
|
|
|
|
|
Editor.log(`[mcp-bridge] Loading default texture with UUID: ${args.defaultSpriteUuid}`);
|
|
|
|
|
|
cc.assetManager.loadAny(args.defaultSpriteUuid, (err, asset) => {
|
|
|
|
|
|
if (err) {
|
|
|
|
|
|
Editor.error(`[mcp-bridge] Failed to load default texture: ${err.message}`);
|
|
|
|
|
|
} else if (asset instanceof cc.Texture2D || asset instanceof cc.SpriteFrame) {
|
|
|
|
|
|
Editor.log(`[mcp-bridge] Default texture loaded successfully.`);
|
|
|
|
|
|
particleSystem.texture = asset;
|
|
|
|
|
|
Editor.Ipc.sendToMain("scene:dirty");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Editor.warn(`[mcp-bridge] Loaded asset is not a texture: ${asset}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (action === "create") {
|
|
|
|
|
|
let newNode = new cc.Node(name || "New Particle");
|
|
|
|
|
|
let particleSystem = newNode.addComponent(cc.ParticleSystem);
|
|
|
|
|
|
|
|
|
|
|
|
// 设置默认值
|
|
|
|
|
|
particleSystem.resetSystem();
|
|
|
|
|
|
particleSystem.custom = true; // 确保新创建的也是 custom 模式
|
|
|
|
|
|
|
|
|
|
|
|
applyParticleProperties(particleSystem, properties);
|
|
|
|
|
|
|
|
|
|
|
|
let parent = parentId ? cc.engine.getInstanceById(parentId) : scene;
|
|
|
|
|
|
if (parent) {
|
|
|
|
|
|
newNode.parent = parent;
|
|
|
|
|
|
Editor.Ipc.sendToMain("scene:dirty");
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
Editor.Ipc.sendToAll("scene:node-created", {
|
|
|
|
|
|
uuid: newNode.uuid,
|
|
|
|
|
|
parentUuid: parent.uuid,
|
|
|
|
|
|
});
|
|
|
|
|
|
}, 10);
|
|
|
|
|
|
if (event.reply) event.reply(null, newNode.uuid);
|
|
|
|
|
|
} else {
|
2026-02-10 09:14:50 +08:00
|
|
|
|
if (event.reply) event.reply(new Error("找不到父节点"));
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} else if (action === "update") {
|
2026-02-07 23:14:12 +08:00
|
|
|
|
let node = findNode(nodeId);
|
2026-02-03 19:55:51 +08:00
|
|
|
|
if (node) {
|
|
|
|
|
|
let particleSystem = node.getComponent(cc.ParticleSystem);
|
|
|
|
|
|
if (!particleSystem) {
|
|
|
|
|
|
// 如果没有组件,自动添加
|
|
|
|
|
|
particleSystem = node.addComponent(cc.ParticleSystem);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
applyParticleProperties(particleSystem, properties);
|
|
|
|
|
|
|
|
|
|
|
|
Editor.Ipc.sendToMain("scene:dirty");
|
|
|
|
|
|
Editor.Ipc.sendToAll("scene:node-changed", { uuid: nodeId });
|
2026-02-07 22:29:17 +08:00
|
|
|
|
if (event.reply) event.reply(null, "特效已更新");
|
2026-02-03 19:55:51 +08:00
|
|
|
|
} else {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
if (event.reply) event.reply(new Error("找不到节点"));
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} else if (action === "get_info") {
|
2026-02-07 23:14:12 +08:00
|
|
|
|
let node = findNode(nodeId);
|
2026-02-03 19:55:51 +08:00
|
|
|
|
if (node) {
|
|
|
|
|
|
let ps = node.getComponent(cc.ParticleSystem);
|
|
|
|
|
|
if (ps) {
|
|
|
|
|
|
const info = {
|
|
|
|
|
|
duration: ps.duration,
|
|
|
|
|
|
emissionRate: ps.emissionRate,
|
|
|
|
|
|
life: ps.life,
|
|
|
|
|
|
lifeVar: ps.lifeVar,
|
|
|
|
|
|
startColor: ps.startColor.toHEX("#RRGGBB"),
|
|
|
|
|
|
endColor: ps.endColor.toHEX("#RRGGBB"),
|
|
|
|
|
|
startSize: ps.startSize,
|
|
|
|
|
|
endSize: ps.endSize,
|
|
|
|
|
|
speed: ps.speed,
|
|
|
|
|
|
angle: ps.angle,
|
|
|
|
|
|
gravity: { x: ps.gravity.x, y: ps.gravity.y },
|
|
|
|
|
|
file: ps.file ? ps.file.name : null
|
|
|
|
|
|
};
|
|
|
|
|
|
if (event.reply) event.reply(null, info);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (event.reply) event.reply(null, { hasParticleSystem: false });
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2026-02-10 09:14:50 +08:00
|
|
|
|
if (event.reply) event.reply(new Error("找不到节点"));
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
2026-02-07 22:29:17 +08:00
|
|
|
|
if (event.reply) event.reply(new Error(`未知的特效操作类型: ${action}`));
|
2026-02-03 19:55:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-02-04 01:57:12 +08:00
|
|
|
|
|
2026-02-07 23:14:12 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 控制节点的动画组件 (播放、暂停、停止等)
|
|
|
|
|
|
* @param {Object} event IPC 事件对象
|
|
|
|
|
|
* @param {Object} args 参数 (action, nodeId, clipName)
|
|
|
|
|
|
*/
|
2026-02-04 01:57:12 +08:00
|
|
|
|
"manage-animation": function (event, args) {
|
|
|
|
|
|
const { action, nodeId, clipName } = args;
|
2026-02-07 23:14:12 +08:00
|
|
|
|
const node = findNode(nodeId);
|
2026-02-04 01:57:12 +08:00
|
|
|
|
|
|
|
|
|
|
if (!node) {
|
2026-02-10 09:14:50 +08:00
|
|
|
|
if (event.reply) event.reply(new Error(`找不到节点: ${nodeId}`));
|
2026-02-04 01:57:12 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const anim = node.getComponent(cc.Animation);
|
|
|
|
|
|
if (!anim) {
|
2026-02-10 09:14:50 +08:00
|
|
|
|
if (event.reply) event.reply(new Error(`在节点 ${nodeId} 上找不到 Animation 组件`));
|
2026-02-04 01:57:12 +08:00
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
switch (action) {
|
|
|
|
|
|
case "get_list":
|
|
|
|
|
|
const clips = anim.getClips();
|
|
|
|
|
|
const clipList = clips.map(c => ({
|
|
|
|
|
|
name: c.name,
|
|
|
|
|
|
duration: c.duration,
|
|
|
|
|
|
sample: c.sample,
|
|
|
|
|
|
speed: c.speed,
|
|
|
|
|
|
wrapMode: c.wrapMode
|
|
|
|
|
|
}));
|
|
|
|
|
|
if (event.reply) event.reply(null, clipList);
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "get_info":
|
|
|
|
|
|
const currentClip = anim.currentClip;
|
|
|
|
|
|
let isPlaying = false;
|
|
|
|
|
|
// [安全修复] 只有在有当前 Clip 时才获取状态,避免 Animation 组件无 Clip 时的崩溃
|
|
|
|
|
|
if (currentClip) {
|
|
|
|
|
|
const state = anim.getAnimationState(currentClip.name);
|
|
|
|
|
|
if (state) {
|
|
|
|
|
|
isPlaying = state.isPlaying;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
const info = {
|
|
|
|
|
|
currentClip: currentClip ? currentClip.name : null,
|
|
|
|
|
|
clips: anim.getClips().map(c => c.name),
|
|
|
|
|
|
playOnLoad: anim.playOnLoad,
|
|
|
|
|
|
isPlaying: isPlaying
|
|
|
|
|
|
};
|
|
|
|
|
|
if (event.reply) event.reply(null, info);
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "play":
|
|
|
|
|
|
if (!clipName) {
|
|
|
|
|
|
anim.play();
|
2026-02-10 09:14:50 +08:00
|
|
|
|
if (event.reply) event.reply(null, "正在播放默认动画剪辑");
|
2026-02-04 01:57:12 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
anim.play(clipName);
|
2026-02-10 09:14:50 +08:00
|
|
|
|
if (event.reply) event.reply(null, `正在播放动画剪辑: ${clipName}`);
|
2026-02-04 01:57:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "stop":
|
|
|
|
|
|
anim.stop();
|
2026-02-10 09:14:50 +08:00
|
|
|
|
if (event.reply) event.reply(null, "动画已停止");
|
2026-02-04 01:57:12 +08:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "pause":
|
|
|
|
|
|
anim.pause();
|
2026-02-10 09:14:50 +08:00
|
|
|
|
if (event.reply) event.reply(null, "动画已暂停");
|
2026-02-04 01:57:12 +08:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
case "resume":
|
|
|
|
|
|
anim.resume();
|
2026-02-10 09:14:50 +08:00
|
|
|
|
if (event.reply) event.reply(null, "动画已恢复播放");
|
2026-02-04 01:57:12 +08:00
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
|
|
default:
|
2026-02-10 09:14:50 +08:00
|
|
|
|
if (event.reply) event.reply(new Error(`未知的动画操作类型: ${action}`));
|
2026-02-04 01:57:12 +08:00
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-01-29 13:47:38 +08:00
|
|
|
|
};
|