Bug fixes (verified against Cocos Creator 3.8.8): - diagnostics: shell:true for .cmd/.bat on Windows (T125/T126) - prefabs: duplicatePrefab target resolves under assets/ (T420) - cocos-project: add preview.open candidate for 3.8.8 (T444) - assets-advanced: detect directory assets in inspectAssetDependencies (T110) - scene: improve component-not-found errors with compilation hint (T429) Documentation improvements: - tool-registry: add enum and injected vars to execute_javascript, path format examples, asset ref limitation note - resources: add cocos://mcp/execute-context resource with variables, patterns, pitfalls New tools (core 37->38, full 101->110): - scene-management: create_scene, query_scene_state (core), copy_paste_node, rename_node, reparent_node - scripts: create_script with component/plain templates - prefabs: create_prefab - assets-advanced: batch_asset_ops, find_unused_assets
245 lines
8.3 KiB
JavaScript
245 lines
8.3 KiB
JavaScript
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { tryEditorRequests } = require('./cocos-project');
|
|
const { openAsset } = require('../assets');
|
|
|
|
const TEMPLATE_PATH = path.join(__dirname, '..', '..', 'resources', 'template.scene');
|
|
|
|
function buildSceneContent(sceneName, templatePath) {
|
|
const tplPath = templatePath || TEMPLATE_PATH;
|
|
let content;
|
|
try {
|
|
content = fs.readFileSync(tplPath, 'utf8');
|
|
} catch (err) {
|
|
throw new Error(`Failed to read scene template at ${tplPath}: ${err.message}`);
|
|
}
|
|
const json = JSON.parse(content);
|
|
if (json[0] && json[0]._name !== undefined) {
|
|
json[0]._name = sceneName;
|
|
}
|
|
if (json[1] && json[1]._name !== undefined) {
|
|
json[1]._name = sceneName;
|
|
}
|
|
return JSON.stringify(json);
|
|
}
|
|
|
|
function createSceneManagementTools({ createSchema, sceneBridge }) {
|
|
return [
|
|
{
|
|
name: 'create_scene',
|
|
profile: 'full',
|
|
description: '[core] Create a new scene asset in the Cocos project and optionally open it.',
|
|
inputSchema: createSchema(
|
|
{
|
|
sceneName: { type: 'string', description: 'Name of the new scene (without extension).' },
|
|
savePath: { type: 'string', description: 'Asset-db URL, e.g. db://assets/scenes/NewScene.scene.' },
|
|
open: { type: 'boolean', description: 'Whether to open the scene after creation. Default true.' },
|
|
overwrite: { type: 'boolean', description: 'Overwrite if the asset already exists. Default false.' },
|
|
},
|
|
['sceneName', 'savePath']
|
|
),
|
|
handler: async (args) => {
|
|
const sceneName = String(args.sceneName || '').trim();
|
|
const savePath = String(args.savePath || '').trim();
|
|
if (!sceneName) {
|
|
throw new Error('sceneName is required.');
|
|
}
|
|
if (!savePath) {
|
|
throw new Error('savePath is required.');
|
|
}
|
|
if (!savePath.endsWith('.scene')) {
|
|
throw new Error('savePath must end with .scene');
|
|
}
|
|
|
|
const content = buildSceneContent(sceneName);
|
|
const createArgs = [savePath, content];
|
|
if (args.overwrite === true) {
|
|
createArgs.push({ overwrite: true });
|
|
}
|
|
|
|
const result = await tryEditorRequests([
|
|
{ channel: 'asset-db', method: 'create-asset', args: createArgs },
|
|
]);
|
|
|
|
const url = (result.result && (result.result.url || result.result.source)) || savePath;
|
|
const uuid = result.result && result.result.uuid;
|
|
|
|
if (args.open !== false) {
|
|
try {
|
|
await openAsset(url);
|
|
} catch (_) {
|
|
// Opening is best-effort; creation succeeded.
|
|
}
|
|
}
|
|
|
|
return {
|
|
created: true,
|
|
sceneName,
|
|
url,
|
|
uuid,
|
|
opened: args.open !== false,
|
|
};
|
|
},
|
|
},
|
|
{
|
|
name: 'query_scene_state',
|
|
profile: 'core',
|
|
description: '[specialist] Query scene state: dirty (unsaved changes), ready, or soft-reload.',
|
|
inputSchema: createSchema(
|
|
{
|
|
action: {
|
|
type: 'string',
|
|
enum: ['is_dirty', 'is_ready', 'soft_reload'],
|
|
description: 'State query or action.',
|
|
},
|
|
},
|
|
['action']
|
|
),
|
|
handler: async (args) => {
|
|
const action = String(args.action || '').trim();
|
|
const IPC_MAP = {
|
|
is_dirty: [
|
|
{ channel: 'scene', method: 'query-dirty' },
|
|
],
|
|
is_ready: [
|
|
{ channel: 'scene', method: 'query-is-ready' },
|
|
],
|
|
soft_reload: [
|
|
{ channel: 'scene', method: 'soft-reload' },
|
|
],
|
|
};
|
|
const candidates = IPC_MAP[action];
|
|
if (!candidates) {
|
|
throw new Error(`Unknown action: ${action}. Must be one of: is_dirty, is_ready, soft_reload.`);
|
|
}
|
|
const result = await tryEditorRequests(candidates);
|
|
return {
|
|
action,
|
|
result: result.result,
|
|
};
|
|
},
|
|
},
|
|
{
|
|
name: 'copy_paste_node',
|
|
profile: 'full',
|
|
description: '[core] Copy, cut, or paste scene nodes via the Cocos editor clipboard.',
|
|
inputSchema: createSchema(
|
|
{
|
|
action: {
|
|
type: 'string',
|
|
enum: ['copy', 'cut', 'paste', 'duplicate'],
|
|
description: 'Clipboard or duplication action.',
|
|
},
|
|
uuids: {
|
|
type: 'array',
|
|
items: { type: 'string' },
|
|
description: 'Node UUIDs to copy, cut, or duplicate.',
|
|
},
|
|
target: { type: 'string', description: 'Target parent node UUID for paste.' },
|
|
keepWorldTransform: {
|
|
type: 'boolean',
|
|
description: 'Keep world transform on paste. Default false.',
|
|
},
|
|
},
|
|
['action']
|
|
),
|
|
handler: async (args) => {
|
|
const action = String(args.action || '').trim();
|
|
|
|
if (action === 'copy' || action === 'cut') {
|
|
const uuids = Array.isArray(args.uuids) ? args.uuids.filter(Boolean) : [];
|
|
if (uuids.length === 0) {
|
|
throw new Error(`uuids is required for action: ${action}.`);
|
|
}
|
|
const method = action === 'copy' ? 'copy-node' : 'cut-node';
|
|
const result = await tryEditorRequests([
|
|
{ channel: 'scene', method, args: [uuids] },
|
|
]);
|
|
return { action, uuids, result: result.result };
|
|
}
|
|
|
|
if (action === 'duplicate') {
|
|
const uuids = Array.isArray(args.uuids) ? args.uuids.filter(Boolean) : [];
|
|
if (uuids.length === 0) {
|
|
throw new Error('uuids is required for action: duplicate.');
|
|
}
|
|
const result = await tryEditorRequests([
|
|
{ channel: 'scene', method: 'duplicate-node', args: [uuids] },
|
|
]);
|
|
return { action, uuids, result: result.result };
|
|
}
|
|
|
|
if (action === 'paste') {
|
|
const target = String(args.target || '').trim();
|
|
if (!target) {
|
|
throw new Error('target is required for action: paste.');
|
|
}
|
|
const keepWorldTransform = args.keepWorldTransform === true;
|
|
const pasteOpts = { target, keepWorldTransform };
|
|
const uuids = Array.isArray(args.uuids) ? args.uuids.filter(Boolean) : [];
|
|
if (uuids.length > 0) {
|
|
pasteOpts.uuids = uuids;
|
|
}
|
|
const result = await tryEditorRequests([
|
|
{
|
|
channel: 'scene',
|
|
method: 'paste-node',
|
|
args: [pasteOpts],
|
|
},
|
|
]);
|
|
return { action, target, keepWorldTransform, result: result.result };
|
|
}
|
|
|
|
throw new Error(`Unknown action: ${action}. Must be one of: copy, cut, paste, duplicate.`);
|
|
},
|
|
},
|
|
{
|
|
name: 'rename_node',
|
|
profile: 'full',
|
|
description: '[core] Rename a scene node.',
|
|
inputSchema: createSchema(
|
|
{
|
|
path: { type: 'string', description: 'Node hierarchy path.' },
|
|
uuid: { type: 'string', description: 'Node UUID.' },
|
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
|
newName: { type: 'string', description: 'New name for the node.' },
|
|
},
|
|
['newName']
|
|
),
|
|
handler: async (args) => sceneBridge.call('renameNode', args),
|
|
},
|
|
{
|
|
name: 'reparent_node',
|
|
profile: 'full',
|
|
description: '[core] Move a node to a new parent in the scene hierarchy.',
|
|
inputSchema: createSchema(
|
|
{
|
|
path: { type: 'string', description: 'Node hierarchy path to move.' },
|
|
uuid: { type: 'string', description: 'Node UUID to move.' },
|
|
name: { type: 'string', description: 'Fallback exact node name.' },
|
|
targetPath: { type: 'string', description: 'New parent node path.' },
|
|
targetUuid: { type: 'string', description: 'New parent node UUID.' },
|
|
targetName: { type: 'string', description: 'Fallback exact new parent name.' },
|
|
siblingIndex: {
|
|
type: 'number',
|
|
description: 'Optional insert position among siblings. Default: append.',
|
|
},
|
|
keepWorldTransform: {
|
|
type: 'boolean',
|
|
description: 'Keep world transform after reparenting. Default false.',
|
|
},
|
|
},
|
|
[]
|
|
),
|
|
handler: async (args) => sceneBridge.call('reparentNode', args),
|
|
},
|
|
];
|
|
}
|
|
|
|
module.exports = {
|
|
buildSceneContent,
|
|
createSceneManagementTools,
|
|
};
|