优化了组件属性的设置的接口,使AI调用起来更简单,更准确。每次更新场景信息、组件信息或者节点信息,端口将会返回最新的信息供AI进行追踪和比对。
This commit is contained in:
@@ -89,7 +89,7 @@ export class NodeTools implements ToolExecutor {
|
||||
},
|
||||
{
|
||||
name: 'set_node_property',
|
||||
description: 'Set node property value',
|
||||
description: 'Set node property value (prefer using set_node_transform for position/rotation/scale)',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@@ -99,7 +99,7 @@ export class NodeTools implements ToolExecutor {
|
||||
},
|
||||
property: {
|
||||
type: 'string',
|
||||
description: 'Property name (e.g., position, rotation, scale, active)'
|
||||
description: 'Property name (e.g., active, name, layer)'
|
||||
},
|
||||
value: {
|
||||
description: 'Property value'
|
||||
@@ -108,6 +108,47 @@ export class NodeTools implements ToolExecutor {
|
||||
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',
|
||||
@@ -163,6 +204,20 @@ export class NodeTools implements ToolExecutor {
|
||||
},
|
||||
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']
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -181,12 +236,16 @@ export class NodeTools implements ToolExecutor {
|
||||
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}`);
|
||||
}
|
||||
@@ -254,44 +313,67 @@ export class NodeTools implements ToolExecutor {
|
||||
|
||||
Editor.Message.request('scene', 'create-node', createNodeOptions).then((nodeUuid: any) => {
|
||||
// 如果需要设置特定的兄弟索引,使用set-parent API
|
||||
// 添加延迟以避免内部状态竞争
|
||||
if (args.siblingIndex !== undefined && args.siblingIndex >= 0 && nodeUuid) {
|
||||
Editor.Message.request('scene', 'set-parent', {
|
||||
parent: targetParentUuid,
|
||||
uuids: [nodeUuid],
|
||||
keepWorldTransform: false
|
||||
}).then(() => {
|
||||
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,
|
||||
parentUuid: targetParentUuid,
|
||||
message: args.parentUuid
|
||||
? `Node '${args.name}' created under specified parent`
|
||||
: `Node '${args.name}' created at scene root (no parent specified)`
|
||||
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 but may not be under intended parent`,
|
||||
warning: 'Failed to move node to specified parent'
|
||||
message: `Node '${args.name}' created successfully (verification failed)`
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
resolve({
|
||||
success: true,
|
||||
data: {
|
||||
uuid: nodeUuid,
|
||||
name: args.name,
|
||||
message: `Node '${args.name}' created successfully`
|
||||
}
|
||||
});
|
||||
}
|
||||
}).catch((err: Error) => {
|
||||
resolve({ success: false, error: err.message });
|
||||
@@ -527,9 +609,30 @@ export class NodeTools implements ToolExecutor {
|
||||
value: value
|
||||
}
|
||||
}).then(() => {
|
||||
resolve({
|
||||
success: true,
|
||||
message: `Property '${property}' updated successfully`
|
||||
// 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) => {
|
||||
// 如果直接设置失败,尝试使用场景脚本
|
||||
@@ -548,6 +651,215 @@ export class NodeTools implements ToolExecutor {
|
||||
});
|
||||
}
|
||||
|
||||
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(() => {
|
||||
@@ -595,4 +907,113 @@ export class NodeTools implements ToolExecutor {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user