Files
mingyuansi b65a4f22c3 fix: Windows spawn, prefab path, preview API, asset deps, error messages; add execute-context resource, tool descriptions, scene/script/asset tools
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
2026-06-30 21:53:06 +08:00

1575 lines
44 KiB
JavaScript

'use strict';
module.paths.push(Editor.App.path + '/node_modules');
const cc = require('cc');
const {
Node,
director,
Vec3,
Quat,
Color,
assetManager,
instantiate,
Prefab,
SceneAsset,
js,
Component,
EventHandler,
Canvas,
UITransform,
Label,
Sprite,
Button,
Widget,
Camera,
Animation,
AnimationClip,
} = cc;
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
function getScene() {
const scene = director.getScene();
if (!scene) {
throw new Error('No active scene is loaded.');
}
return scene;
}
function getComponentNames(node) {
const components = Array.isArray(node.components) ? node.components : [];
return components
.map((component) => component && component.constructor && component.constructor.name)
.filter(Boolean);
}
function getSerializableKeys(target) {
const keys = new Set();
let current = target;
let depth = 0;
while (current && current !== Object.prototype && depth < 4) {
for (const key of Object.keys(current)) {
if (!key.startsWith('_')) {
keys.add(key);
}
}
current = Object.getPrototypeOf(current);
depth += 1;
}
return Array.from(keys);
}
function getNodePath(node) {
const names = [];
let current = node;
const scene = getScene();
while (current && current !== scene) {
names.unshift(current.name);
current = current.parent;
}
return names.join('/');
}
function vectorToObject(value) {
if (!value) {
return null;
}
return { x: value.x, y: value.y, z: value.z };
}
function quatToObject(value) {
if (!value) {
return null;
}
return { x: value.x, y: value.y, z: value.z, w: value.w };
}
function colorToObject(value) {
if (!value) {
return null;
}
return { r: value.r, g: value.g, b: value.b, a: value.a };
}
function summarizeNode(node, depth, maxDepth, includeComponents, includeInactive) {
if (!includeInactive && !node.active) {
return null;
}
const summary = {
name: node.name,
path: getNodePath(node),
uuid: node.uuid,
active: Boolean(node.active),
layer: node.layer,
position: vectorToObject(node.position),
rotation: quatToObject(node.rotation),
scale: vectorToObject(node.scale),
};
if (includeComponents) {
summary.components = getComponentNames(node);
}
if (depth < maxDepth) {
const children = [];
for (const child of node.children) {
const childSummary = summarizeNode(child, depth + 1, maxDepth, includeComponents, includeInactive);
if (childSummary) {
children.push(childSummary);
}
}
summary.children = children;
} else {
summary.childCount = node.children.length;
}
return summary;
}
function walkNodes(visitor, node) {
visitor(node);
for (const child of node.children) {
walkNodes(visitor, child);
}
}
function findNodeByPath(nodePath) {
if (!nodePath) {
return null;
}
const segments = String(nodePath)
.split('/')
.map((segment) => segment.trim())
.filter(Boolean);
let current = getScene();
if (segments[0] === current.name) {
segments.shift();
}
for (const segment of segments) {
current = current.children.find((child) => child.name === segment);
if (!current) {
return null;
}
}
return current;
}
function findNodeByUuid(uuid) {
if (!uuid) {
return null;
}
let match = null;
walkNodes((node) => {
if (!match && node.uuid === uuid) {
match = node;
}
}, getScene());
return match;
}
function findNodeByName(name) {
if (!name) {
return null;
}
let match = null;
walkNodes((node) => {
if (!match && node.name === name) {
match = node;
}
}, getScene());
return match;
}
function findNode(input) {
return findNodeByUuid(input.uuid) || findNodeByPath(input.path) || findNodeByName(input.name);
}
function resolveComponentClass(componentName) {
if (!componentName) {
return null;
}
if (typeof componentName !== 'string') {
return componentName;
}
const direct = js && typeof js.getClassByName === 'function' ? js.getClassByName(componentName) : null;
if (direct) {
return direct;
}
const candidates = [componentName, `cc.${componentName}`];
for (const candidate of candidates) {
const found = js && typeof js.getClassByName === 'function' ? js.getClassByName(candidate) : null;
if (found) {
return found;
}
}
return null;
}
function findComponent(node, options = {}) {
if (!node) {
return null;
}
if (Number.isInteger(options.index)) {
return node.components[options.index] || null;
}
if (options.componentName) {
const exact = node.components.find((component) => component && component.constructor && component.constructor.name === options.componentName);
if (exact) {
return exact;
}
const componentClass = resolveComponentClass(options.componentName);
if (componentClass) {
return node.getComponent(componentClass);
}
}
return null;
}
function getEventHandlerClass() {
return (Component && Component.EventHandler) || EventHandler || null;
}
function serializeEventHandler(handler) {
if (!handler) {
return null;
}
return {
target: handler.target && handler.target.name ? getNodePath(handler.target) : '',
targetUuid: handler.target && handler.target.uuid ? handler.target.uuid : '',
component: handler.component || '',
handler: handler.handler || '',
customEventData: handler.customEventData || '',
};
}
function getOrAddComponent(node, componentClass) {
return node.getComponent(componentClass) || node.addComponent(componentClass);
}
function configureNodeBasics(node, options = {}) {
if (options.position) {
node.setPosition(options.position.x || 0, options.position.y || 0, options.position.z || 0);
}
if (options.scale) {
node.setScale(options.scale.x || 1, options.scale.y || 1, options.scale.z || 1);
}
if (options.eulerAngles) {
node.setRotationFromEuler(
options.eulerAngles.x || 0,
options.eulerAngles.y || 0,
options.eulerAngles.z || 0
);
}
if (typeof options.active === 'boolean') {
node.active = options.active;
}
}
function configureUITransform(node, options = {}) {
const transform = getOrAddComponent(node, UITransform);
const width = Number.isFinite(options.width) ? options.width : 160;
const height = Number.isFinite(options.height) ? options.height : 60;
transform.setContentSize(width, height);
if (options.anchor) {
transform.setAnchorPoint(options.anchor.x ?? 0.5, options.anchor.y ?? 0.5);
}
return transform;
}
function parseColor(value, fallback = Color.WHITE) {
if (!value) {
return fallback.clone ? fallback.clone() : fallback;
}
if (typeof value === 'string') {
const normalized = value.startsWith('#') ? value.slice(1) : value;
const number = Number.parseInt(normalized.length === 6 ? `${normalized}ff` : normalized, 16);
if (Number.isFinite(number)) {
return new Color(
(number >> 24) & 255,
(number >> 16) & 255,
(number >> 8) & 255,
number & 255
);
}
}
return new Color(value.r ?? 255, value.g ?? 255, value.b ?? 255, value.a ?? 255);
}
function findComponentsByClass(componentClass) {
const results = [];
walkNodes((node) => {
if (node === getScene()) {
return;
}
const component = node.getComponent(componentClass);
if (component) {
results.push(component);
}
}, getScene());
return results;
}
function getPrefabInfo(node) {
const prefab = node && node._prefab;
if (!prefab) {
return {
linked: false,
};
}
const asset = prefab.asset || prefab._asset || null;
return {
linked: Boolean(asset || prefab.fileId || prefab.root),
fileId: prefab.fileId || '',
asset: asset
? {
name: asset.name || '',
uuid: asset.uuid || asset._uuid || '',
}
: null,
instance: prefab.instance ? plain(prefab.instance) : null,
sync: prefab.sync,
rawKeys: Object.keys(prefab).slice(0, 50),
};
}
function collectSceneStats() {
const stats = {
nodeCount: 0,
activeNodeCount: 0,
inactiveNodeCount: 0,
maxDepth: 0,
componentCount: 0,
componentsByType: {},
prefabInstanceCount: 0,
uiTransformCount: 0,
canvasCount: 0,
cameraCount: 0,
labelCount: 0,
spriteCount: 0,
buttonCount: 0,
};
function visit(node, depth) {
if (node !== getScene()) {
stats.nodeCount += 1;
stats.maxDepth = Math.max(stats.maxDepth, depth);
if (node.active) stats.activeNodeCount += 1;
else stats.inactiveNodeCount += 1;
if (node._prefab) stats.prefabInstanceCount += 1;
}
for (const component of node.components || []) {
const name = component && component.constructor ? component.constructor.name : 'UnknownComponent';
stats.componentCount += 1;
stats.componentsByType[name] = (stats.componentsByType[name] || 0) + 1;
if (component instanceof UITransform) stats.uiTransformCount += 1;
if (component instanceof Canvas) stats.canvasCount += 1;
if (component instanceof Camera) stats.cameraCount += 1;
if (component instanceof Label) stats.labelCount += 1;
if (component instanceof Sprite) stats.spriteCount += 1;
if (component instanceof Button) stats.buttonCount += 1;
}
for (const child of node.children) {
visit(child, depth + 1);
}
}
visit(getScene(), 0);
return stats;
}
function buildSceneWarnings(stats) {
const warnings = [];
if (stats.nodeCount === 0) {
warnings.push({ severity: 'warn', code: 'empty_scene', message: 'The active scene has no child nodes.' });
}
if (stats.cameraCount === 0) {
warnings.push({ severity: 'warn', code: 'missing_camera', message: 'No Camera component was found in the active scene.' });
}
if (stats.nodeCount > 500) {
warnings.push({ severity: 'info', code: 'large_node_count', message: `Scene has ${stats.nodeCount} nodes.` });
}
if (stats.maxDepth > 12) {
warnings.push({ severity: 'info', code: 'deep_hierarchy', message: `Scene hierarchy depth is ${stats.maxDepth}.` });
}
if (stats.labelCount > 80) {
warnings.push({ severity: 'info', code: 'many_labels', message: `Scene has ${stats.labelCount} Label components.` });
}
return warnings;
}
function getScheduler() {
return typeof director.getScheduler === 'function' ? director.getScheduler() : null;
}
function loadAnimationClipByUuid(uuid) {
return new Promise((resolve, reject) => {
assetManager.loadAny(uuid, (error, asset) => {
if (error) {
reject(error);
return;
}
if (!(asset instanceof AnimationClip)) {
reject(new Error(`Asset '${uuid}' is not an AnimationClip.`));
return;
}
resolve(asset);
});
});
}
function getValueByPath(target, propertyPath) {
const segments = String(propertyPath || '')
.split('.')
.map((segment) => segment.trim())
.filter(Boolean);
let current = target;
for (const segment of segments) {
if (current == null) {
return undefined;
}
current = current[segment];
}
return current;
}
function setValueByPath(target, propertyPath, value) {
const segments = String(propertyPath || '')
.split('.')
.map((segment) => segment.trim())
.filter(Boolean);
if (!segments.length) {
throw new Error('propertyPath is required.');
}
let current = target;
for (let index = 0; index < segments.length - 1; index += 1) {
const segment = segments[index];
if (current[segment] == null || typeof current[segment] !== 'object') {
current[segment] = {};
}
current = current[segment];
}
current[segments[segments.length - 1]] = value;
}
function resetValueByPath(target, propertyPath) {
const segments = String(propertyPath || '')
.split('.')
.map((segment) => segment.trim())
.filter(Boolean);
if (!segments.length) {
throw new Error('propertyPath is required.');
}
let current = target;
for (let index = 0; index < segments.length - 1; index += 1) {
current = current[segments[index]];
if (current == null) {
return;
}
}
const key = segments[segments.length - 1];
if (current && Object.prototype.hasOwnProperty.call(current, key)) {
delete current[key];
} else if (current) {
current[key] = undefined;
}
}
function loadAssetByUuid(uuid) {
return new Promise((resolve, reject) => {
assetManager.loadAny(uuid, (error, asset) => {
if (error) {
reject(error);
return;
}
resolve(asset);
});
});
}
function plain(value, depth = 0, seen = new WeakSet()) {
if (value == null) {
return value;
}
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return value;
}
if (value instanceof Node) {
return {
name: value.name,
path: getNodePath(value),
uuid: value.uuid,
active: Boolean(value.active),
components: getComponentNames(value),
};
}
if (value instanceof Vec3) {
return vectorToObject(value);
}
if (value instanceof Quat) {
return quatToObject(value);
}
if (value instanceof Color) {
return colorToObject(value);
}
if (Array.isArray(value)) {
if (depth >= 5) {
return `[Array(${value.length})]`;
}
return value.map((item) => plain(item, depth + 1, seen));
}
if (typeof value === 'object') {
if (seen.has(value)) {
return '[Circular]';
}
seen.add(value);
if (depth >= 5) {
return `[${value.constructor && value.constructor.name ? value.constructor.name : 'Object'}]`;
}
const output = {};
for (const key of Object.keys(value)) {
try {
output[key] = plain(value[key], depth + 1, seen);
} catch (error) {
output[key] = `[Unserializable: ${error.message}]`;
}
}
return output;
}
return String(value);
}
async function executeUserCode(code, args) {
const scene = getScene();
const runner = new AsyncFunction('require', 'cc', 'Editor', 'scene', 'director', 'args', `
const module = { exports: {} };
const exports = module.exports;
${code}
if (typeof run === 'function') {
return await run({ cc, Editor, scene, director, args });
}
if (typeof module.exports === 'function') {
return await module.exports({ cc, Editor, scene, director, args });
}
if (module.exports && typeof module.exports.run === 'function') {
return await module.exports.run({ cc, Editor, scene, director, args });
}
`);
return await runner(require, cc, global.Editor, scene, director, args || {});
}
exports.methods = {
async getSceneInfo(options = {}) {
const maxDepth = Number.isFinite(options.maxDepth) ? options.maxDepth : 2;
const includeComponents = options.includeComponents !== false;
const scene = getScene();
return {
sceneName: scene.name,
uuid: scene.uuid,
childCount: scene.children.length,
nodes: scene.children
.map((child) => summarizeNode(child, 1, Math.max(1, maxDepth), includeComponents, true))
.filter(Boolean),
};
},
async getHierarchy(options = {}) {
const root = options.rootPath ? findNodeByPath(options.rootPath) : getScene();
if (!root) {
throw new Error(`Node not found: ${options.rootPath}`);
}
const maxDepth = Number.isFinite(options.maxDepth) ? options.maxDepth : 3;
const includeComponents = options.includeComponents !== false;
const includeInactive = options.includeInactive !== false;
if (root === getScene()) {
return {
sceneName: root.name,
nodes: root.children
.map((child) => summarizeNode(child, 1, Math.max(1, maxDepth), includeComponents, includeInactive))
.filter(Boolean),
};
}
return summarizeNode(root, 0, Math.max(1, maxDepth), includeComponents, includeInactive);
},
async inspectNode(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Target node was not found.');
}
return {
name: node.name,
path: getNodePath(node),
uuid: node.uuid,
active: Boolean(node.active),
layer: node.layer,
siblingIndex: node.getSiblingIndex(),
position: vectorToObject(node.position),
rotation: quatToObject(node.rotation),
scale: vectorToObject(node.scale),
children: node.children.map((child) => ({
name: child.name,
path: getNodePath(child),
uuid: child.uuid,
})),
components: getComponentNames(node),
};
},
async findNodes(options = {}) {
const name = options.name ? String(options.name) : '';
const pathContains = options.pathContains ? String(options.pathContains) : '';
const component = options.component ? String(options.component) : '';
const includeInactive = options.includeInactive !== false;
const results = [];
walkNodes((node) => {
if (node === getScene()) {
return;
}
if (!includeInactive && !node.active) {
return;
}
const nodePath = getNodePath(node);
const components = getComponentNames(node);
if (name && node.name !== name) {
return;
}
if (pathContains && !nodePath.includes(pathContains)) {
return;
}
if (component && !components.includes(component)) {
return;
}
results.push({
name: node.name,
path: nodePath,
uuid: node.uuid,
active: Boolean(node.active),
components,
});
}, getScene());
return {
count: results.length,
nodes: results.slice(0, 200),
};
},
async createNode(options = {}) {
const name = String(options.name || '').trim();
if (!name) {
throw new Error('name is required.');
}
const parent = options.parentPath ? findNodeByPath(options.parentPath) : getScene();
if (!parent) {
throw new Error(`Parent not found: ${options.parentPath}`);
}
const node = new Node(name);
node.parent = parent;
if (options.position) {
node.setPosition(options.position.x || 0, options.position.y || 0, options.position.z || 0);
}
if (options.scale) {
node.setScale(options.scale.x || 1, options.scale.y || 1, options.scale.z || 1);
}
if (options.eulerAngles) {
node.setRotationFromEuler(
options.eulerAngles.x || 0,
options.eulerAngles.y || 0,
options.eulerAngles.z || 0
);
}
if (typeof options.active === 'boolean') {
node.active = options.active;
}
return {
created: true,
name: node.name,
path: getNodePath(node),
uuid: node.uuid,
};
},
async deleteNode(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Target node was not found.');
}
const targetPath = getNodePath(node);
node.removeFromParent();
node.destroy();
return {
deleted: true,
path: targetPath,
uuid: node.uuid,
};
},
async renameNode(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Target node was not found.');
}
const oldName = node.name;
const oldPath = getNodePath(node);
const newName = String(options.newName || '').trim();
if (!newName) {
throw new Error('newName is required.');
}
node.name = newName;
return {
renamed: true,
oldName,
newName,
oldPath,
newPath: getNodePath(node),
uuid: node.uuid,
};
},
async reparentNode(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Target node was not found.');
}
const oldPath = getNodePath(node);
const oldParent = node.parent;
const keepWorldTransform = options.keepWorldTransform === true;
const oldWorldPos = keepWorldTransform ? node.worldPosition.clone() : null;
const oldWorldRot = keepWorldTransform ? node.worldRotation.clone() : null;
const oldWorldScale = keepWorldTransform ? node.worldScale.clone() : null;
let newParent = null;
if (options.targetUuid) {
newParent = findNodeByUuid(options.targetUuid);
} else if (options.targetPath) {
newParent = findNodeByPath(options.targetPath);
} else if (options.targetName) {
newParent = findNodeByName(options.targetName);
}
if (!newParent) {
throw new Error('Target parent node was not found.');
}
const siblingIndex = Number.isFinite(options.siblingIndex) ? options.siblingIndex : -1;
if (siblingIndex >= 0 && siblingIndex <= newParent.children.length) {
newParent.insertChild(node, siblingIndex);
} else {
node.parent = newParent;
}
if (keepWorldTransform) {
node.worldPosition = oldWorldPos;
node.worldRotation = oldWorldRot;
node.worldScale = oldWorldScale;
}
return {
reparented: true,
uuid: node.uuid,
oldPath,
newPath: getNodePath(node),
oldParent: oldParent ? oldParent.name : null,
newParent: newParent.name,
siblingIndex: node.getSiblingIndex(),
keepWorldTransform,
};
},
async setNodeTransform(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Target node was not found.');
}
if (options.position) {
node.setPosition(options.position.x || 0, options.position.y || 0, options.position.z || 0);
}
if (options.scale) {
node.setScale(options.scale.x || 1, options.scale.y || 1, options.scale.z || 1);
}
if (options.eulerAngles) {
node.setRotationFromEuler(
options.eulerAngles.x || 0,
options.eulerAngles.y || 0,
options.eulerAngles.z || 0
);
}
if (typeof options.active === 'boolean') {
node.active = options.active;
}
return {
updated: true,
name: node.name,
path: getNodePath(node),
active: Boolean(node.active),
position: vectorToObject(node.position),
rotation: quatToObject(node.rotation),
scale: vectorToObject(node.scale),
};
},
async listComponents(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Target node was not found.');
}
return {
node: {
name: node.name,
path: getNodePath(node),
uuid: node.uuid,
},
components: node.components.map((component, index) => ({
index,
name: component && component.constructor ? component.constructor.name : 'UnknownComponent',
enabled: typeof component.enabled === 'boolean' ? component.enabled : undefined,
keys: getSerializableKeys(component).slice(0, 50),
})),
};
},
async addComponent(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Target node was not found.');
}
const componentClass = resolveComponentClass(options.componentName);
if (!componentClass) {
throw new Error(`Component class not found: ${options.componentName}`);
}
const component = node.addComponent(componentClass);
return {
added: true,
node: getNodePath(node),
component: component.constructor ? component.constructor.name : options.componentName,
index: node.components.indexOf(component),
};
},
async removeComponent(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Target node was not found.');
}
const component = findComponent(node, options);
if (!component) {
throw new Error(`Target component was not found${options.componentName ? ': ' + options.componentName : ''}. Ensure the script is compiled and the component is attached to the node.`);
}
const componentName = component.constructor ? component.constructor.name : 'UnknownComponent';
node.removeComponent(component);
return {
removed: true,
node: getNodePath(node),
component: componentName,
};
},
async inspectComponent(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Target node was not found.');
}
const component = findComponent(node, options);
if (!component) {
throw new Error(`Target component was not found${options.componentName ? ': ' + options.componentName : ''}. Ensure the script is compiled and the component is attached to the node.`);
}
return {
node: {
name: node.name,
path: getNodePath(node),
uuid: node.uuid,
},
component: {
name: component.constructor ? component.constructor.name : 'UnknownComponent',
enabled: typeof component.enabled === 'boolean' ? component.enabled : undefined,
data: plain(component),
},
};
},
async setComponentProperty(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Target node was not found.');
}
const component = findComponent(node, options);
if (!component) {
throw new Error(`Target component was not found${options.componentName ? ': ' + options.componentName : ''}. Ensure the script is compiled and the component is attached to the node.`);
}
setValueByPath(component, options.propertyPath, options.value);
return {
updated: true,
node: getNodePath(node),
component: component.constructor ? component.constructor.name : 'UnknownComponent',
propertyPath: options.propertyPath,
value: plain(getValueByPath(component, options.propertyPath)),
};
},
async resetComponentProperty(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Target node was not found.');
}
const component = findComponent(node, options);
if (!component) {
throw new Error(`Target component was not found${options.componentName ? ': ' + options.componentName : ''}. Ensure the script is compiled and the component is attached to the node.`);
}
resetValueByPath(component, options.propertyPath);
return {
reset: true,
node: getNodePath(node),
component: component.constructor ? component.constructor.name : 'UnknownComponent',
propertyPath: options.propertyPath,
value: plain(getValueByPath(component, options.propertyPath)),
};
},
async instantiatePrefab(options = {}) {
const prefabUuid = String(options.prefabUuid || '').trim();
if (!prefabUuid) {
throw new Error('prefabUuid is required.');
}
const parent = options.parentPath ? findNodeByPath(options.parentPath) : getScene();
if (!parent) {
throw new Error(`Parent not found: ${options.parentPath}`);
}
const asset = await loadAssetByUuid(prefabUuid);
if (!(asset instanceof Prefab)) {
throw new Error(`Asset '${prefabUuid}' is not a Prefab.`);
}
const node = instantiate(asset);
node.parent = parent;
if (options.name) {
node.name = options.name;
}
if (options.position) {
node.setPosition(options.position.x || 0, options.position.y || 0, options.position.z || 0);
}
return {
instantiated: true,
prefabUuid,
node: {
name: node.name,
path: getNodePath(node),
uuid: node.uuid,
},
};
},
async runSceneAsset(options = {}) {
const sceneUuid = String(options.sceneUuid || '').trim();
if (!sceneUuid) {
throw new Error('sceneUuid is required.');
}
const asset = await loadAssetByUuid(sceneUuid);
if (!(asset instanceof SceneAsset)) {
throw new Error(`Asset '${sceneUuid}' is not a SceneAsset.`);
}
director.runSceneImmediate(asset);
const scene = getScene();
return {
loaded: true,
sceneUuid,
sceneName: scene.name,
childCount: scene.children.length,
};
},
async createCanvas(options = {}) {
const parent = options.parentPath ? findNodeByPath(options.parentPath) : getScene();
if (!parent) {
throw new Error(`Parent not found: ${options.parentPath}`);
}
const node = new Node(options.name || 'Canvas');
node.parent = parent;
configureNodeBasics(node, options);
getOrAddComponent(node, Canvas);
configureUITransform(node, {
width: Number.isFinite(options.width) ? options.width : 1280,
height: Number.isFinite(options.height) ? options.height : 720,
});
return {
created: true,
name: node.name,
path: getNodePath(node),
uuid: node.uuid,
components: getComponentNames(node),
};
},
async createLabel(options = {}) {
const parent = options.parentPath ? findNodeByPath(options.parentPath) : getScene();
if (!parent) {
throw new Error(`Parent not found: ${options.parentPath}`);
}
const node = new Node(options.name || 'Label');
node.parent = parent;
configureNodeBasics(node, options);
configureUITransform(node, options);
const label = getOrAddComponent(node, Label);
label.string = options.text || 'Label';
label.fontSize = Number.isFinite(options.fontSize) ? options.fontSize : 32;
label.lineHeight = Number.isFinite(options.lineHeight) ? options.lineHeight : label.fontSize + 8;
label.color = parseColor(options.color, Color.WHITE);
return {
created: true,
path: getNodePath(node),
uuid: node.uuid,
text: label.string,
};
},
async createButton(options = {}) {
const parent = options.parentPath ? findNodeByPath(options.parentPath) : getScene();
if (!parent) {
throw new Error(`Parent not found: ${options.parentPath}`);
}
const node = new Node(options.name || 'Button');
node.parent = parent;
configureNodeBasics(node, options);
configureUITransform(node, {
...options,
width: Number.isFinite(options.width) ? options.width : 180,
height: Number.isFinite(options.height) ? options.height : 64,
});
const sprite = getOrAddComponent(node, Sprite);
sprite.color = parseColor(options.backgroundColor, new Color(64, 96, 255, 255));
const button = getOrAddComponent(node, Button);
button.target = node;
const labelNode = new Node(options.labelName || 'Label');
labelNode.parent = node;
configureUITransform(labelNode, {
width: Number.isFinite(options.width) ? options.width : 180,
height: Number.isFinite(options.height) ? options.height : 64,
});
const label = getOrAddComponent(labelNode, Label);
label.string = options.text || 'Button';
label.fontSize = Number.isFinite(options.fontSize) ? options.fontSize : 28;
label.lineHeight = Number.isFinite(options.lineHeight) ? options.lineHeight : label.fontSize + 8;
label.color = parseColor(options.textColor, Color.WHITE);
return {
created: true,
path: getNodePath(node),
uuid: node.uuid,
labelPath: getNodePath(labelNode),
components: getComponentNames(node),
};
},
async createSprite(options = {}) {
const parent = options.parentPath ? findNodeByPath(options.parentPath) : getScene();
if (!parent) {
throw new Error(`Parent not found: ${options.parentPath}`);
}
const node = new Node(options.name || 'Sprite');
node.parent = parent;
configureNodeBasics(node, options);
configureUITransform(node, options);
const sprite = getOrAddComponent(node, Sprite);
sprite.color = parseColor(options.color, Color.WHITE);
if (options.spriteFrameUuid) {
sprite.spriteFrame = await loadAssetByUuid(options.spriteFrameUuid);
}
return {
created: true,
path: getNodePath(node),
uuid: node.uuid,
components: getComponentNames(node),
};
},
async listCameras() {
const cameras = findComponentsByClass(Camera);
return {
count: cameras.length,
cameras: cameras.map((camera) => ({
node: camera.node ? getNodePath(camera.node) : '',
uuid: camera.node ? camera.node.uuid : '',
enabled: Boolean(camera.enabled),
priority: camera.priority,
projection: camera.projection,
visibility: camera.visibility,
clearFlags: camera.clearFlags,
})),
};
},
async createCamera(options = {}) {
const parent = options.parentPath ? findNodeByPath(options.parentPath) : getScene();
if (!parent) {
throw new Error(`Parent not found: ${options.parentPath}`);
}
const node = new Node(options.name || 'Camera');
node.parent = parent;
configureNodeBasics(node, options);
const camera = getOrAddComponent(node, Camera);
if (Number.isFinite(options.priority)) {
camera.priority = options.priority;
}
if (Number.isFinite(options.visibility)) {
camera.visibility = options.visibility;
}
if (Number.isFinite(options.clearFlags)) {
camera.clearFlags = options.clearFlags;
}
return {
created: true,
path: getNodePath(node),
uuid: node.uuid,
camera: plain(camera),
};
},
async setCameraProperties(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Target camera node was not found.');
}
const camera = node.getComponent(Camera);
if (!camera) {
throw new Error('Camera component was not found on target node.');
}
for (const key of ['priority', 'visibility', 'clearFlags', 'projection', 'orthoHeight', 'fov', 'near', 'far']) {
if (options[key] !== undefined) {
camera[key] = options[key];
}
}
return {
updated: true,
node: getNodePath(node),
camera: plain(camera),
};
},
async listAnimations(options = {}) {
const animations = options.path || options.uuid || options.name
? [findNode(options)].filter(Boolean).map((node) => node.getComponent(Animation)).filter(Boolean)
: findComponentsByClass(Animation);
return {
count: animations.length,
animations: animations.map((animation) => ({
node: animation.node ? getNodePath(animation.node) : '',
uuid: animation.node ? animation.node.uuid : '',
enabled: Boolean(animation.enabled),
defaultClip: animation.defaultClip ? animation.defaultClip.name : '',
clips: Array.isArray(animation.clips) ? animation.clips.map((clip) => clip && clip.name).filter(Boolean) : [],
})),
};
},
async addAnimationClip(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Target node was not found.');
}
const clipUuid = String(options.clipUuid || '').trim();
if (!clipUuid) {
throw new Error('clipUuid is required.');
}
const clip = await loadAnimationClipByUuid(clipUuid);
const animation = getOrAddComponent(node, Animation);
const clips = Array.isArray(animation.clips) ? animation.clips.slice() : [];
if (!clips.includes(clip)) {
clips.push(clip);
animation.clips = clips;
}
if (options.makeDefault !== false) {
animation.defaultClip = clip;
}
return {
added: true,
node: getNodePath(node),
clip: clip.name,
clipUuid,
clips: animation.clips.map((item) => item && item.name).filter(Boolean),
};
},
async playAnimation(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Target node was not found.');
}
const animation = node.getComponent(Animation);
if (!animation) {
throw new Error('Animation component was not found on target node.');
}
const state = options.clipName ? animation.play(options.clipName) : animation.play();
return {
playing: true,
node: getNodePath(node),
clip: state && state.clip ? state.clip.name : options.clipName || '',
};
},
async stopAnimation(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Target node was not found.');
}
const animation = node.getComponent(Animation);
if (!animation) {
throw new Error('Animation component was not found on target node.');
}
if (options.clipName) {
animation.stop(options.clipName);
} else {
animation.stop();
}
return {
stopped: true,
node: getNodePath(node),
clip: options.clipName || '(all)',
};
},
async getRuntimeState() {
const scheduler = getScheduler();
return {
sceneName: getScene().name,
paused: typeof director.isPaused === 'function' ? director.isPaused() : false,
timeScale: scheduler && typeof scheduler.getTimeScale === 'function' ? scheduler.getTimeScale() : 1,
totalFrames: typeof director.getTotalFrames === 'function' ? director.getTotalFrames() : undefined,
};
},
async getPerformanceSnapshot() {
const scheduler = getScheduler();
const stats = collectSceneStats();
const memory = typeof performance !== 'undefined' && performance.memory
? {
jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
totalJSHeapSize: performance.memory.totalJSHeapSize,
usedJSHeapSize: performance.memory.usedJSHeapSize,
}
: null;
return {
sceneName: getScene().name,
runtime: {
paused: typeof director.isPaused === 'function' ? director.isPaused() : false,
timeScale: scheduler && typeof scheduler.getTimeScale === 'function' ? scheduler.getTimeScale() : 1,
totalFrames: typeof director.getTotalFrames === 'function' ? director.getTotalFrames() : undefined,
},
stats,
memory,
warnings: buildSceneWarnings(stats),
};
},
async getPrefabInstanceInfo(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Target node was not found.');
}
return {
node: {
name: node.name,
path: getNodePath(node),
uuid: node.uuid,
},
prefab: getPrefabInfo(node),
};
},
async pauseRuntime() {
if (typeof director.pause === 'function') {
director.pause();
}
return await exports.methods.getRuntimeState();
},
async resumeRuntime() {
if (typeof director.resume === 'function') {
director.resume();
}
return await exports.methods.getRuntimeState();
},
async setTimeScale(options = {}) {
const scale = Number(options.scale);
if (!Number.isFinite(scale) || scale < 0 || scale > 100) {
throw new Error('scale must be a number between 0 and 100.');
}
const scheduler = getScheduler();
if (!scheduler || typeof scheduler.setTimeScale !== 'function') {
throw new Error('director scheduler time scale API is unavailable.');
}
scheduler.setTimeScale(scale);
return await exports.methods.getRuntimeState();
},
async emitNodeEvent(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Target node was not found.');
}
const eventName = String(options.eventName || '').trim();
if (!eventName) {
throw new Error('eventName is required.');
}
node.emit(eventName, options.payload || {});
return {
emitted: true,
node: getNodePath(node),
eventName,
};
},
async simulateButtonClick(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Target node was not found.');
}
const button = node.getComponent(Button);
if (!button) {
throw new Error('Button component was not found on target node.');
}
if (Component && Component.EventHandler && typeof Component.EventHandler.emitEvents === 'function') {
Component.EventHandler.emitEvents(button.clickEvents, button);
}
node.emit(Button.EventType ? Button.EventType.CLICK : 'click', button);
return {
clicked: true,
node: getNodePath(node),
clickEventCount: Array.isArray(button.clickEvents) ? button.clickEvents.length : 0,
};
},
async listButtonClickEvents(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Target node was not found.');
}
const button = node.getComponent(Button);
if (!button) {
throw new Error('Button component was not found on target node.');
}
return {
node: getNodePath(node),
uuid: node.uuid,
clickEventCount: Array.isArray(button.clickEvents) ? button.clickEvents.length : 0,
clickEvents: Array.isArray(button.clickEvents)
? button.clickEvents.map(serializeEventHandler).filter(Boolean)
: [],
};
},
async bindButtonClickEvent(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Button node was not found.');
}
const button = node.getComponent(Button);
if (!button) {
throw new Error('Button component was not found on target node.');
}
const target = findNode({
path: options.targetPath,
uuid: options.targetUuid,
name: options.targetName,
});
if (!target) {
throw new Error('Event target node was not found.');
}
const componentName = String(options.componentName || '').trim();
const handlerName = String(options.handler || options.handlerName || '').trim();
if (!componentName || !handlerName) {
throw new Error('componentName and handler are required.');
}
const component = findComponent(target, { componentName });
if (!component) {
throw new Error(`Target component was not found: ${componentName}. Ensure the script is compiled and the component is attached to the target node.`);
}
if (typeof component[handlerName] !== 'function') {
throw new Error(`Target component method was not found: ${componentName}.${handlerName}`);
}
const HandlerClass = getEventHandlerClass();
if (!HandlerClass) {
throw new Error('Cocos EventHandler class is unavailable.');
}
const existing = Array.isArray(button.clickEvents) ? button.clickEvents : [];
const duplicate = existing.find((event) => (
event &&
event.target === target &&
event.component === componentName &&
event.handler === handlerName &&
String(event.customEventData || '') === String(options.customEventData || '')
));
if (duplicate && options.replace !== true) {
return {
bound: false,
duplicate: true,
node: getNodePath(node),
event: serializeEventHandler(duplicate),
clickEventCount: existing.length,
};
}
const event = new HandlerClass();
event.target = target;
event.component = componentName;
event.handler = handlerName;
event.customEventData = String(options.customEventData || '');
button.clickEvents = options.replace === true
? existing.filter((item) => item !== duplicate).concat(event)
: existing.concat(event);
return {
bound: true,
node: getNodePath(node),
uuid: node.uuid,
event: serializeEventHandler(event),
clickEventCount: button.clickEvents.length,
};
},
async invokeComponentMethod(options = {}) {
const node = findNode(options);
if (!node) {
throw new Error('Target node was not found.');
}
const component = findComponent(node, options);
if (!component) {
throw new Error(`Target component was not found${options.componentName ? ': ' + options.componentName : ''}. Ensure the script is compiled and the component is attached to the node.`);
}
const methodName = String(options.methodName || '').trim();
if (!methodName || typeof component[methodName] !== 'function') {
throw new Error(`Component method not found: ${methodName}`);
}
const result = component[methodName](...(Array.isArray(options.args) ? options.args : []));
return {
invoked: true,
node: getNodePath(node),
component: component.constructor ? component.constructor.name : 'UnknownComponent',
methodName,
result: plain(result),
};
},
async executeCode(options = {}) {
const code = String(options.code || '');
if (!code.trim()) {
throw new Error('code is required.');
}
const result = await executeUserCode(code, options.args || {});
return {
ok: true,
result: plain(result),
sceneName: getScene().name,
};
},
};