Files
cc-3-8-x-mcp/cli/src/editor/index.js
T

177 lines
6.6 KiB
JavaScript

// ============================================================
// editor/index.js — 声明式批量编辑 prefab 主入口
//
// editPrefab(filePath, ops[], options?)
// - 内存内依次执行所有 op
// - 自动判别 stub vs 普通节点
// - 任一 op 失败抛错、不落盘
// - dryRun: 跑完不写盘,返回字段级 diff
// ============================================================
'use strict';
const { parsePrefab } = require('../parse.js');
const { writePrefab } = require('../write.js');
const { computeDiff } = require('./diff.js');
// 各 op handler
const { execSetPosition } = require('./ops/set-position.js');
const { execSetLabelText } = require('./ops/set-label-text.js');
const { execSetSpriteFrame } = require('./ops/set-sprite-frame.js');
const { execSetActive } = require('./ops/set-active.js');
const { execSetComponentField } = require('./ops/set-component-field.js');
const { execSetComponentEnabled } = require('./ops/set-component-enabled.js');
const { execSetAnchor } = require('./ops/set-anchor.js');
const { execSetSize } = require('./ops/set-size.js');
const { execAdjustPosition } = require('./ops/adjust-position.js');
const { execRenameNode } = require('./ops/rename-node.js');
const { execReparent } = require('./ops/reparent.js');
const { execReorderChildren } = require('./ops/reorder-children.js');
const { execAddNode } = require('./ops/add-node.js');
const { execRemoveNode } = require('./ops/remove-node.js');
const { execCloneNode } = require('./ops/clone-node.js');
const { execAddComponent } = require('./ops/add-component.js');
const { execRemoveComponent } = require('./ops/remove-component.js');
const { execSetComponentRef } = require('./ops/set-component-ref.js');
const { execSetNestedComponentField } = require('./ops/set-nested-component-field.js');
const { execBulkSet } = require('./ops/bulk-set.js');
const { execDedupeComponent } = require('./ops/dedupe-component.js');
const { execSetEditBox } = require('./ops/set-editbox.js');
const { execSetLabel } = require('./ops/set-label.js');
const { execSetButton } = require('./ops/set-button.js');
const { execSetLayout } = require('./ops/set-layout.js');
const { execSetRichText } = require('./ops/set-richtext.js');
const { execSetSprite } = require('./ops/set-sprite.js');
const { execSetNodeColor } = require('./ops/set-node-color.js');
const { execReplaceNestedPrefab } = require('./ops/replace-nested-prefab.js');
const { execAddNestedPrefab } = require('./ops/add-nested-prefab.js');
const { execAddSpineSocket } = require('./ops/add-spine-socket.js');
const { execResetOverrides } = require('./ops/reset-overrides.js');
const { execEnsureMeta } = require('./ops/ensure-meta.js');
const { execSyncNestedRoots } = require('./ops/sync-nested-roots.js');
const { validateOps } = require('./op-schema.js');
const OP_HANDLERS = {
'set-position': execSetPosition,
'set-label-text': execSetLabelText,
'set-sprite-frame': execSetSpriteFrame,
'set-active': execSetActive,
'set-component-field': execSetComponentField,
'set-component-enabled': execSetComponentEnabled,
'set-anchor': execSetAnchor,
'set-size': execSetSize,
'adjust-position': execAdjustPosition,
'rename-node': execRenameNode,
'reparent': execReparent,
'reorder-children': execReorderChildren,
'add-node': execAddNode,
'remove-node': execRemoveNode,
'clone-node': execCloneNode,
'add-component': execAddComponent,
'remove-component': execRemoveComponent,
'set-component-ref': execSetComponentRef,
'set-nested-component-field': execSetNestedComponentField,
'bulk-set': execBulkSet,
'dedupe-component': execDedupeComponent,
'set-editbox': execSetEditBox,
'set-label': execSetLabel,
'set-button': execSetButton,
'set-layout': execSetLayout,
'set-richtext': execSetRichText,
'set-sprite': execSetSprite,
'set-node-color': execSetNodeColor,
'replace-nested-prefab': execReplaceNestedPrefab,
'add-nested-prefab': execAddNestedPrefab,
'add-spine-socket': execAddSpineSocket,
'reset-overrides': execResetOverrides,
'ensure-meta': execEnsureMeta,
'sync-nested-roots': execSyncNestedRoots,
};
/**
* 声明式批量编辑 prefab
*
* @param {string} filePath prefab 文件路径(读取 + 写回同一路径)
* @param {object[]} ops op 描述数组
* @param {object} [options]
* @param {string} [options.projectRoot] 项目根目录(含 assets/),默认从 filePath 向上推断。
* @param {boolean} [options.dryRun] true 时不写盘,仅返回模拟结果(含 diff)。
* @returns {{ changed: boolean, opsApplied: number, nodesAffected: (string|number)[], dryRun?: boolean, diff?: object[] }}
*
* @throws 任一 op 失败时抛错,不落盘
*/
function editPrefab(filePath, ops, options) {
if (typeof filePath !== 'string') {
throw new Error('editPrefab: filePath 必须是字符串');
}
if (!Array.isArray(ops) || ops.length === 0) {
throw new Error('editPrefab: ops 必须是非空数组');
}
// schema 预校验:跑前发现拼错的字段(comp / ref / 拼漏 op 等),不到 handler 才报错
validateOps(ops, Object.keys(OP_HANDLERS));
const opts = options || {};
const prefabData = parsePrefab(filePath);
prefabData.resolverStartPath = opts.projectRoot || filePath;
const dryRun = !!opts.dryRun;
prefabData.dryRun = dryRun;
const beforeSnapshot = dryRun
? JSON.parse(JSON.stringify(prefabData.elements))
: null;
const affectedIds = new Set();
const affectedNames = [];
let opsApplied = 0;
for (const op of ops) {
if (!op || typeof op.op !== 'string') {
throw new Error(`editPrefab: op 格式错误(缺少 op 字段): ${JSON.stringify(op)}`);
}
const handler = OP_HANDLERS[op.op];
if (!handler) {
throw new Error(
`editPrefab: 不支持的 op 类型 "${op.op}",支持: ${Object.keys(OP_HANDLERS).join(', ')}`
);
}
const nodeId = handler(prefabData, op);
if (typeof nodeId === 'number' && nodeId >= 0) {
affectedIds.add(nodeId);
}
// bulk-set 0 匹配时返回 -1,跳过 affectedIds
opsApplied++;
}
if (!dryRun) {
writePrefab(filePath, prefabData.elements, prefabData.raw);
}
for (const id of affectedIds) {
const node = prefabData.elements[id];
if (node && node._name) {
affectedNames.push(node._name);
} else {
affectedNames.push(id);
}
}
const result = {
changed: !dryRun,
opsApplied,
nodesAffected: affectedNames,
};
if (dryRun) {
result.dryRun = true;
result.diff = computeDiff(beforeSnapshot, prefabData.elements);
}
return result;
}
module.exports = { editPrefab, OP_HANDLERS };