Files
cocos-mcp/source/tools/node-tools.ts

1019 lines
43 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ToolDefinition, ToolResponse, ToolExecutor, NodeInfo } from '../types';
export class NodeTools implements ToolExecutor {
getTools(): ToolDefinition[] {
return [
{
name: 'create_node',
description: 'Create a new node in the scene. IMPORTANT: You should always provide parentUuid to specify where to create the node. If parentUuid is not provided, the node will be created at the scene root.',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Node name'
},
parentUuid: {
type: 'string',
description: 'Parent node UUID. STRONGLY RECOMMENDED: Always provide this parameter. Use get_current_scene or get_all_nodes to find parent UUIDs. If not provided, node will be created at scene root.'
},
nodeType: {
type: 'string',
description: 'Node type: Node, 2DNode, 3DNode',
enum: ['Node', '2DNode', '3DNode'],
default: 'Node'
},
siblingIndex: {
type: 'number',
description: 'Sibling index for ordering (-1 means append at end)',
default: -1
}
},
required: ['name']
}
},
{
name: 'get_node_info',
description: 'Get node information by UUID',
inputSchema: {
type: 'object',
properties: {
uuid: {
type: 'string',
description: 'Node UUID'
}
},
required: ['uuid']
}
},
{
name: 'find_nodes',
description: 'Find nodes by name pattern',
inputSchema: {
type: 'object',
properties: {
pattern: {
type: 'string',
description: 'Name pattern to search'
},
exactMatch: {
type: 'boolean',
description: 'Exact match or partial match',
default: false
}
},
required: ['pattern']
}
},
{
name: 'find_node_by_name',
description: 'Find first node by exact name',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Node name to find'
}
},
required: ['name']
}
},
{
name: 'get_all_nodes',
description: 'Get all nodes in the scene with their UUIDs',
inputSchema: {
type: 'object',
properties: {}
}
},
{
name: 'set_node_property',
description: 'Set node property value (prefer using set_node_transform for position/rotation/scale)',
inputSchema: {
type: 'object',
properties: {
uuid: {
type: 'string',
description: 'Node UUID'
},
property: {
type: 'string',
description: 'Property name (e.g., active, name, layer)'
},
value: {
description: 'Property value'
}
},
required: ['uuid', 'property', 'value']
}
},
{
name: 'set_node_transform',
description: 'Set node transform properties (position, rotation, scale) with unified interface. Automatically handles 2D/3D node differences.',
inputSchema: {
type: 'object',
properties: {
uuid: {
type: 'string',
description: 'Node UUID'
},
position: {
type: 'object',
properties: {
x: { type: 'number' },
y: { type: 'number' },
z: { type: 'number', description: 'Z coordinate (ignored for 2D nodes)' }
},
description: 'Node position. For 2D nodes, only x,y are used; z is ignored. For 3D nodes, all coordinates are used.'
},
rotation: {
type: 'object',
properties: {
x: { type: 'number', description: 'X rotation (ignored for 2D nodes)' },
y: { type: 'number', description: 'Y rotation (ignored for 2D nodes)' },
z: { type: 'number', description: 'Z rotation (main rotation axis for 2D nodes)' }
},
description: 'Node rotation in euler angles. For 2D nodes, only z rotation is used. For 3D nodes, all axes are used.'
},
scale: {
type: 'object',
properties: {
x: { type: 'number' },
y: { type: 'number' },
z: { type: 'number', description: 'Z scale (usually 1 for 2D nodes)' }
},
description: 'Node scale. For 2D nodes, z is typically 1. For 3D nodes, all axes are used.'
}
},
required: ['uuid']
}
},
{
name: 'delete_node',
description: 'Delete a node from scene',
inputSchema: {
type: 'object',
properties: {
uuid: {
type: 'string',
description: 'Node UUID to delete'
}
},
required: ['uuid']
}
},
{
name: 'move_node',
description: 'Move node to new parent',
inputSchema: {
type: 'object',
properties: {
nodeUuid: {
type: 'string',
description: 'Node UUID to move'
},
newParentUuid: {
type: 'string',
description: 'New parent node UUID'
},
siblingIndex: {
type: 'number',
description: 'Sibling index in new parent',
default: -1
}
},
required: ['nodeUuid', 'newParentUuid']
}
},
{
name: 'duplicate_node',
description: 'Duplicate a node',
inputSchema: {
type: 'object',
properties: {
uuid: {
type: 'string',
description: 'Node UUID to duplicate'
},
includeChildren: {
type: 'boolean',
description: 'Include children nodes',
default: true
}
},
required: ['uuid']
}
},
{
name: 'detect_node_type',
description: 'Detect if a node is 2D or 3D based on its components and properties',
inputSchema: {
type: 'object',
properties: {
uuid: {
type: 'string',
description: 'Node UUID to analyze'
}
},
required: ['uuid']
}
}
];
}
async execute(toolName: string, args: any): Promise<ToolResponse> {
switch (toolName) {
case 'create_node':
return await this.createNode(args);
case 'get_node_info':
return await this.getNodeInfo(args.uuid);
case 'find_nodes':
return await this.findNodes(args.pattern, args.exactMatch);
case 'find_node_by_name':
return await this.findNodeByName(args.name);
case 'get_all_nodes':
return await this.getAllNodes();
case 'set_node_property':
return await this.setNodeProperty(args.uuid, args.property, args.value);
case 'set_node_transform':
return await this.setNodeTransform(args);
case 'delete_node':
return await this.deleteNode(args.uuid);
case 'move_node':
return await this.moveNode(args.nodeUuid, args.newParentUuid, args.siblingIndex);
case 'duplicate_node':
return await this.duplicateNode(args.uuid, args.includeChildren);
case 'detect_node_type':
return await this.detectNodeType(args.uuid);
default:
throw new Error(`Unknown tool: ${toolName}`);
}
}
private async createNode(args: any): Promise<ToolResponse> {
return new Promise(async (resolve) => {
let targetParentUuid = args.parentUuid;
// 如果没有提供父节点UUID获取场景根节点
if (!targetParentUuid) {
try {
const sceneInfo = await Editor.Message.request('scene', 'query-node-tree');
if (sceneInfo && typeof sceneInfo === 'object' && !Array.isArray(sceneInfo) && Object.prototype.hasOwnProperty.call(sceneInfo, 'uuid')) {
targetParentUuid = (sceneInfo as any).uuid;
console.log(`No parent specified, using scene root: ${targetParentUuid}`);
} else if (Array.isArray(sceneInfo) && sceneInfo.length > 0 && sceneInfo[0].uuid) {
// 如果返回的是数组,使用第一个元素(通常是场景根节点)
targetParentUuid = sceneInfo[0].uuid;
console.log(`No parent specified, using scene root: ${targetParentUuid}`);
} else {
// 备用方案:尝试获取当前场景
const currentScene = await Editor.Message.request('scene', 'query-current-scene');
if (currentScene && currentScene.uuid) {
targetParentUuid = currentScene.uuid;
}
}
} catch (err) {
console.warn('Failed to get scene root, will use default behavior');
}
}
// 如果指定了父节点,先验证父节点是否存在
if (targetParentUuid) {
try {
const parentNode = await Editor.Message.request('scene', 'query-node', targetParentUuid);
if (!parentNode) {
resolve({
success: false,
error: `Parent node with UUID '${targetParentUuid}' not found`
});
return;
}
} catch (err) {
resolve({
success: false,
error: `Failed to verify parent node: ${err}`
});
return;
}
}
const nodeData: any = {
name: args.name,
type: args.nodeType || 'cc.Node'
};
// 使用正确的create-node API参数结构
if (targetParentUuid) {
const createNodeOptions = {
parent: targetParentUuid,
name: args.name,
components: args.nodeType && args.nodeType !== 'Node' ? [args.nodeType] : undefined
};
Editor.Message.request('scene', 'create-node', createNodeOptions).then((nodeUuid: any) => {
// 如果需要设置特定的兄弟索引使用set-parent API
// 添加延迟以避免内部状态竞争
if (args.siblingIndex !== undefined && args.siblingIndex >= 0 && nodeUuid) {
setTimeout(() => {
Editor.Message.request('scene', 'set-parent', {
parent: targetParentUuid,
uuids: [nodeUuid],
keepWorldTransform: false
}).then(() => {
resolve({
success: true,
data: {
uuid: nodeUuid,
name: args.name,
parentUuid: targetParentUuid,
message: args.parentUuid
? `Node '${args.name}' created under specified parent`
: `Node '${args.name}' created at scene root (no parent specified)`
}
});
}).catch(() => {
// 即使移动失败,节点已创建,返回成功但带警告
resolve({
success: true,
data: {
uuid: nodeUuid,
name: args.name,
message: `Node '${args.name}' created but may not be under intended parent`,
warning: 'Failed to move node to specified parent'
}
});
});
}, 100); // 100ms延迟
} else {
// Get complete node info for verification
this.getNodeInfo(nodeUuid).then((nodeInfo) => {
resolve({
success: true,
data: {
uuid: nodeUuid,
name: args.name,
message: `Node '${args.name}' created successfully`
},
verificationData: {
nodeInfo: nodeInfo.data,
creationDetails: {
parentUuid: targetParentUuid,
nodeType: args.nodeType || 'Node',
timestamp: new Date().toISOString()
}
}
});
}).catch(() => {
resolve({
success: true,
data: {
uuid: nodeUuid,
name: args.name,
message: `Node '${args.name}' created successfully (verification failed)`
}
});
});
}
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
} else {
// 没有找到场景根节点,使用默认行为(创建在场景根节点)
const createNodeOptions = {
name: args.name,
components: args.nodeType && args.nodeType !== 'Node' ? [args.nodeType] : undefined
};
Editor.Message.request('scene', 'create-node', createNodeOptions).then((result: any) => {
resolve({
success: true,
data: {
uuid: result,
name: args.name,
message: `Node '${args.name}' created at default location (scene root not found)`,
warning: 'Could not determine scene root, node created at default location'
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
}
});
}
private async getNodeInfo(uuid: string): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'query-node', uuid).then((nodeData: any) => {
if (!nodeData) {
resolve({
success: false,
error: 'Node not found or invalid response'
});
return;
}
// 根据实际返回的数据结构解析节点信息
const info: NodeInfo = {
uuid: nodeData.uuid?.value || uuid,
name: nodeData.name?.value || 'Unknown',
active: nodeData.active?.value !== undefined ? nodeData.active.value : true,
position: nodeData.position?.value || { x: 0, y: 0, z: 0 },
rotation: nodeData.rotation?.value || { x: 0, y: 0, z: 0 },
scale: nodeData.scale?.value || { x: 1, y: 1, z: 1 },
parent: nodeData.parent?.value?.uuid || null,
children: nodeData.children || [],
components: (nodeData.__comps__ || []).map((comp: any) => ({
type: comp.__type__ || 'Unknown',
enabled: comp.enabled !== undefined ? comp.enabled : true
})),
layer: nodeData.layer?.value || 1073741824,
mobility: nodeData.mobility?.value || 0
};
resolve({ success: true, data: info });
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async findNodes(pattern: string, exactMatch: boolean = false): Promise<ToolResponse> {
return new Promise((resolve) => {
// Note: 'query-nodes-by-name' API doesn't exist in official documentation
// Using tree traversal as primary approach
Editor.Message.request('scene', 'query-node-tree').then((tree: any) => {
const nodes: any[] = [];
const searchTree = (node: any, currentPath: string = '') => {
const nodePath = currentPath ? `${currentPath}/${node.name}` : node.name;
const matches = exactMatch ?
node.name === pattern :
node.name.toLowerCase().includes(pattern.toLowerCase());
if (matches) {
nodes.push({
uuid: node.uuid,
name: node.name,
path: nodePath
});
}
if (node.children) {
for (const child of node.children) {
searchTree(child, nodePath);
}
}
};
if (tree) {
searchTree(tree);
}
resolve({ success: true, data: nodes });
}).catch((err: Error) => {
// 备用方案:使用场景脚本
const options = {
name: 'cocos-mcp-server',
method: 'findNodes',
args: [pattern, exactMatch]
};
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
resolve(result);
}).catch((err2: Error) => {
resolve({ success: false, error: `Tree search failed: ${err.message}, Scene script failed: ${err2.message}` });
});
});
});
}
private async findNodeByName(name: string): Promise<ToolResponse> {
return new Promise((resolve) => {
// 优先尝试使用 Editor API 查询节点树并搜索
Editor.Message.request('scene', 'query-node-tree').then((tree: any) => {
const foundNode = this.searchNodeInTree(tree, name);
if (foundNode) {
resolve({
success: true,
data: {
uuid: foundNode.uuid,
name: foundNode.name,
path: this.getNodePath(foundNode)
}
});
} else {
resolve({ success: false, error: `Node '${name}' not found` });
}
}).catch((err: Error) => {
// 备用方案:使用场景脚本
const options = {
name: 'cocos-mcp-server',
method: 'findNodeByName',
args: [name]
};
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
resolve(result);
}).catch((err2: Error) => {
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
});
});
});
}
private searchNodeInTree(node: any, targetName: string): any {
if (node.name === targetName) {
return node;
}
if (node.children) {
for (const child of node.children) {
const found = this.searchNodeInTree(child, targetName);
if (found) {
return found;
}
}
}
return null;
}
private async getAllNodes(): Promise<ToolResponse> {
return new Promise((resolve) => {
// 尝试查询场景节点树
Editor.Message.request('scene', 'query-node-tree').then((tree: any) => {
const nodes: any[] = [];
const traverseTree = (node: any) => {
nodes.push({
uuid: node.uuid,
name: node.name,
type: node.type,
active: node.active,
path: this.getNodePath(node)
});
if (node.children) {
for (const child of node.children) {
traverseTree(child);
}
}
};
if (tree && tree.children) {
traverseTree(tree);
}
resolve({
success: true,
data: {
totalNodes: nodes.length,
nodes: nodes
}
});
}).catch((err: Error) => {
// 备用方案:使用场景脚本
const options = {
name: 'cocos-mcp-server',
method: 'getAllNodes',
args: []
};
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
resolve(result);
}).catch((err2: Error) => {
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
});
});
});
}
private getNodePath(node: any): string {
const path = [node.name];
let current = node.parent;
while (current && current.name !== 'Canvas') {
path.unshift(current.name);
current = current.parent;
}
return path.join('/');
}
private async setNodeProperty(uuid: string, property: string, value: any): Promise<ToolResponse> {
return new Promise((resolve) => {
// 尝试直接使用 Editor API 设置节点属性
Editor.Message.request('scene', 'set-property', {
uuid: uuid,
path: property,
dump: {
value: value
}
}).then(() => {
// Get comprehensive verification data including updated node info
this.getNodeInfo(uuid).then((nodeInfo) => {
resolve({
success: true,
message: `Property '${property}' updated successfully`,
data: {
nodeUuid: uuid,
property: property,
newValue: value
},
verificationData: {
nodeInfo: nodeInfo.data,
changeDetails: {
property: property,
value: value,
timestamp: new Date().toISOString()
}
}
});
}).catch(() => {
resolve({
success: true,
message: `Property '${property}' updated successfully (verification failed)`
});
});
}).catch((err: Error) => {
// 如果直接设置失败,尝试使用场景脚本
const options = {
name: 'cocos-mcp-server',
method: 'setNodeProperty',
args: [uuid, property, value]
};
Editor.Message.request('scene', 'execute-scene-script', options).then((result: any) => {
resolve(result);
}).catch((err2: Error) => {
resolve({ success: false, error: `Direct API failed: ${err.message}, Scene script failed: ${err2.message}` });
});
});
});
}
private async setNodeTransform(args: any): Promise<ToolResponse> {
return new Promise(async (resolve) => {
const { uuid, position, rotation, scale } = args;
const updatePromises: Promise<any>[] = [];
const updates: string[] = [];
const warnings: string[] = [];
try {
// First get node info to determine if it's 2D or 3D
const nodeInfoResponse = await this.getNodeInfo(uuid);
if (!nodeInfoResponse.success || !nodeInfoResponse.data) {
resolve({ success: false, error: 'Failed to get node information' });
return;
}
const nodeInfo = nodeInfoResponse.data;
const is2DNode = this.is2DNode(nodeInfo);
if (position) {
const normalizedPosition = this.normalizeTransformValue(position, 'position', is2DNode);
if (normalizedPosition.warning) {
warnings.push(normalizedPosition.warning);
}
updatePromises.push(
Editor.Message.request('scene', 'set-property', {
uuid: uuid,
path: 'position',
dump: { value: normalizedPosition.value }
})
);
updates.push('position');
}
if (rotation) {
const normalizedRotation = this.normalizeTransformValue(rotation, 'rotation', is2DNode);
if (normalizedRotation.warning) {
warnings.push(normalizedRotation.warning);
}
updatePromises.push(
Editor.Message.request('scene', 'set-property', {
uuid: uuid,
path: 'rotation',
dump: { value: normalizedRotation.value }
})
);
updates.push('rotation');
}
if (scale) {
const normalizedScale = this.normalizeTransformValue(scale, 'scale', is2DNode);
if (normalizedScale.warning) {
warnings.push(normalizedScale.warning);
}
updatePromises.push(
Editor.Message.request('scene', 'set-property', {
uuid: uuid,
path: 'scale',
dump: { value: normalizedScale.value }
})
);
updates.push('scale');
}
if (updatePromises.length === 0) {
resolve({ success: false, error: 'No transform properties specified' });
return;
}
await Promise.all(updatePromises);
// Verify the changes by getting updated node info
const updatedNodeInfo = await this.getNodeInfo(uuid);
const response: any = {
success: true,
message: `Transform properties updated: ${updates.join(', ')} ${is2DNode ? '(2D node)' : '(3D node)'}`,
updatedProperties: updates,
data: {
nodeUuid: uuid,
nodeType: is2DNode ? '2D' : '3D',
appliedChanges: updates,
transformConstraints: {
position: is2DNode ? 'x, y only (z ignored)' : 'x, y, z all used',
rotation: is2DNode ? 'z only (x, y ignored)' : 'x, y, z all used',
scale: is2DNode ? 'x, y main, z typically 1' : 'x, y, z all used'
}
},
verificationData: {
nodeInfo: updatedNodeInfo.data,
transformDetails: {
originalNodeType: is2DNode ? '2D' : '3D',
appliedTransforms: updates,
timestamp: new Date().toISOString()
},
beforeAfterComparison: {
before: nodeInfo,
after: updatedNodeInfo.data
}
}
};
if (warnings.length > 0) {
response.warning = warnings.join('; ');
}
resolve(response);
} catch (err: any) {
resolve({
success: false,
error: `Failed to update transform: ${err.message}`
});
}
});
}
private is2DNode(nodeInfo: any): boolean {
// Check if node has 2D-specific components or is under Canvas
const components = nodeInfo.components || [];
// Check for common 2D components
const has2DComponents = components.some((comp: any) =>
comp.type && (
comp.type.includes('cc.Sprite') ||
comp.type.includes('cc.Label') ||
comp.type.includes('cc.Button') ||
comp.type.includes('cc.Layout') ||
comp.type.includes('cc.Widget') ||
comp.type.includes('cc.Mask') ||
comp.type.includes('cc.Graphics')
)
);
if (has2DComponents) {
return true;
}
// Check for 3D-specific components
const has3DComponents = components.some((comp: any) =>
comp.type && (
comp.type.includes('cc.MeshRenderer') ||
comp.type.includes('cc.Camera') ||
comp.type.includes('cc.Light') ||
comp.type.includes('cc.DirectionalLight') ||
comp.type.includes('cc.PointLight') ||
comp.type.includes('cc.SpotLight')
)
);
if (has3DComponents) {
return false;
}
// Default heuristic: if z position is 0 and hasn't been changed, likely 2D
const position = nodeInfo.position;
if (position && Math.abs(position.z) < 0.001) {
return true;
}
// Default to 3D if uncertain
return false;
}
private normalizeTransformValue(value: any, type: 'position' | 'rotation' | 'scale', is2D: boolean): { value: any, warning?: string } {
const result = { ...value };
let warning: string | undefined;
if (is2D) {
switch (type) {
case 'position':
if (value.z !== undefined && Math.abs(value.z) > 0.001) {
warning = `2D node: z position (${value.z}) ignored, set to 0`;
result.z = 0;
} else if (value.z === undefined) {
result.z = 0;
}
break;
case 'rotation':
if ((value.x !== undefined && Math.abs(value.x) > 0.001) ||
(value.y !== undefined && Math.abs(value.y) > 0.001)) {
warning = `2D node: x,y rotations ignored, only z rotation applied`;
result.x = 0;
result.y = 0;
} else {
result.x = result.x || 0;
result.y = result.y || 0;
}
result.z = result.z || 0;
break;
case 'scale':
if (value.z === undefined) {
result.z = 1; // Default scale for 2D
}
break;
}
} else {
// 3D node - ensure all axes are defined
result.x = result.x !== undefined ? result.x : (type === 'scale' ? 1 : 0);
result.y = result.y !== undefined ? result.y : (type === 'scale' ? 1 : 0);
result.z = result.z !== undefined ? result.z : (type === 'scale' ? 1 : 0);
}
return { value: result, warning };
}
private async deleteNode(uuid: string): Promise<ToolResponse> {
return new Promise((resolve) => {
Editor.Message.request('scene', 'remove-node', { uuid: uuid }).then(() => {
resolve({
success: true,
message: 'Node deleted successfully'
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async moveNode(nodeUuid: string, newParentUuid: string, siblingIndex: number = -1): Promise<ToolResponse> {
return new Promise((resolve) => {
// Use correct set-parent API instead of move-node
Editor.Message.request('scene', 'set-parent', {
parent: newParentUuid,
uuids: [nodeUuid],
keepWorldTransform: false
}).then(() => {
resolve({
success: true,
message: 'Node moved successfully'
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async duplicateNode(uuid: string, includeChildren: boolean = true): Promise<ToolResponse> {
return new Promise((resolve) => {
// Note: includeChildren parameter is accepted for future use but not currently implemented
Editor.Message.request('scene', 'duplicate-node', uuid).then((result: any) => {
resolve({
success: true,
data: {
newUuid: result.uuid,
message: 'Node duplicated successfully'
}
});
}).catch((err: Error) => {
resolve({ success: false, error: err.message });
});
});
}
private async detectNodeType(uuid: string): Promise<ToolResponse> {
return new Promise(async (resolve) => {
try {
const nodeInfoResponse = await this.getNodeInfo(uuid);
if (!nodeInfoResponse.success || !nodeInfoResponse.data) {
resolve({ success: false, error: 'Failed to get node information' });
return;
}
const nodeInfo = nodeInfoResponse.data;
const is2D = this.is2DNode(nodeInfo);
const components = nodeInfo.components || [];
// Collect detection reasons
const detectionReasons: string[] = [];
// Check for 2D components
const twoDComponents = components.filter((comp: any) =>
comp.type && (
comp.type.includes('cc.Sprite') ||
comp.type.includes('cc.Label') ||
comp.type.includes('cc.Button') ||
comp.type.includes('cc.Layout') ||
comp.type.includes('cc.Widget') ||
comp.type.includes('cc.Mask') ||
comp.type.includes('cc.Graphics')
)
);
// Check for 3D components
const threeDComponents = components.filter((comp: any) =>
comp.type && (
comp.type.includes('cc.MeshRenderer') ||
comp.type.includes('cc.Camera') ||
comp.type.includes('cc.Light') ||
comp.type.includes('cc.DirectionalLight') ||
comp.type.includes('cc.PointLight') ||
comp.type.includes('cc.SpotLight')
)
);
if (twoDComponents.length > 0) {
detectionReasons.push(`Has 2D components: ${twoDComponents.map((c: any) => c.type).join(', ')}`);
}
if (threeDComponents.length > 0) {
detectionReasons.push(`Has 3D components: ${threeDComponents.map((c: any) => c.type).join(', ')}`);
}
// Check position for heuristic
const position = nodeInfo.position;
if (position && Math.abs(position.z) < 0.001) {
detectionReasons.push('Z position is ~0 (likely 2D)');
} else if (position && Math.abs(position.z) > 0.001) {
detectionReasons.push(`Z position is ${position.z} (likely 3D)`);
}
if (detectionReasons.length === 0) {
detectionReasons.push('No specific indicators found, defaulting based on heuristics');
}
resolve({
success: true,
data: {
nodeUuid: uuid,
nodeName: nodeInfo.name,
nodeType: is2D ? '2D' : '3D',
detectionReasons: detectionReasons,
components: components.map((comp: any) => ({
type: comp.type,
category: this.getComponentCategory(comp.type)
})),
position: nodeInfo.position,
transformConstraints: {
position: is2D ? 'x, y only (z ignored)' : 'x, y, z all used',
rotation: is2D ? 'z only (x, y ignored)' : 'x, y, z all used',
scale: is2D ? 'x, y main, z typically 1' : 'x, y, z all used'
}
}
});
} catch (err: any) {
resolve({
success: false,
error: `Failed to detect node type: ${err.message}`
});
}
});
}
private getComponentCategory(componentType: string): string {
if (!componentType) return 'unknown';
if (componentType.includes('cc.Sprite') || componentType.includes('cc.Label') ||
componentType.includes('cc.Button') || componentType.includes('cc.Layout') ||
componentType.includes('cc.Widget') || componentType.includes('cc.Mask') ||
componentType.includes('cc.Graphics')) {
return '2D';
}
if (componentType.includes('cc.MeshRenderer') || componentType.includes('cc.Camera') ||
componentType.includes('cc.Light') || componentType.includes('cc.DirectionalLight') ||
componentType.includes('cc.PointLight') || componentType.includes('cc.SpotLight')) {
return '3D';
}
return 'generic';
}
}