3054 lines
106 KiB
JavaScript
Executable File
3054 lines
106 KiB
JavaScript
Executable File
"use strict";
|
||
var __create = Object.create;
|
||
var __defProp = Object.defineProperty;
|
||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||
var __getProtoOf = Object.getPrototypeOf;
|
||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||
var __export = (target, all) => {
|
||
for (var name in all)
|
||
__defProp(target, name, { get: all[name], enumerable: true });
|
||
};
|
||
var __copyProps = (to, from, except, desc) => {
|
||
if (from && typeof from === "object" || typeof from === "function") {
|
||
for (let key of __getOwnPropNames(from))
|
||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||
}
|
||
return to;
|
||
};
|
||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
||
// If the importer is in node compatibility mode or this is not an ESM
|
||
// file that has been converted to a CommonJS file using a Babel-
|
||
// compatible transform (i.e. "__esModule" has not been set), then set
|
||
// "default" to the CommonJS "module.exports" for node compatibility.
|
||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
||
mod
|
||
));
|
||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||
|
||
// packages/cocos-ext/src/scene.ts
|
||
var scene_exports = {};
|
||
__export(scene_exports, {
|
||
methods: () => methods
|
||
});
|
||
module.exports = __toCommonJS(scene_exports);
|
||
var path = __toESM(require("path"));
|
||
var COCOS_MCP_WIRE_PROTOCOL_VERSION = 1;
|
||
var uuidToNid = /* @__PURE__ */ new Map();
|
||
var nidToUuid = /* @__PURE__ */ new Map();
|
||
var detachedNodesForUndo = /* @__PURE__ */ new Map();
|
||
var DETACHED_NODE_STASH_MAX = 2048;
|
||
var nextNid = 1;
|
||
var currentSceneUuid = "";
|
||
var _modulePaths = module.paths;
|
||
try {
|
||
const EditorApp = globalThis.Editor?.App;
|
||
if (EditorApp?.path) {
|
||
const nm = path.join(EditorApp.path, "node_modules");
|
||
if (!_modulePaths.includes(nm)) {
|
||
_modulePaths.push(nm);
|
||
}
|
||
}
|
||
} catch {
|
||
}
|
||
function getCC() {
|
||
return require("cc");
|
||
}
|
||
function shortComponentName(comp, cc) {
|
||
try {
|
||
const full = cc.js?.getClassName?.(comp);
|
||
if (full && typeof full === "string") {
|
||
const parts2 = full.split(".");
|
||
return parts2[parts2.length - 1] || full;
|
||
}
|
||
} catch {
|
||
}
|
||
const raw = comp.name || comp.constructor?.name || "Component";
|
||
const parts = raw.split(".");
|
||
return parts[parts.length - 1] || raw;
|
||
}
|
||
function getOrCreateNid(uuid) {
|
||
let nid = uuidToNid.get(uuid);
|
||
if (nid === void 0) {
|
||
nid = nextNid++;
|
||
uuidToNid.set(uuid, nid);
|
||
nidToUuid.set(nid, uuid);
|
||
}
|
||
return nid;
|
||
}
|
||
function componentList(node, cc) {
|
||
let list = [];
|
||
try {
|
||
list = node.getComponents(cc.Component);
|
||
} catch {
|
||
return "";
|
||
}
|
||
const names = list.map((c) => shortComponentName(c, cc)).filter(Boolean);
|
||
return names.join(",");
|
||
}
|
||
function getCcValueCtors() {
|
||
return require("cc");
|
||
}
|
||
function getCcAssetManager() {
|
||
const cc = require("cc");
|
||
if (!cc.assetManager) {
|
||
throw new Error("cc.assetManager is unavailable in this runtime.");
|
||
}
|
||
return cc.assetManager;
|
||
}
|
||
function runtimeDirectSnapshot(value) {
|
||
if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
||
return value;
|
||
}
|
||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||
return void 0;
|
||
}
|
||
const record = value;
|
||
const copyNumbers = (keys) => {
|
||
const output = {};
|
||
for (const key of keys) {
|
||
const item = record[key];
|
||
if (typeof item !== "number" || !Number.isFinite(item)) {
|
||
return void 0;
|
||
}
|
||
output[key] = item;
|
||
}
|
||
return output;
|
||
};
|
||
return copyNumbers(["r", "g", "b", "a"]) ?? copyNumbers(["width", "height"]) ?? copyNumbers(["x", "y"]);
|
||
}
|
||
function materializeRuntimeDirectValue(value) {
|
||
if (value === null || typeof value !== "object") {
|
||
return value;
|
||
}
|
||
const cc = getCcValueCtors();
|
||
if ("r" in value && "g" in value && "b" in value && "a" in value) {
|
||
return new cc.Color(value.r, value.g, value.b, value.a);
|
||
}
|
||
if ("width" in value && "height" in value) {
|
||
return new cc.Size(value.width, value.height);
|
||
}
|
||
if ("x" in value && "y" in value) {
|
||
return new cc.Vec2(value.x, value.y);
|
||
}
|
||
return value;
|
||
}
|
||
function assetClassName(asset, cc) {
|
||
if (!asset || typeof asset !== "object") {
|
||
return "";
|
||
}
|
||
try {
|
||
const full = cc.js?.getClassName?.(asset);
|
||
if (typeof full === "string" && full.length > 0) {
|
||
return full;
|
||
}
|
||
} catch {
|
||
}
|
||
const constructor = asset.constructor;
|
||
return typeof constructor?.name === "string" ? constructor.name : "";
|
||
}
|
||
function validateAssetKind(asset, componentType, property, cc) {
|
||
const className = assetClassName(asset, cc).toLowerCase();
|
||
const normalizedProperty = property.trim().replace(/^_/, "").toLowerCase();
|
||
if (normalizedProperty === "spriteframe" && !className.includes("spriteframe")) {
|
||
return `Loaded asset is ${className || "unknown"}, expected SpriteFrame for ${componentType}.${property}.`;
|
||
}
|
||
if (normalizedProperty === "font" && !className.includes("font")) {
|
||
return `Loaded asset is ${className || "unknown"}, expected Font for ${componentType}.${property}.`;
|
||
}
|
||
return null;
|
||
}
|
||
function readAssetReferenceUuid(value, label) {
|
||
if (value === null || value === void 0) {
|
||
return { ok: true, uuid: null };
|
||
}
|
||
if (typeof value === "object" && !Array.isArray(value)) {
|
||
const uuid = value.uuid;
|
||
if (typeof uuid === "string" && uuid.trim().length > 0) {
|
||
return { ok: true, uuid: uuid.trim() };
|
||
}
|
||
}
|
||
return {
|
||
ok: false,
|
||
message: `${label} contains a non-null asset reference whose UUID cannot be observed safely.`
|
||
};
|
||
}
|
||
async function loadCcAssetByUuid(assetManager, uuid) {
|
||
const cached = assetManager.assets?.get?.(uuid);
|
||
if (cached) {
|
||
return cached;
|
||
}
|
||
if (typeof assetManager.loadAny !== "function") {
|
||
throw new Error("assetManager.loadAny is unavailable in this runtime.");
|
||
}
|
||
return await new Promise((resolve, reject) => {
|
||
let settled = false;
|
||
const timer = setTimeout(() => {
|
||
if (!settled) {
|
||
settled = true;
|
||
reject(new Error(`Timed out loading asset UUID ${uuid}.`));
|
||
}
|
||
}, 5e3);
|
||
const finish = (error, loaded) => {
|
||
if (settled) {
|
||
return;
|
||
}
|
||
settled = true;
|
||
clearTimeout(timer);
|
||
if (error) {
|
||
reject(error);
|
||
return;
|
||
}
|
||
const candidate = Array.isArray(loaded) ? loaded.find((item) => {
|
||
if (!item || typeof item !== "object") return false;
|
||
return item.uuid === uuid;
|
||
}) ?? loaded[0] : loaded;
|
||
if (!candidate) {
|
||
reject(new Error(`Asset manager returned no asset for UUID ${uuid}.`));
|
||
return;
|
||
}
|
||
resolve(candidate);
|
||
};
|
||
try {
|
||
assetManager.loadAny?.call(assetManager, uuid, finish);
|
||
} catch (error) {
|
||
finish(error instanceof Error ? error : new Error(String(error)));
|
||
}
|
||
});
|
||
}
|
||
function coerceNonNegativeNumber(val, prop) {
|
||
const n = typeof val === "number" ? val : Number(val);
|
||
if (!Number.isFinite(n)) {
|
||
throw new Error(`${prop} \u5FC5\u987B\u662F\u6709\u9650\u6570\u5B57\u3002`);
|
||
}
|
||
if (n < 0) {
|
||
throw new Error(`${prop} \u5FC5\u987B\u662F\u975E\u8D1F\u6570\u3002`);
|
||
}
|
||
return n;
|
||
}
|
||
function coerceFiniteNumber(val, prop) {
|
||
const n = typeof val === "number" ? val : Number(val);
|
||
if (!Number.isFinite(n)) {
|
||
throw new Error(`${prop} \u5FC5\u987B\u662F\u6709\u9650\u6570\u5B57\u3002`);
|
||
}
|
||
return n;
|
||
}
|
||
function coerceBoolean(val, prop) {
|
||
if (typeof val !== "boolean") {
|
||
throw new Error(`${prop} \u5FC5\u987B\u662F\u5E03\u5C14\u503C\u3002`);
|
||
}
|
||
return val;
|
||
}
|
||
function coerceString(val, prop, maxLen = 8192) {
|
||
if (typeof val !== "string") {
|
||
throw new Error(`${prop} \u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u3002`);
|
||
}
|
||
if (val.length > maxLen) {
|
||
throw new Error(`${prop} \u957F\u5EA6\u4E0D\u80FD\u8D85\u8FC7 ${maxLen}\u3002`);
|
||
}
|
||
return val;
|
||
}
|
||
function coerceEnumString(val, prop, mapping) {
|
||
if (typeof val !== "string") {
|
||
throw new Error(`${prop} \u5FC5\u987B\u662F\u679A\u4E3E\u5B57\u7B26\u4E32\u3002`);
|
||
}
|
||
const key = val.trim().toUpperCase();
|
||
const hit = mapping[key];
|
||
if (hit === void 0) {
|
||
throw new Error(`${prop} \u975E\u6CD5\u679A\u4E3E\u503C\u3002\u53EF\u9009\uFF1A${Object.keys(mapping).join(", ")}`);
|
||
}
|
||
return hit;
|
||
}
|
||
function parseHexColorRGBA(input) {
|
||
const s = input.trim();
|
||
if (!/^#([0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(s)) {
|
||
throw new Error("color \u5FC5\u987B\u662F #RRGGBB \u6216 #RRGGBBAA \u7684 Hex \u5B57\u7B26\u4E32\u3002");
|
||
}
|
||
const hex = s.slice(1);
|
||
const r = Number.parseInt(hex.slice(0, 2), 16);
|
||
const g = Number.parseInt(hex.slice(2, 4), 16);
|
||
const b = Number.parseInt(hex.slice(4, 6), 16);
|
||
const a = hex.length === 8 ? Number.parseInt(hex.slice(6, 8), 16) : 255;
|
||
return { r, g, b, a };
|
||
}
|
||
function coerceHexColor(val) {
|
||
if (typeof val !== "string") {
|
||
throw new Error("color \u5165\u53C2\u5FC5\u987B\u662F Hex \u5B57\u7B26\u4E32\u3002");
|
||
}
|
||
const rgba = parseHexColorRGBA(val);
|
||
const cc = getCcValueCtors();
|
||
const c = new cc.Color(rgba.r, rgba.g, rgba.b, rgba.a);
|
||
return { r: c.r, g: c.g, b: c.b, a: c.a };
|
||
}
|
||
function coerceToSize(val) {
|
||
if (!val || typeof val !== "object" || Array.isArray(val)) {
|
||
throw new Error("size \u5165\u53C2\u5FC5\u987B\u662F { width, height } \u5BF9\u8C61\u3002");
|
||
}
|
||
const o = val;
|
||
const cc = getCcValueCtors();
|
||
const w = coerceNonNegativeNumber(o.width, "width");
|
||
const h = coerceNonNegativeNumber(o.height, "height");
|
||
const sz = new cc.Size(w, h);
|
||
return { width: sz.width, height: sz.height };
|
||
}
|
||
function coerceToVec2(val) {
|
||
if (!val || typeof val !== "object" || Array.isArray(val)) {
|
||
throw new Error("vec2 \u5165\u53C2\u5FC5\u987B\u662F { x, y } \u5BF9\u8C61\u3002");
|
||
}
|
||
const o = val;
|
||
const cc = getCcValueCtors();
|
||
const x = typeof o.x === "number" ? o.x : Number(o.x);
|
||
const y = typeof o.y === "number" ? o.y : Number(o.y);
|
||
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
||
throw new Error("vec2 \u5750\u6807\u5FC5\u987B\u662F\u6709\u9650\u6570\u5B57\u3002");
|
||
}
|
||
const v = new cc.Vec2(x, y);
|
||
return { x: v.x, y: v.y };
|
||
}
|
||
function coerceAssetUuid(val, prop) {
|
||
if (typeof val !== "string") {
|
||
throw new Error(`${prop} \u8D44\u6E90\u5165\u53C2\u5FC5\u987B\u662F UUID \u5B57\u7B26\u4E32\u3002`);
|
||
}
|
||
const uuid = val.trim();
|
||
if (!uuid) {
|
||
throw new Error(`${prop} \u8D44\u6E90 UUID \u4E0D\u80FD\u4E3A\u7A7A\u3002`);
|
||
}
|
||
return { __uuid__: uuid };
|
||
}
|
||
var COMPONENT_SCHEMAS = {
|
||
UITransform: {
|
||
width: {
|
||
component: "UITransform",
|
||
prop: "width",
|
||
type: "number",
|
||
coercer: (val) => coerceNonNegativeNumber(val, "width")
|
||
},
|
||
height: {
|
||
component: "UITransform",
|
||
prop: "height",
|
||
type: "number",
|
||
coercer: (val) => coerceNonNegativeNumber(val, "height")
|
||
},
|
||
anchorPoint: {
|
||
component: "UITransform",
|
||
prop: "anchorPoint",
|
||
type: "vec2",
|
||
coercer: coerceToVec2
|
||
}
|
||
},
|
||
Sprite: {
|
||
color: {
|
||
component: "Sprite",
|
||
prop: "color",
|
||
type: "hex-color",
|
||
coercer: coerceHexColor
|
||
},
|
||
spriteFrame: {
|
||
component: "Sprite",
|
||
prop: "spriteFrame",
|
||
dumpProp: "_spriteFrame",
|
||
type: "asset-uuid",
|
||
coercer: (val) => coerceAssetUuid(val, "spriteFrame")
|
||
},
|
||
type: {
|
||
component: "Sprite",
|
||
prop: "type",
|
||
type: "enum-string",
|
||
coercer: (val) => coerceEnumString(val, "type", { SIMPLE: 0, SLICED: 1, TILED: 2, FILLED: 3, MESH: 4 })
|
||
},
|
||
sizeMode: {
|
||
component: "Sprite",
|
||
prop: "sizeMode",
|
||
type: "enum-string",
|
||
coercer: (val) => coerceEnumString(val, "sizeMode", { CUSTOM: 0, TRIMMED: 1, RAW: 2 })
|
||
}
|
||
},
|
||
Label: {
|
||
color: {
|
||
component: "Label",
|
||
prop: "color",
|
||
type: "hex-color",
|
||
coercer: coerceHexColor
|
||
},
|
||
string: {
|
||
component: "Label",
|
||
prop: "string",
|
||
type: "string",
|
||
coercer: (val) => coerceString(val, "string")
|
||
},
|
||
fontSize: {
|
||
component: "Label",
|
||
prop: "fontSize",
|
||
type: "number",
|
||
coercer: (val) => coerceNonNegativeNumber(val, "fontSize")
|
||
},
|
||
lineHeight: {
|
||
component: "Label",
|
||
prop: "lineHeight",
|
||
type: "number",
|
||
coercer: (val) => coerceNonNegativeNumber(val, "lineHeight")
|
||
},
|
||
overflow: {
|
||
component: "Label",
|
||
prop: "overflow",
|
||
type: "enum-string",
|
||
coercer: (val) => coerceEnumString(val, "overflow", { NONE: 0, CLAMP: 1, SHRINK: 2, RESIZE_HEIGHT: 3 })
|
||
},
|
||
horizontalAlign: {
|
||
component: "Label",
|
||
prop: "horizontalAlign",
|
||
type: "enum-string",
|
||
coercer: (val) => coerceEnumString(val, "horizontalAlign", { LEFT: 0, CENTER: 1, RIGHT: 2 })
|
||
},
|
||
verticalAlign: {
|
||
component: "Label",
|
||
prop: "verticalAlign",
|
||
type: "enum-string",
|
||
coercer: (val) => coerceEnumString(val, "verticalAlign", { TOP: 0, CENTER: 1, BOTTOM: 2 })
|
||
},
|
||
font: {
|
||
component: "Label",
|
||
prop: "font",
|
||
dumpProp: "_font",
|
||
type: "asset-uuid",
|
||
coercer: (val) => coerceAssetUuid(val, "font")
|
||
}
|
||
},
|
||
Button: {
|
||
interactable: {
|
||
component: "Button",
|
||
prop: "interactable",
|
||
type: "boolean",
|
||
coercer: (val) => coerceBoolean(val, "interactable")
|
||
},
|
||
transition: {
|
||
component: "Button",
|
||
prop: "transition",
|
||
type: "enum-string",
|
||
coercer: (val) => coerceEnumString(val, "transition", { NONE: 0, COLOR: 1, SPRITE: 2, SCALE: 3 })
|
||
},
|
||
zoomScale: {
|
||
component: "Button",
|
||
prop: "zoomScale",
|
||
type: "number",
|
||
coercer: (val) => coerceNonNegativeNumber(val, "zoomScale")
|
||
}
|
||
},
|
||
Layout: {
|
||
type: {
|
||
component: "Layout",
|
||
prop: "type",
|
||
type: "enum-string",
|
||
coercer: (val) => coerceEnumString(val, "type", { NONE: 0, HORIZONTAL: 1, VERTICAL: 2, GRID: 3 })
|
||
},
|
||
resizeMode: {
|
||
component: "Layout",
|
||
prop: "resizeMode",
|
||
type: "enum-string",
|
||
coercer: (val) => coerceEnumString(val, "resizeMode", { NONE: 0, CONTAINER: 1, CHILDREN: 2 })
|
||
},
|
||
constraint: {
|
||
component: "Layout",
|
||
prop: "constraint",
|
||
type: "enum-string",
|
||
coercer: (val) => coerceEnumString(val, "constraint", { NONE: 0, FIXED_ROW: 1, FIXED_COL: 2 })
|
||
},
|
||
spacingX: {
|
||
component: "Layout",
|
||
prop: "spacingX",
|
||
type: "number",
|
||
coercer: (val) => coerceNonNegativeNumber(val, "spacingX")
|
||
},
|
||
spacingY: {
|
||
component: "Layout",
|
||
prop: "spacingY",
|
||
type: "number",
|
||
coercer: (val) => coerceNonNegativeNumber(val, "spacingY")
|
||
},
|
||
paddingLeft: {
|
||
component: "Layout",
|
||
prop: "paddingLeft",
|
||
type: "number",
|
||
coercer: (val) => coerceNonNegativeNumber(val, "paddingLeft")
|
||
},
|
||
paddingRight: {
|
||
component: "Layout",
|
||
prop: "paddingRight",
|
||
type: "number",
|
||
coercer: (val) => coerceNonNegativeNumber(val, "paddingRight")
|
||
},
|
||
paddingTop: {
|
||
component: "Layout",
|
||
prop: "paddingTop",
|
||
type: "number",
|
||
coercer: (val) => coerceNonNegativeNumber(val, "paddingTop")
|
||
},
|
||
paddingBottom: {
|
||
component: "Layout",
|
||
prop: "paddingBottom",
|
||
type: "number",
|
||
coercer: (val) => coerceNonNegativeNumber(val, "paddingBottom")
|
||
},
|
||
cellSize: {
|
||
component: "Layout",
|
||
prop: "cellSize",
|
||
type: "size",
|
||
coercer: coerceToSize
|
||
},
|
||
constraintNum: {
|
||
component: "Layout",
|
||
prop: "constraintNum",
|
||
type: "number",
|
||
coercer: (val) => coerceNonNegativeNumber(val, "constraintNum")
|
||
}
|
||
},
|
||
Widget: {
|
||
isAlignTop: {
|
||
component: "Widget",
|
||
prop: "isAlignTop",
|
||
type: "boolean",
|
||
coercer: (val) => coerceBoolean(val, "isAlignTop")
|
||
},
|
||
isAlignBottom: {
|
||
component: "Widget",
|
||
prop: "isAlignBottom",
|
||
type: "boolean",
|
||
coercer: (val) => coerceBoolean(val, "isAlignBottom")
|
||
},
|
||
isAlignLeft: {
|
||
component: "Widget",
|
||
prop: "isAlignLeft",
|
||
type: "boolean",
|
||
coercer: (val) => coerceBoolean(val, "isAlignLeft")
|
||
},
|
||
isAlignRight: {
|
||
component: "Widget",
|
||
prop: "isAlignRight",
|
||
type: "boolean",
|
||
coercer: (val) => coerceBoolean(val, "isAlignRight")
|
||
},
|
||
isAlignHorizontalCenter: {
|
||
component: "Widget",
|
||
prop: "isAlignHorizontalCenter",
|
||
type: "boolean",
|
||
coercer: (val) => coerceBoolean(val, "isAlignHorizontalCenter")
|
||
},
|
||
isAlignVerticalCenter: {
|
||
component: "Widget",
|
||
prop: "isAlignVerticalCenter",
|
||
type: "boolean",
|
||
coercer: (val) => coerceBoolean(val, "isAlignVerticalCenter")
|
||
},
|
||
top: {
|
||
component: "Widget",
|
||
prop: "top",
|
||
type: "number",
|
||
coercer: (val) => coerceFiniteNumber(val, "top")
|
||
},
|
||
bottom: {
|
||
component: "Widget",
|
||
prop: "bottom",
|
||
type: "number",
|
||
coercer: (val) => coerceFiniteNumber(val, "bottom")
|
||
},
|
||
left: {
|
||
component: "Widget",
|
||
prop: "left",
|
||
type: "number",
|
||
coercer: (val) => coerceFiniteNumber(val, "left")
|
||
},
|
||
right: {
|
||
component: "Widget",
|
||
prop: "right",
|
||
type: "number",
|
||
coercer: (val) => coerceFiniteNumber(val, "right")
|
||
},
|
||
horizontalCenter: {
|
||
component: "Widget",
|
||
prop: "horizontalCenter",
|
||
type: "number",
|
||
coercer: (val) => coerceFiniteNumber(val, "horizontalCenter")
|
||
},
|
||
verticalCenter: {
|
||
component: "Widget",
|
||
prop: "verticalCenter",
|
||
type: "number",
|
||
coercer: (val) => coerceFiniteNumber(val, "verticalCenter")
|
||
}
|
||
}
|
||
};
|
||
function shouldSkipNode(name, includeInternal) {
|
||
if (includeInternal) {
|
||
return false;
|
||
}
|
||
return name.startsWith("_");
|
||
}
|
||
var EDITOR_DECORATION_ROOT_NAMES = new Set(
|
||
[
|
||
"Editor Scene Foreground",
|
||
"Editor Scene Background",
|
||
"gizmoRoot",
|
||
"GAME_VIEW",
|
||
"Editor Debugger",
|
||
"Scene Gizmo Camera",
|
||
"Scene Gizmo Root",
|
||
"Internal Gizmo",
|
||
"Scene Editor Camera"
|
||
].map((n) => n.toLowerCase())
|
||
);
|
||
function shouldSkipEditorDecorationRoot(name, includeEditorDecorations) {
|
||
if (includeEditorDecorations) {
|
||
return false;
|
||
}
|
||
return EDITOR_DECORATION_ROOT_NAMES.has(name.trim().toLowerCase());
|
||
}
|
||
function parentInspectMeta(node, scene) {
|
||
const par = node.parent ?? null;
|
||
if (par == null || par.uuid === scene.uuid) {
|
||
return { parentNid: null, parentRelation: "scene" };
|
||
}
|
||
const pnid = uuidToNid.get(par.uuid);
|
||
if (pnid !== void 0) {
|
||
return { parentNid: pnid, parentRelation: "indexed" };
|
||
}
|
||
return { parentNid: null, parentRelation: "unknown" };
|
||
}
|
||
function findNodeByUuid(root, targetUuid) {
|
||
if (!root) {
|
||
return null;
|
||
}
|
||
if (root.uuid === targetUuid) {
|
||
return root;
|
||
}
|
||
for (const child of root.children) {
|
||
const found = findNodeByUuid(child, targetUuid);
|
||
if (found) {
|
||
return found;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function releaseDetachedNodeStash(uuid) {
|
||
const stashed = detachedNodesForUndo.get(uuid);
|
||
if (!stashed) {
|
||
return { released: false, destroyed: false, liveNodePreserved: false };
|
||
}
|
||
let reachable = null;
|
||
try {
|
||
reachable = findNodeByUuid(stashed.scene, uuid);
|
||
} catch {
|
||
reachable = null;
|
||
}
|
||
const liveNodePreserved = reachable === stashed.node || stashed.node.parent !== null;
|
||
detachedNodesForUndo.delete(uuid);
|
||
if (liveNodePreserved) {
|
||
return { released: true, destroyed: false, liveNodePreserved: true };
|
||
}
|
||
try {
|
||
stashed.node.destroy?.();
|
||
return { released: true, destroyed: true, liveNodePreserved: false };
|
||
} catch {
|
||
return { released: true, destroyed: false, liveNodePreserved: false };
|
||
}
|
||
}
|
||
function nearlyEqual(actual, expected, epsilon = 0.01) {
|
||
return typeof actual === "number" && Number.isFinite(actual) && Math.abs(actual - expected) <= epsilon;
|
||
}
|
||
function requestedUiSize(expected) {
|
||
const defaults = expected.kind === "label" ? { width: 220, height: 48 } : expected.kind === "button" ? { width: 180, height: 56 } : expected.kind === "container" ? { width: 320, height: 180 } : { width: 100, height: 100 };
|
||
return expected.size ?? defaults;
|
||
}
|
||
function settledUiSize(expected) {
|
||
const seed = requestedUiSize(expected);
|
||
const layout = expected.layout;
|
||
if (!layout || layout.resizeMode !== "container") {
|
||
return seed;
|
||
}
|
||
const implicitChildren = expected.kind === "button" ? [
|
||
{
|
||
kind: "label",
|
||
size: expected.size ?? { width: 180, height: 56 },
|
||
fontSize: expected.fontSize ?? 24
|
||
}
|
||
] : [];
|
||
const children = [...implicitChildren, ...expected.children ?? []].filter((child) => child.active !== false).map((child) => settledUiSize(child));
|
||
const count = children.length;
|
||
const paddingH = (layout.paddingLeft ?? 0) + (layout.paddingRight ?? 0);
|
||
const paddingV = (layout.paddingTop ?? 0) + (layout.paddingBottom ?? 0);
|
||
if (layout.type === "horizontal") {
|
||
return {
|
||
width: children.reduce((sum, child) => sum + child.width, 0) + (count - 1) * (layout.spacingX ?? 0) + paddingH,
|
||
height: seed.height
|
||
};
|
||
}
|
||
if (layout.type === "vertical") {
|
||
return {
|
||
width: seed.width,
|
||
height: children.reduce((sum, child) => sum + child.height, 0) + (count - 1) * (layout.spacingY ?? 0) + paddingV
|
||
};
|
||
}
|
||
let columns = null;
|
||
const constraintNum = layout.constraintNum ?? 2;
|
||
const constraint = layout.constraint ?? ((layout.constraintNum ?? 0) > 0 ? "fixed-column" : "none");
|
||
if (constraint === "fixed-column" && constraintNum > 0) {
|
||
columns = constraintNum;
|
||
} else if (constraint === "fixed-row" && constraintNum > 0) {
|
||
columns = count > 0 ? Math.ceil(count / constraintNum) : 1;
|
||
}
|
||
const rowHeights = [];
|
||
if (columns !== null) {
|
||
for (let index = 0; index < count; index += columns) {
|
||
rowHeights.push(Math.max(...children.slice(index, index + columns).map((child) => child.height), 0));
|
||
}
|
||
} else {
|
||
const availableWidth = Math.max(0, seed.width - paddingH);
|
||
let rowWidth = 0;
|
||
let rowHeight = 0;
|
||
for (const child of children) {
|
||
const nextWidth = rowWidth === 0 ? child.width : rowWidth + (layout.spacingX ?? 0) + child.width;
|
||
if (rowWidth > 0 && nextWidth > availableWidth) {
|
||
rowHeights.push(rowHeight);
|
||
rowWidth = child.width;
|
||
rowHeight = child.height;
|
||
} else {
|
||
rowWidth = nextWidth;
|
||
rowHeight = Math.max(rowHeight, child.height);
|
||
}
|
||
}
|
||
if (rowWidth > 0) {
|
||
rowHeights.push(rowHeight);
|
||
}
|
||
}
|
||
return {
|
||
width: seed.width,
|
||
height: rowHeights.reduce((sum, height) => sum + height, 0) + Math.max(0, rowHeights.length - 1) * (layout.spacingY ?? 0) + paddingV
|
||
};
|
||
}
|
||
function uiComponent(node, cc, name) {
|
||
try {
|
||
return node.getComponents(cc.Component).find((component) => shortComponentName(component, cc) === name) ?? null;
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
function rgbaMatches(actual, expectedHex) {
|
||
if (!actual || typeof actual !== "object" || Array.isArray(actual)) {
|
||
return false;
|
||
}
|
||
const record = actual;
|
||
const expected = parseHexColorRGBA(expectedHex);
|
||
return record.r === expected.r && record.g === expected.g && record.b === expected.b && record.a === expected.a;
|
||
}
|
||
function verifyUiNodeRuntime(node, expected, cc) {
|
||
const mismatches = [];
|
||
const parentLayout = node.parent ? uiComponent(node.parent, cc, "Layout") : null;
|
||
const widgetOwnsX = expected.widget?.left !== void 0 || expected.widget?.right !== void 0 || expected.widget?.horizontalCenter !== void 0;
|
||
const widgetOwnsY = expected.widget?.top !== void 0 || expected.widget?.bottom !== void 0 || expected.widget?.verticalCenter !== void 0;
|
||
const parentLayoutOwnsX = parentLayout?.type === 1 || parentLayout?.type === 3;
|
||
const parentLayoutOwnsY = parentLayout?.type === 2 || parentLayout?.type === 3;
|
||
if (expected.name && node.name !== expected.name) {
|
||
mismatches.push(`name expected ${expected.name}, observed ${node.name}`);
|
||
}
|
||
if (expected.active !== void 0 && node.active !== expected.active) {
|
||
mismatches.push(`active expected ${String(expected.active)}, observed ${String(node.active)}`);
|
||
}
|
||
if (expected.position) {
|
||
for (const axis of ["x", "y", "z"]) {
|
||
const engineOwned = axis === "x" && (widgetOwnsX || parentLayoutOwnsX) || axis === "y" && (widgetOwnsY || parentLayoutOwnsY);
|
||
if (engineOwned) {
|
||
if (!Number.isFinite(node.position[axis])) {
|
||
mismatches.push(`engine-owned position.${axis} is invalid: ${String(node.position[axis])}`);
|
||
}
|
||
} else if (!nearlyEqual(node.position[axis], expected.position[axis])) {
|
||
mismatches.push(`position.${axis} expected ${expected.position[axis]}, observed ${node.position[axis]}`);
|
||
}
|
||
}
|
||
}
|
||
const settledExpectedSize = node.activeInHierarchy === false ? requestedUiSize(expected) : settledUiSize(expected);
|
||
let expectedWidth = settledExpectedSize.width;
|
||
let expectedHeight = settledExpectedSize.height;
|
||
const numeric = (value, fallback = 0) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
||
const scaleX = Math.abs(node.scale.x) || 1;
|
||
const scaleY = Math.abs(node.scale.y) || 1;
|
||
const parentTransform = node.parent ? uiComponent(node.parent, cc, "UITransform") : null;
|
||
const parentContentSize = parentTransform?.contentSize;
|
||
const parentWidth = numeric(parentTransform?.width, numeric(parentContentSize?.width));
|
||
const parentHeight = numeric(parentTransform?.height, numeric(parentContentSize?.height));
|
||
if (expected.widget?.left !== void 0 && expected.widget?.right !== void 0 && node.parent) {
|
||
expectedWidth = (parentWidth - expected.widget.left - expected.widget.right) / scaleX;
|
||
}
|
||
if (expected.widget?.top !== void 0 && expected.widget?.bottom !== void 0 && node.parent) {
|
||
expectedHeight = (parentHeight - expected.widget.top - expected.widget.bottom) / scaleY;
|
||
}
|
||
if (parentLayout?.resizeMode === 2 && node.parent) {
|
||
const usefulChildren = node.parent.children.filter(
|
||
(child) => child.activeInHierarchy !== false && uiComponent(child, cc, "UITransform") !== null
|
||
);
|
||
const childCount = usefulChildren.length;
|
||
const affectedByScale = parentLayout.affectedByScale === true;
|
||
const usedScaleX = affectedByScale ? scaleX : 1;
|
||
const usedScaleY = affectedByScale ? scaleY : 1;
|
||
if (parentLayout.type === 1 && childCount > 0) {
|
||
expectedWidth = (parentWidth - numeric(parentLayout.paddingLeft) - numeric(parentLayout.paddingRight) - (childCount - 1) * numeric(parentLayout.spacingX)) / childCount / usedScaleX;
|
||
} else if (parentLayout.type === 2 && childCount > 0) {
|
||
expectedHeight = (parentHeight - numeric(parentLayout.paddingTop) - numeric(parentLayout.paddingBottom) - (childCount - 1) * numeric(parentLayout.spacingY)) / childCount / usedScaleY;
|
||
} else if (parentLayout.type === 3) {
|
||
const cellSize = parentLayout.cellSize;
|
||
expectedWidth = numeric(cellSize?.width) / usedScaleX;
|
||
expectedHeight = numeric(cellSize?.height) / usedScaleY;
|
||
}
|
||
}
|
||
const transform = uiComponent(node, cc, "UITransform");
|
||
if (!transform) {
|
||
mismatches.push("UITransform component missing");
|
||
} else {
|
||
const contentSize = transform.contentSize;
|
||
const width = typeof transform.width === "number" ? transform.width : contentSize?.width;
|
||
const height = typeof transform.height === "number" ? transform.height : contentSize?.height;
|
||
if (!nearlyEqual(width, expectedWidth)) {
|
||
mismatches.push(`width expected ${expectedWidth}, observed ${String(width)}`);
|
||
}
|
||
if (!nearlyEqual(height, expectedHeight)) {
|
||
mismatches.push(`height expected ${expectedHeight}, observed ${String(height)}`);
|
||
}
|
||
}
|
||
if (expected.kind === "shape" || expected.kind === "sprite" || expected.kind === "button") {
|
||
const sprite = uiComponent(node, cc, "Sprite");
|
||
if (!sprite) {
|
||
mismatches.push("Sprite component missing");
|
||
} else {
|
||
if (!sprite.spriteFrame) {
|
||
mismatches.push("Sprite.spriteFrame is empty");
|
||
}
|
||
const expectedColor = expected.color ?? (expected.kind === "shape" ? "#FFFFFFFF" : expected.kind === "button" ? "#3478F6FF" : void 0);
|
||
if (expectedColor && !rgbaMatches(sprite.color, expectedColor)) {
|
||
mismatches.push(`Sprite.color does not match ${expectedColor}`);
|
||
}
|
||
if (expected.nineSlice === true && sprite.type !== 1) {
|
||
mismatches.push(`Sprite.type expected SLICED(1), observed ${String(sprite.type)}`);
|
||
}
|
||
}
|
||
}
|
||
if (expected.kind === "label") {
|
||
const label = uiComponent(node, cc, "Label");
|
||
if (!label) {
|
||
mismatches.push("Label component missing");
|
||
} else {
|
||
const expectedText = expected.text ?? expected.buttonLabel ?? "Label";
|
||
if (label.string !== expectedText) {
|
||
mismatches.push(`Label.string expected ${JSON.stringify(expectedText)}, observed ${JSON.stringify(label.string)}`);
|
||
}
|
||
if (!nearlyEqual(label.fontSize, expected.fontSize ?? 28)) {
|
||
mismatches.push(`Label.fontSize expected ${expected.fontSize ?? 28}, observed ${String(label.fontSize)}`);
|
||
}
|
||
if (!nearlyEqual(label.lineHeight, expected.lineHeight ?? expected.fontSize ?? 32)) {
|
||
mismatches.push(
|
||
`Label.lineHeight expected ${expected.lineHeight ?? expected.fontSize ?? 32}, observed ${String(label.lineHeight)}`
|
||
);
|
||
}
|
||
if (label.overflow !== 1) {
|
||
mismatches.push(`Label.overflow expected CLAMP(1), observed ${String(label.overflow)}`);
|
||
}
|
||
const horizontal = { left: 0, center: 1, right: 2 };
|
||
const vertical = { top: 0, center: 1, bottom: 2 };
|
||
if (label.horizontalAlign !== horizontal[expected.horizontalAlign ?? "center"]) {
|
||
mismatches.push("Label.horizontalAlign does not match");
|
||
}
|
||
if (label.verticalAlign !== vertical[expected.verticalAlign ?? "center"]) {
|
||
mismatches.push("Label.verticalAlign does not match");
|
||
}
|
||
if (expected.color && !rgbaMatches(label.color, expected.color)) {
|
||
mismatches.push(`Label.color does not match ${expected.color}`);
|
||
}
|
||
}
|
||
}
|
||
if (expected.kind === "button") {
|
||
const button = uiComponent(node, cc, "Button");
|
||
if (!button) {
|
||
mismatches.push("Button component missing");
|
||
} else if (button.interactable !== (expected.interactable !== false)) {
|
||
mismatches.push(`Button.interactable expected ${String(expected.interactable !== false)}`);
|
||
}
|
||
}
|
||
if (expected.layout) {
|
||
const layout = uiComponent(node, cc, "Layout");
|
||
if (!layout) {
|
||
mismatches.push("Layout component missing");
|
||
} else {
|
||
const typeMap = { horizontal: 1, vertical: 2, grid: 3 };
|
||
const resizeMap = { none: 0, container: 1, children: 2 };
|
||
const constraintMap = { none: 0, "fixed-row": 1, "fixed-column": 2 };
|
||
if (layout.type !== typeMap[expected.layout.type]) {
|
||
mismatches.push(`Layout.type does not match ${expected.layout.type}`);
|
||
}
|
||
if (layout.resizeMode !== resizeMap[expected.layout.resizeMode ?? "none"]) {
|
||
mismatches.push(`Layout.resizeMode does not match ${expected.layout.resizeMode ?? "none"}`);
|
||
}
|
||
if (expected.layout.constraint && layout.constraint !== constraintMap[expected.layout.constraint]) {
|
||
mismatches.push(`Layout.constraint does not match ${expected.layout.constraint}`);
|
||
}
|
||
for (const property of [
|
||
"spacingX",
|
||
"spacingY",
|
||
"paddingLeft",
|
||
"paddingRight",
|
||
"paddingTop",
|
||
"paddingBottom",
|
||
"constraintNum"
|
||
]) {
|
||
const expectedValue = expected.layout[property];
|
||
if (typeof expectedValue === "number" && !nearlyEqual(layout[property], expectedValue)) {
|
||
mismatches.push(`Layout.${property} expected ${expectedValue}, observed ${String(layout[property])}`);
|
||
}
|
||
}
|
||
if (expected.layout.cellSize) {
|
||
const cellSize = layout.cellSize;
|
||
if (!cellSize || !nearlyEqual(cellSize.width, expected.layout.cellSize.width) || !nearlyEqual(cellSize.height, expected.layout.cellSize.height)) {
|
||
mismatches.push("Layout.cellSize does not match");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (expected.widget) {
|
||
const widget = uiComponent(node, cc, "Widget");
|
||
if (!widget) {
|
||
mismatches.push("Widget component missing");
|
||
} else {
|
||
const alignments = [
|
||
["top", "isAlignTop"],
|
||
["bottom", "isAlignBottom"],
|
||
["left", "isAlignLeft"],
|
||
["right", "isAlignRight"],
|
||
["horizontalCenter", "isAlignHorizontalCenter"],
|
||
["verticalCenter", "isAlignVerticalCenter"]
|
||
];
|
||
for (const [valueProperty, enabledProperty] of alignments) {
|
||
const expectedValue = expected.widget[valueProperty];
|
||
if (typeof expectedValue === "number") {
|
||
if (widget[enabledProperty] !== true) {
|
||
mismatches.push(`Widget.${enabledProperty} is not enabled`);
|
||
}
|
||
if (!nearlyEqual(widget[valueProperty], expectedValue)) {
|
||
mismatches.push(`Widget.${valueProperty} expected ${expectedValue}, observed ${String(widget[valueProperty])}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return mismatches;
|
||
}
|
||
function resolveComponentTypeInput(input) {
|
||
const raw = input.trim();
|
||
if (!raw) {
|
||
throw new Error("componentType must be non-empty.");
|
||
}
|
||
if (raw.includes(".")) {
|
||
return raw;
|
||
}
|
||
if (raw.startsWith("cc")) {
|
||
return raw;
|
||
}
|
||
return `cc.${raw}`;
|
||
}
|
||
function resolveCtorByPath(cc, fullName) {
|
||
const raw = fullName.trim();
|
||
if (!raw) {
|
||
return null;
|
||
}
|
||
const pathParts = raw.split(".").filter(Boolean);
|
||
if (pathParts.length === 0) {
|
||
return null;
|
||
}
|
||
const parts = pathParts.length === 1 ? ["cc", pathParts[0]] : pathParts;
|
||
let cur = cc;
|
||
for (const p of parts) {
|
||
if (!cur || typeof cur !== "object") {
|
||
return null;
|
||
}
|
||
cur = cur[p];
|
||
}
|
||
if (typeof cur === "function") {
|
||
return cur;
|
||
}
|
||
try {
|
||
const ccJs = cc.js ?? void 0;
|
||
const byName = ccJs?.getClassByName?.(raw);
|
||
if (typeof byName === "function") {
|
||
return byName;
|
||
}
|
||
if (raw.startsWith("cc.")) {
|
||
const short = raw.slice(3);
|
||
const byShort = ccJs?.getClassByName?.(short);
|
||
if (typeof byShort === "function") {
|
||
return byShort;
|
||
}
|
||
}
|
||
} catch {
|
||
}
|
||
return null;
|
||
}
|
||
function resolveComponentCtor(rawType) {
|
||
const trimmed = rawType.trim();
|
||
if (!trimmed) {
|
||
return null;
|
||
}
|
||
const fullName = trimmed.includes(".") ? trimmed : `cc.${trimmed}`;
|
||
const ccAny = getCC();
|
||
const byPath = resolveCtorByPath(ccAny, fullName);
|
||
if (byPath) {
|
||
return byPath;
|
||
}
|
||
const js = ccAny.js;
|
||
if (typeof js?.getClassByName === "function") {
|
||
try {
|
||
const c = js.getClassByName(fullName);
|
||
if (typeof c === "function") {
|
||
return c;
|
||
}
|
||
} catch {
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
function isNodeUnderAncestorUuid(node, ancestorUuid) {
|
||
let cur = node;
|
||
while (cur) {
|
||
if (cur.uuid === ancestorUuid) {
|
||
return true;
|
||
}
|
||
cur = cur.parent ?? null;
|
||
}
|
||
return false;
|
||
}
|
||
function buildInspectPlain(node, cc, scene) {
|
||
const p = node.position;
|
||
const e = node.eulerAngles;
|
||
const s = node.scale;
|
||
const wp = node.worldPosition ?? p;
|
||
const activeInHierarchy = typeof node.activeInHierarchy === "boolean" ? node.activeInHierarchy : node.active;
|
||
const layer = typeof node.layer === "number" && Number.isFinite(node.layer) ? node.layer : 0;
|
||
const childCount = Array.isArray(node.children) ? node.children.length : 0;
|
||
const { parentNid, parentRelation } = parentInspectMeta(node, scene);
|
||
let comps = [];
|
||
try {
|
||
comps = node.getComponents(cc.Component);
|
||
} catch {
|
||
comps = [];
|
||
}
|
||
const components = comps.map((c) => shortComponentName(c, cc));
|
||
return {
|
||
name: node.name,
|
||
active: node.active,
|
||
activeInHierarchy,
|
||
layer,
|
||
childCount,
|
||
position: { x: p.x, y: p.y, z: p.z },
|
||
worldPosition: { x: wp.x, y: wp.y, z: wp.z },
|
||
rotationEuler: { x: e.x, y: e.y, z: e.z },
|
||
scale: { x: s.x, y: s.y, z: s.z },
|
||
components,
|
||
parentNid,
|
||
parentRelation
|
||
};
|
||
}
|
||
var methods = {
|
||
ping() {
|
||
return { status: "ok", layer: "scene" };
|
||
},
|
||
/**
|
||
* Runtime scene name + asset uuid for Main (`asset-db` → MCP `assetPath`). UUID is never sent to MCP clients.
|
||
*/
|
||
getCurrentSceneMeta() {
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: true, hasScene: false };
|
||
}
|
||
return { ok: true, hasScene: true, sceneName: scene.name, sceneAssetUuid: scene.uuid };
|
||
},
|
||
getHierarchy(...args) {
|
||
const opts = args[0] || {};
|
||
const maxDepth = opts.maxDepth !== void 0 ? Math.max(0, Math.min(100, opts.maxDepth)) : 5;
|
||
const maxNodes = opts.maxNodes !== void 0 ? Math.max(1, Math.min(5e4, opts.maxNodes)) : 800;
|
||
const includeInternal = opts.includeInternal === true;
|
||
const includeEditorDecorations = opts.includeEditorDecorations === true;
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return "[Error] No active scene in director.";
|
||
}
|
||
if (scene.uuid !== currentSceneUuid) {
|
||
uuidToNid.clear();
|
||
nidToUuid.clear();
|
||
nextNid = 1;
|
||
currentSceneUuid = scene.uuid;
|
||
}
|
||
const lines = [];
|
||
lines.push(
|
||
`[Scene: ${scene.name} | proto:${COCOS_MCP_WIRE_PROTOCOL_VERSION} | Session Time: ${Date.now()} | Note: NIDs are session-only | editorDecor:${includeEditorDecorations ? "on" : "off"}]`
|
||
);
|
||
let nodeCount = 0;
|
||
let truncatedByDepth = false;
|
||
let truncatedByNodes = false;
|
||
const visit = (node, depth) => {
|
||
if (nodeCount >= maxNodes) {
|
||
truncatedByNodes = true;
|
||
return;
|
||
}
|
||
if (shouldSkipNode(node.name, includeInternal)) {
|
||
return;
|
||
}
|
||
if (shouldSkipEditorDecorationRoot(node.name, includeEditorDecorations)) {
|
||
return;
|
||
}
|
||
if (depth > maxDepth) {
|
||
truncatedByDepth = true;
|
||
return;
|
||
}
|
||
const nid = getOrCreateNid(node.uuid);
|
||
nodeCount += 1;
|
||
const indent = " ".repeat(depth);
|
||
const comps = componentList(node, cc);
|
||
const inactive = node.active ? "" : " [INACTIVE]";
|
||
lines.push(`${indent}[${nid}] ${node.name} (${comps})${inactive}`);
|
||
if (depth === maxDepth) {
|
||
if (node.children.length > 0) {
|
||
truncatedByDepth = true;
|
||
}
|
||
return;
|
||
}
|
||
for (const child of node.children) {
|
||
if (nodeCount >= maxNodes) {
|
||
truncatedByNodes = true;
|
||
return;
|
||
}
|
||
visit(child, depth + 1);
|
||
}
|
||
};
|
||
visit(scene, 0);
|
||
if (truncatedByDepth) {
|
||
lines.push("... (truncated by maxDepth limitation)");
|
||
}
|
||
if (truncatedByNodes) {
|
||
lines.push("... (truncated by maxNodes limitation)");
|
||
}
|
||
return lines.join("\n");
|
||
},
|
||
searchNodes(...args) {
|
||
const opts = args[0] || {};
|
||
const nameSub = (opts.nameSubstring ?? "").trim().toLowerCase();
|
||
const compF = (opts.componentFilter ?? "").trim().toLowerCase();
|
||
if (!nameSub && !compF) {
|
||
return "[Error] Provide non-empty nameSubstring and/or componentFilter to limit matches.";
|
||
}
|
||
const maxResults = opts.maxResults !== void 0 ? Math.max(1, Math.min(500, opts.maxResults)) : 80;
|
||
const includeInternal = opts.includeInternal === true;
|
||
const includeEditorDecorations = opts.includeEditorDecorations === true;
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return "[Error] No active scene in director.";
|
||
}
|
||
if (scene.uuid !== currentSceneUuid) {
|
||
uuidToNid.clear();
|
||
nidToUuid.clear();
|
||
nextNid = 1;
|
||
currentSceneUuid = scene.uuid;
|
||
}
|
||
const lines = [];
|
||
lines.push(
|
||
`[Search | Scene: ${scene.name} | proto:${COCOS_MCP_WIRE_PROTOCOL_VERSION} | maxResults: ${maxResults} | NIDs session-scoped]`
|
||
);
|
||
let count = 0;
|
||
let capped = false;
|
||
const visit = (node, pathSegs) => {
|
||
if (count >= maxResults) {
|
||
capped = true;
|
||
return;
|
||
}
|
||
if (shouldSkipNode(node.name, includeInternal)) {
|
||
return;
|
||
}
|
||
if (shouldSkipEditorDecorationRoot(node.name, includeEditorDecorations)) {
|
||
return;
|
||
}
|
||
const comps = componentList(node, cc);
|
||
const nm = node.name.toLowerCase();
|
||
const cl = comps.toLowerCase();
|
||
const nameOk = !nameSub || nm.includes(nameSub);
|
||
const compOk = !compF || cl.includes(compF);
|
||
if (nameOk && compOk) {
|
||
count += 1;
|
||
const nid = getOrCreateNid(node.uuid);
|
||
const pathStr = pathSegs.length > 0 ? `${pathSegs.join("/")}/${node.name}` : node.name;
|
||
lines.push(`[${nid}] ${pathStr} (${comps})`);
|
||
}
|
||
for (const child of node.children) {
|
||
if (count >= maxResults) {
|
||
capped = true;
|
||
return;
|
||
}
|
||
visit(child, [...pathSegs, node.name]);
|
||
}
|
||
};
|
||
for (const child of scene.children) {
|
||
if (count >= maxResults) {
|
||
capped = true;
|
||
break;
|
||
}
|
||
visit(child, [scene.name]);
|
||
}
|
||
if (count === 0) {
|
||
lines.push("(no matches)");
|
||
}
|
||
if (capped) {
|
||
lines.push("... (truncated by maxResults)");
|
||
}
|
||
return lines.join("\n");
|
||
},
|
||
inspectNode(...args) {
|
||
const raw = args[0];
|
||
const nid = raw?.nid;
|
||
if (typeof nid !== "number" || !Number.isFinite(nid) || !Number.isInteger(nid)) {
|
||
return { ok: false, error: "invalid_args", message: "nid must be an integer (use getHierarchy NIDs)." };
|
||
}
|
||
const uuid = nidToUuid.get(nid);
|
||
if (!uuid) {
|
||
return {
|
||
ok: false,
|
||
error: "nid_not_found",
|
||
message: "Unknown nid \u2014 scene may have changed; call getHierarchy again."
|
||
};
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, error: "no_scene", message: "No active scene." };
|
||
}
|
||
if (scene.uuid !== currentSceneUuid) {
|
||
return {
|
||
ok: false,
|
||
error: "stale_session",
|
||
message: "Scene changed since last getHierarchy; refresh NID map first."
|
||
};
|
||
}
|
||
const node = findNodeByUuid(scene, uuid);
|
||
if (!node) {
|
||
return {
|
||
ok: false,
|
||
error: "node_missing",
|
||
message: "Node uuid no longer in scene; call getHierarchy again."
|
||
};
|
||
}
|
||
return { ok: true, data: buildInspectPlain(node, cc, scene) };
|
||
},
|
||
/**
|
||
* Force deferred Label/Layout/Widget work to a stable runtime state before
|
||
* task-level verification. Creator otherwise flushes these components on a
|
||
* later render frame, which can make an immediate read a false positive.
|
||
*/
|
||
settleUiNodes(...args) {
|
||
const rawNids = args[0];
|
||
if (!Array.isArray(rawNids) || rawNids.length < 1 || rawNids.length > 64) {
|
||
return {
|
||
ok: false,
|
||
error: "ui_settle_failed",
|
||
message: "settleUiNodes requires an array of 1 to 64 nids."
|
||
};
|
||
}
|
||
const nids = [...new Set(rawNids)];
|
||
if (nids.some((nid) => typeof nid !== "number" || !Number.isInteger(nid))) {
|
||
return {
|
||
ok: false,
|
||
error: "ui_settle_failed",
|
||
message: "settleUiNodes requires integer nids."
|
||
};
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, error: "ui_settle_failed", message: "No active scene." };
|
||
}
|
||
const nodes = [];
|
||
for (const nid of nids) {
|
||
const uuid = nidToUuid.get(nid);
|
||
const node = uuid ? findNodeByUuid(scene, uuid) : null;
|
||
if (!node) {
|
||
return {
|
||
ok: false,
|
||
error: "ui_settle_failed",
|
||
message: `UI node ${nid} is missing from the active runtime scene.`
|
||
};
|
||
}
|
||
nodes.push(node);
|
||
}
|
||
const depthOf = (node) => {
|
||
let depth = 0;
|
||
let current = node.parent;
|
||
while (current && depth < 64) {
|
||
depth += 1;
|
||
current = current.parent;
|
||
}
|
||
return depth;
|
||
};
|
||
const shallowToDeep = nodes.map((node, index) => ({ node, index, depth: depthOf(node) })).sort((left, right) => left.depth - right.depth || left.index - right.index).map((entry) => entry.node);
|
||
const deepToShallow = [...shallowToDeep].reverse();
|
||
const stableNumber = (value) => typeof value === "number" && Number.isFinite(value) ? Math.round(value * 1e4) / 1e4 : null;
|
||
const geometrySnapshot = () => JSON.stringify(
|
||
shallowToDeep.map((node) => {
|
||
const transform = uiComponent(node, cc, "UITransform");
|
||
const contentSize = transform?.contentSize;
|
||
return [
|
||
node.uuid,
|
||
stableNumber(node.position.x),
|
||
stableNumber(node.position.y),
|
||
stableNumber(node.position.z),
|
||
stableNumber(typeof transform?.width === "number" ? transform.width : contentSize?.width),
|
||
stableNumber(typeof transform?.height === "number" ? transform.height : contentSize?.height)
|
||
];
|
||
})
|
||
);
|
||
let labelUpdates = 0;
|
||
let layoutUpdates = 0;
|
||
let widgetUpdates = 0;
|
||
let previousSnapshot = null;
|
||
let stablePasses = 0;
|
||
try {
|
||
const maxPasses = Math.min(16, Math.max(4, shallowToDeep.length + 2));
|
||
for (let pass = 1; pass <= maxPasses; pass += 1) {
|
||
for (const node of shallowToDeep) {
|
||
if (node.activeInHierarchy === false) continue;
|
||
const label = uiComponent(node, cc, "Label");
|
||
const updateRenderData = label?.updateRenderData;
|
||
if (typeof updateRenderData === "function") {
|
||
updateRenderData.call(label, true);
|
||
labelUpdates += 1;
|
||
}
|
||
}
|
||
for (const node of deepToShallow) {
|
||
if (node.activeInHierarchy === false) continue;
|
||
const layout = uiComponent(node, cc, "Layout");
|
||
const updateLayout = layout?.updateLayout;
|
||
if (typeof updateLayout === "function") {
|
||
updateLayout.call(layout, true);
|
||
layoutUpdates += 1;
|
||
}
|
||
}
|
||
for (const node of shallowToDeep) {
|
||
if (node.activeInHierarchy === false) continue;
|
||
const widget = uiComponent(node, cc, "Widget");
|
||
const updateAlignment = widget?.updateAlignment;
|
||
if (typeof updateAlignment === "function") {
|
||
updateAlignment.call(widget);
|
||
widgetUpdates += 1;
|
||
}
|
||
}
|
||
for (const node of deepToShallow) {
|
||
if (node.activeInHierarchy === false) continue;
|
||
const layout = uiComponent(node, cc, "Layout");
|
||
const updateLayout = layout?.updateLayout;
|
||
if (typeof updateLayout === "function") {
|
||
updateLayout.call(layout, true);
|
||
layoutUpdates += 1;
|
||
}
|
||
}
|
||
for (const node of shallowToDeep) {
|
||
if (node.activeInHierarchy === false) continue;
|
||
const label = uiComponent(node, cc, "Label");
|
||
const updateRenderData = label?.updateRenderData;
|
||
if (typeof updateRenderData === "function") {
|
||
updateRenderData.call(label, true);
|
||
labelUpdates += 1;
|
||
}
|
||
}
|
||
const snapshot = geometrySnapshot();
|
||
if (previousSnapshot === snapshot) {
|
||
stablePasses = pass;
|
||
break;
|
||
}
|
||
previousSnapshot = snapshot;
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
return { ok: false, error: "ui_settle_failed", message: `UI settle failed: ${message}` };
|
||
}
|
||
if (stablePasses === 0) {
|
||
return {
|
||
ok: false,
|
||
error: "ui_settle_failed",
|
||
message: "UI geometry did not stabilize within the bounded settle passes."
|
||
};
|
||
}
|
||
return { ok: true, passes: stablePasses, labelUpdates, layoutUpdates, widgetUpdates };
|
||
},
|
||
verifyUiNode(...args) {
|
||
const raw = args[0];
|
||
if (!raw || typeof raw.nid !== "number" || !Number.isInteger(raw.nid)) {
|
||
return {
|
||
ok: false,
|
||
error: "ui_verification_failed",
|
||
message: "verifyUiNode requires an integer nid.",
|
||
mismatches: ["invalid nid"]
|
||
};
|
||
}
|
||
if (!raw.expected || typeof raw.expected !== "object" || Array.isArray(raw.expected)) {
|
||
return {
|
||
ok: false,
|
||
error: "ui_verification_failed",
|
||
message: "verifyUiNode requires an expected UI node specification.",
|
||
mismatches: ["invalid expected UI node specification"]
|
||
};
|
||
}
|
||
const uuid = nidToUuid.get(raw.nid);
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
const node = uuid && scene ? findNodeByUuid(scene, uuid) : null;
|
||
if (!node) {
|
||
return {
|
||
ok: false,
|
||
error: "ui_verification_failed",
|
||
message: `UI node ${raw.nid} is missing from the active runtime scene.`,
|
||
mismatches: ["node missing"]
|
||
};
|
||
}
|
||
const mismatches = verifyUiNodeRuntime(node, raw.expected, cc);
|
||
if (mismatches.length > 0) {
|
||
return {
|
||
ok: false,
|
||
error: "ui_verification_failed",
|
||
message: `Runtime UI verification failed for ${node.name}: ${mismatches.join("; ")}`,
|
||
mismatches
|
||
};
|
||
}
|
||
return { ok: true };
|
||
},
|
||
/**
|
||
* Returns node uuid for Main to call `scene` `query-node` + `set-property` with `record: true` (undo).
|
||
* Does not mutate the scene.
|
||
*/
|
||
prepareSetNodeActive(...args) {
|
||
const raw = args[0];
|
||
const nid = raw?.nid;
|
||
const nextActive = raw?.active;
|
||
if (typeof nid !== "number" || !Number.isFinite(nid) || !Number.isInteger(nid)) {
|
||
return { ok: false, error: "invalid_args", message: "nid must be an integer (use getHierarchy NIDs)." };
|
||
}
|
||
if (typeof nextActive !== "boolean") {
|
||
return { ok: false, error: "invalid_args", message: "active must be a boolean." };
|
||
}
|
||
const uuid = nidToUuid.get(nid);
|
||
if (!uuid) {
|
||
return {
|
||
ok: false,
|
||
error: "nid_not_found",
|
||
message: "Unknown nid \u2014 scene may have changed; call getHierarchy again."
|
||
};
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, error: "no_scene", message: "No active scene." };
|
||
}
|
||
if (scene.uuid !== currentSceneUuid) {
|
||
return {
|
||
ok: false,
|
||
error: "stale_session",
|
||
message: "Scene changed since last getHierarchy; refresh NID map first."
|
||
};
|
||
}
|
||
const node = findNodeByUuid(scene, uuid);
|
||
if (!node) {
|
||
return {
|
||
ok: false,
|
||
error: "node_missing",
|
||
message: "Node uuid no longer in scene; call getHierarchy again."
|
||
};
|
||
}
|
||
return { ok: true, uuid: node.uuid };
|
||
},
|
||
/** Same nid/stale checks as `prepareSetNodeActive`; Main applies transform via query-node + set-property. */
|
||
prepareSetNodeTransform(...args) {
|
||
const raw = args[0];
|
||
const nid = raw?.nid;
|
||
if (typeof nid !== "number" || !Number.isFinite(nid) || !Number.isInteger(nid)) {
|
||
return { ok: false, error: "invalid_args", message: "nid must be an integer (use getHierarchy NIDs)." };
|
||
}
|
||
const uuid = nidToUuid.get(nid);
|
||
if (!uuid) {
|
||
return {
|
||
ok: false,
|
||
error: "nid_not_found",
|
||
message: "Unknown nid \u2014 scene may have changed; call getHierarchy again."
|
||
};
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, error: "no_scene", message: "No active scene." };
|
||
}
|
||
if (scene.uuid !== currentSceneUuid) {
|
||
return {
|
||
ok: false,
|
||
error: "stale_session",
|
||
message: "Scene changed since last getHierarchy; refresh NID map first."
|
||
};
|
||
}
|
||
const node = findNodeByUuid(scene, uuid);
|
||
if (!node) {
|
||
return {
|
||
ok: false,
|
||
error: "node_missing",
|
||
message: "Node uuid no longer in scene; call getHierarchy again."
|
||
};
|
||
}
|
||
return { ok: true, uuid: node.uuid };
|
||
},
|
||
/** Validates trimmed node `name` (1–256 chars, no ASCII control); Main sets path `name` via `set-property`. */
|
||
prepareSetNodeName(...args) {
|
||
const raw = args[0];
|
||
const nid = raw?.nid;
|
||
const rawName = raw?.name;
|
||
if (typeof nid !== "number" || !Number.isFinite(nid) || !Number.isInteger(nid)) {
|
||
return { ok: false, error: "invalid_args", message: "nid must be an integer (use getHierarchy NIDs)." };
|
||
}
|
||
if (typeof rawName !== "string") {
|
||
return { ok: false, error: "invalid_args", message: "name must be a string." };
|
||
}
|
||
const name = rawName.trim();
|
||
if (name.length === 0) {
|
||
return { ok: false, error: "invalid_args", message: "name must be non-empty after trim." };
|
||
}
|
||
if (name.length > 256) {
|
||
return { ok: false, error: "invalid_args", message: "name exceeds 256 characters." };
|
||
}
|
||
if (/[\u0000-\u001F\u007F]/.test(name)) {
|
||
return { ok: false, error: "invalid_args", message: "name must not contain control characters." };
|
||
}
|
||
const uuid = nidToUuid.get(nid);
|
||
if (!uuid) {
|
||
return {
|
||
ok: false,
|
||
error: "nid_not_found",
|
||
message: "Unknown nid \u2014 scene may have changed; call getHierarchy again."
|
||
};
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, error: "no_scene", message: "No active scene." };
|
||
}
|
||
if (scene.uuid !== currentSceneUuid) {
|
||
return {
|
||
ok: false,
|
||
error: "stale_session",
|
||
message: "Scene changed since last getHierarchy; refresh NID map first."
|
||
};
|
||
}
|
||
const node = findNodeByUuid(scene, uuid);
|
||
if (!node) {
|
||
return {
|
||
ok: false,
|
||
error: "node_missing",
|
||
message: "Node uuid no longer in scene; call getHierarchy again."
|
||
};
|
||
}
|
||
return { ok: true, uuid: node.uuid, name };
|
||
},
|
||
/** Validates parent `nid` + trimmed `name`; Main calls `scene` `create-node`. */
|
||
prepareCreateNode(...args) {
|
||
const raw = args[0];
|
||
const parentNid = raw?.parentNid;
|
||
const rawName = raw?.name;
|
||
if (typeof parentNid !== "number" || !Number.isFinite(parentNid) || !Number.isInteger(parentNid)) {
|
||
return { ok: false, error: "invalid_args", message: "parentNid must be an integer (use getHierarchy NIDs)." };
|
||
}
|
||
if (typeof rawName !== "string") {
|
||
return { ok: false, error: "invalid_args", message: "name must be a string." };
|
||
}
|
||
const name = rawName.trim();
|
||
if (name.length === 0) {
|
||
return { ok: false, error: "invalid_args", message: "name must be non-empty after trim." };
|
||
}
|
||
if (name.length > 256) {
|
||
return { ok: false, error: "invalid_args", message: "name exceeds 256 characters." };
|
||
}
|
||
if (/[\u0000-\u001F\u007F]/.test(name)) {
|
||
return { ok: false, error: "invalid_args", message: "name must not contain control characters." };
|
||
}
|
||
const parentUuid = nidToUuid.get(parentNid);
|
||
if (!parentUuid) {
|
||
return {
|
||
ok: false,
|
||
error: "nid_not_found",
|
||
message: "Unknown parentNid \u2014 call getHierarchy again."
|
||
};
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, error: "no_scene", message: "No active scene." };
|
||
}
|
||
if (scene.uuid !== currentSceneUuid) {
|
||
return {
|
||
ok: false,
|
||
error: "stale_session",
|
||
message: "Scene changed since last getHierarchy; refresh NID map first."
|
||
};
|
||
}
|
||
const parentNode = findNodeByUuid(scene, parentUuid);
|
||
if (!parentNode) {
|
||
return {
|
||
ok: false,
|
||
error: "node_missing",
|
||
message: "Parent node no longer in scene; call getHierarchy again."
|
||
};
|
||
}
|
||
return { ok: true, parentUuid: parentNode.uuid, name };
|
||
},
|
||
/** Parent `nid` for prefab instantiation (`create-node` + `assetUuid` in Main). */
|
||
prepareInstantiatePrefab(...args) {
|
||
const raw = args[0];
|
||
const parentNid = raw?.parentNid;
|
||
if (typeof parentNid !== "number" || !Number.isFinite(parentNid) || !Number.isInteger(parentNid)) {
|
||
return { ok: false, error: "invalid_args", message: "parentNid must be an integer (use getHierarchy NIDs)." };
|
||
}
|
||
const parentUuid = nidToUuid.get(parentNid);
|
||
if (!parentUuid) {
|
||
return {
|
||
ok: false,
|
||
error: "nid_not_found",
|
||
message: "Unknown parentNid \u2014 call getHierarchy again."
|
||
};
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, error: "no_scene", message: "No active scene." };
|
||
}
|
||
if (scene.uuid !== currentSceneUuid) {
|
||
return {
|
||
ok: false,
|
||
error: "stale_session",
|
||
message: "Scene changed since last getHierarchy; refresh NID map first."
|
||
};
|
||
}
|
||
const parentNode = findNodeByUuid(scene, parentUuid);
|
||
if (!parentNode) {
|
||
return {
|
||
ok: false,
|
||
error: "node_missing",
|
||
message: "Parent node no longer in scene; call getHierarchy again."
|
||
};
|
||
}
|
||
return { ok: true, parentUuid: parentNode.uuid };
|
||
},
|
||
/** Validates delete (scene root forbidden; leaf-only unless `cascade`). */
|
||
prepareDeleteNode(...args) {
|
||
const raw = args[0];
|
||
if (raw?.confirm !== true) {
|
||
return { ok: false, error: "invalid_args", message: "confirm must be true (explicit opt-in)." };
|
||
}
|
||
const nid = raw.nid;
|
||
const cascade = raw.cascade === true;
|
||
if (typeof nid !== "number" || !Number.isFinite(nid) || !Number.isInteger(nid)) {
|
||
return { ok: false, error: "invalid_args", message: "nid must be an integer (use getHierarchy NIDs)." };
|
||
}
|
||
const uuid = nidToUuid.get(nid);
|
||
if (!uuid) {
|
||
return {
|
||
ok: false,
|
||
error: "nid_not_found",
|
||
message: "Unknown nid \u2014 call getHierarchy again."
|
||
};
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, error: "no_scene", message: "No active scene." };
|
||
}
|
||
if (scene.uuid !== currentSceneUuid) {
|
||
return {
|
||
ok: false,
|
||
error: "stale_session",
|
||
message: "Scene changed since last getHierarchy; refresh NID map first."
|
||
};
|
||
}
|
||
if (uuid === scene.uuid) {
|
||
return {
|
||
ok: false,
|
||
error: "invalid_target",
|
||
message: "Cannot delete the scene root node."
|
||
};
|
||
}
|
||
const node = findNodeByUuid(scene, uuid);
|
||
if (!node) {
|
||
return {
|
||
ok: false,
|
||
error: "node_missing",
|
||
message: "Node no longer in scene; call getHierarchy again."
|
||
};
|
||
}
|
||
if (node.children.length > 0 && !cascade) {
|
||
return {
|
||
ok: false,
|
||
error: "non_leaf",
|
||
message: "Node has children: pass cascade: true to delete the whole subtree, or delete children first."
|
||
};
|
||
}
|
||
return { ok: true, uuid: node.uuid };
|
||
},
|
||
/** Validates reparent (`set-parent`): nids in map, no scene-root move, no cycles. */
|
||
prepareReparentNode(...args) {
|
||
const raw = args[0];
|
||
const childNid = raw?.childNid;
|
||
const parentNid = raw?.parentNid;
|
||
const keepWorldTransform = raw?.keepWorldTransform !== false;
|
||
if (typeof childNid !== "number" || !Number.isFinite(childNid) || !Number.isInteger(childNid)) {
|
||
return { ok: false, error: "invalid_args", message: "childNid must be an integer (use getHierarchy NIDs)." };
|
||
}
|
||
if (typeof parentNid !== "number" || !Number.isFinite(parentNid) || !Number.isInteger(parentNid)) {
|
||
return { ok: false, error: "invalid_args", message: "parentNid must be an integer (use getHierarchy NIDs)." };
|
||
}
|
||
if (childNid === parentNid) {
|
||
return { ok: false, error: "invalid_args", message: "childNid and parentNid must differ." };
|
||
}
|
||
const childUuid = nidToUuid.get(childNid);
|
||
const newParentUuid = nidToUuid.get(parentNid);
|
||
if (!childUuid) {
|
||
return {
|
||
ok: false,
|
||
error: "nid_not_found",
|
||
message: "Unknown childNid \u2014 call getHierarchy again."
|
||
};
|
||
}
|
||
if (!newParentUuid) {
|
||
return {
|
||
ok: false,
|
||
error: "nid_not_found",
|
||
message: "Unknown parentNid \u2014 call getHierarchy again."
|
||
};
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, error: "no_scene", message: "No active scene." };
|
||
}
|
||
if (scene.uuid !== currentSceneUuid) {
|
||
return {
|
||
ok: false,
|
||
error: "stale_session",
|
||
message: "Scene changed since last getHierarchy; refresh NID map first."
|
||
};
|
||
}
|
||
if (childUuid === scene.uuid) {
|
||
return {
|
||
ok: false,
|
||
error: "invalid_target",
|
||
message: "Cannot reparent the scene root node."
|
||
};
|
||
}
|
||
const childNode = findNodeByUuid(scene, childUuid);
|
||
const newParentNode = findNodeByUuid(scene, newParentUuid);
|
||
if (!childNode || !newParentNode) {
|
||
return {
|
||
ok: false,
|
||
error: "node_missing",
|
||
message: "Node no longer in scene; call getHierarchy again."
|
||
};
|
||
}
|
||
if (isNodeUnderAncestorUuid(newParentNode, childUuid)) {
|
||
return {
|
||
ok: false,
|
||
error: "invalid_target",
|
||
message: "Invalid reparent: new parent is the child itself or inside the child subtree (cycle)."
|
||
};
|
||
}
|
||
const par = childNode.parent ?? null;
|
||
const previousParentUuid = par == null || par.uuid === scene.uuid ? scene.uuid : par.uuid;
|
||
const noOp = previousParentUuid === newParentUuid;
|
||
return {
|
||
ok: true,
|
||
childUuid: childNode.uuid,
|
||
newParentUuid: newParentNode.uuid,
|
||
previousParentUuid,
|
||
keepWorldTransform,
|
||
noOp
|
||
};
|
||
},
|
||
/** After Main `create-node`, assign the next session `nid` for MCP tools. */
|
||
registerNidForNewNode(...args) {
|
||
const uuid = args[0];
|
||
if (typeof uuid !== "string" || uuid.trim().length === 0) {
|
||
return { ok: false, message: "registerNidForNewNode: invalid uuid" };
|
||
}
|
||
const existing = uuidToNid.get(uuid);
|
||
if (existing !== void 0) {
|
||
return { ok: true, nid: existing };
|
||
}
|
||
const nid = nextNid++;
|
||
uuidToNid.set(uuid, nid);
|
||
nidToUuid.set(nid, uuid);
|
||
return { ok: true, nid };
|
||
},
|
||
/** Drop session mapping (e.g. after MCP undo removed the node). */
|
||
forgetNodeNid(...args) {
|
||
const uuid = args[0];
|
||
if (typeof uuid !== "string") {
|
||
return { ok: true };
|
||
}
|
||
const nid = uuidToNid.get(uuid);
|
||
if (nid !== void 0) {
|
||
uuidToNid.delete(uuid);
|
||
nidToUuid.delete(nid);
|
||
}
|
||
return { ok: true };
|
||
},
|
||
/** Remove session nids for a node and all descendants (call before `remove-node` when cascade). */
|
||
forgetNidsInSubtree(...args) {
|
||
const rootUuid = args[0];
|
||
if (typeof rootUuid !== "string") {
|
||
return { ok: true };
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: true };
|
||
}
|
||
const root = findNodeByUuid(scene, rootUuid);
|
||
if (!root) {
|
||
return { ok: true };
|
||
}
|
||
const forget = (n) => {
|
||
const mapped = uuidToNid.get(n.uuid);
|
||
if (mapped !== void 0) {
|
||
uuidToNid.delete(n.uuid);
|
||
nidToUuid.delete(mapped);
|
||
}
|
||
for (const c of n.children) {
|
||
forget(c);
|
||
}
|
||
};
|
||
forget(root);
|
||
return { ok: true };
|
||
},
|
||
/** Snapshot currently mapped uuid↔nid pairs in a subtree (used for transactional delete). */
|
||
snapshotNidMappingsInSubtree(...args) {
|
||
const rootUuid = args[0];
|
||
if (typeof rootUuid !== "string") {
|
||
return { ok: true, entries: [] };
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: true, entries: [] };
|
||
}
|
||
const root = findNodeByUuid(scene, rootUuid);
|
||
if (!root) {
|
||
return { ok: true, entries: [] };
|
||
}
|
||
const entries = [];
|
||
const walk = (n) => {
|
||
const nid = uuidToNid.get(n.uuid);
|
||
if (nid !== void 0) {
|
||
entries.push({ uuid: n.uuid, nid });
|
||
}
|
||
for (const c of n.children) {
|
||
walk(c);
|
||
}
|
||
};
|
||
walk(root);
|
||
return { ok: true, entries };
|
||
},
|
||
/**
|
||
* Detach a subtree without destroying it so MCP local undo can restore the
|
||
* exact objects and UUIDs. This avoids Creator's scene-wide global undo,
|
||
* which can decode unrelated render assets while a frame is being drawn.
|
||
*/
|
||
detachNodeForMcpUndo(...args) {
|
||
const uuid = args[0];
|
||
if (typeof uuid !== "string" || uuid.length === 0) {
|
||
return { ok: false, message: "detachNodeForMcpUndo: uuid must be a non-empty string." };
|
||
}
|
||
if (detachedNodesForUndo.has(uuid)) {
|
||
return { ok: false, message: `detachNodeForMcpUndo: ${uuid} is already detached.` };
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, message: "No active scene." };
|
||
}
|
||
const node = findNodeByUuid(scene, uuid);
|
||
if (!node || node === scene) {
|
||
return { ok: false, message: "Node is missing or is the scene root." };
|
||
}
|
||
const parent = node.parent;
|
||
if (!parent) {
|
||
return { ok: false, message: "Node has no parent." };
|
||
}
|
||
const siblingIndex = parent.children.findIndex((child) => child.uuid === uuid);
|
||
try {
|
||
if (typeof node.removeFromParent === "function") {
|
||
node.removeFromParent();
|
||
} else {
|
||
node.parent = null;
|
||
}
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
return { ok: false, message: `detachNodeForMcpUndo failed: ${message}` };
|
||
}
|
||
if (node.parent || findNodeByUuid(scene, uuid)) {
|
||
return { ok: false, message: "detachNodeForMcpUndo did not remove the node from the scene tree." };
|
||
}
|
||
while (detachedNodesForUndo.size >= DETACHED_NODE_STASH_MAX) {
|
||
const oldest = detachedNodesForUndo.keys().next().value;
|
||
if (!oldest) {
|
||
break;
|
||
}
|
||
releaseDetachedNodeStash(oldest);
|
||
}
|
||
detachedNodesForUndo.set(uuid, {
|
||
node,
|
||
scene,
|
||
parentUuid: parent.uuid,
|
||
siblingIndex: Math.max(0, siblingIndex)
|
||
});
|
||
return { ok: true, uuid, parentUuid: parent.uuid, siblingIndex: Math.max(0, siblingIndex) };
|
||
},
|
||
/** Restore a subtree previously detached by `detachNodeForMcpUndo`. */
|
||
restoreDetachedNodeForMcpUndo(...args) {
|
||
const uuid = args[0];
|
||
if (typeof uuid !== "string" || uuid.length === 0) {
|
||
return { ok: false, message: "restoreDetachedNodeForMcpUndo: uuid must be a non-empty string." };
|
||
}
|
||
const stashed = detachedNodesForUndo.get(uuid);
|
||
if (!stashed) {
|
||
const current = getCC().director.getScene();
|
||
if (current && findNodeByUuid(current, uuid)) {
|
||
return { ok: true, uuid, alreadyRestored: true };
|
||
}
|
||
return {
|
||
ok: false,
|
||
terminal: true,
|
||
message: `No detached node stash exists for ${uuid}; the local undo entry is no longer recoverable.`
|
||
};
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, message: "No active scene." };
|
||
}
|
||
if (scene !== stashed.scene) {
|
||
releaseDetachedNodeStash(uuid);
|
||
return {
|
||
ok: false,
|
||
terminal: true,
|
||
message: "The active scene changed after the node was detached; the stale undo entry was discarded."
|
||
};
|
||
}
|
||
const existing = findNodeByUuid(scene, uuid);
|
||
if (existing) {
|
||
if (existing !== stashed.node) {
|
||
releaseDetachedNodeStash(uuid);
|
||
} else {
|
||
detachedNodesForUndo.delete(uuid);
|
||
}
|
||
return { ok: true, uuid, alreadyRestored: true };
|
||
}
|
||
const parent = findNodeByUuid(scene, stashed.parentUuid);
|
||
if (!parent) {
|
||
return { ok: false, message: "Original parent is no longer in the active scene." };
|
||
}
|
||
try {
|
||
if (typeof parent.addChild === "function") {
|
||
parent.addChild(stashed.node);
|
||
} else {
|
||
stashed.node.parent = parent;
|
||
}
|
||
stashed.node.setSiblingIndex?.(stashed.siblingIndex);
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
return { ok: false, message: `restoreDetachedNodeForMcpUndo failed: ${message}` };
|
||
}
|
||
if (findNodeByUuid(scene, uuid) !== stashed.node) {
|
||
return { ok: false, message: "Restored node is not reachable from the active scene root." };
|
||
}
|
||
detachedNodesForUndo.delete(uuid);
|
||
return { ok: true, uuid };
|
||
},
|
||
/**
|
||
* Dispose detached subtrees when the MCP undo stack is cleared on unload.
|
||
* With no UUID list, release every stash (including entries evicted from the
|
||
* bounded Main undo stack). A node that another editor undo already restored
|
||
* into a live tree is never destroyed.
|
||
*/
|
||
discardDetachedNodesForMcpUndo(...args) {
|
||
const requested = Array.isArray(args[0]) ? args[0] : Array.from(detachedNodesForUndo.keys());
|
||
let discarded = 0;
|
||
let liveNodesPreserved = 0;
|
||
for (const value of requested) {
|
||
if (typeof value !== "string") {
|
||
continue;
|
||
}
|
||
const released = releaseDetachedNodeStash(value);
|
||
if (released.released) {
|
||
discarded += 1;
|
||
}
|
||
if (released.liveNodePreserved) {
|
||
liveNodesPreserved += 1;
|
||
}
|
||
}
|
||
return { ok: true, discarded, liveNodesPreserved };
|
||
},
|
||
/**
|
||
* Remove nodes created by one UI task in creation order. Removing an outer
|
||
* root first also removes its descendants; later UUIDs are therefore skipped
|
||
* without issuing noisy component-level undo operations.
|
||
*/
|
||
removeCreatedNodesForMcpUndo(...args) {
|
||
const requested = args[0];
|
||
const expectedSceneUuid = args[1];
|
||
if (!Array.isArray(requested) || requested.some((value) => typeof value !== "string")) {
|
||
return {
|
||
ok: false,
|
||
message: "removeCreatedNodesForMcpUndo requires an array of UUID strings.",
|
||
removed: [],
|
||
remaining: []
|
||
};
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, message: "No active scene.", removed: [], remaining: [...requested] };
|
||
}
|
||
if (typeof expectedSceneUuid === "string" && expectedSceneUuid.length > 0 && scene.uuid !== expectedSceneUuid) {
|
||
return {
|
||
ok: false,
|
||
terminal: true,
|
||
message: "The active scene changed after the UI transaction was created.",
|
||
removed: [],
|
||
remaining: [...requested]
|
||
};
|
||
}
|
||
const removed = [];
|
||
const forget = (node) => {
|
||
const nid = uuidToNid.get(node.uuid);
|
||
if (nid !== void 0) {
|
||
uuidToNid.delete(node.uuid);
|
||
nidToUuid.delete(nid);
|
||
}
|
||
for (const child of node.children) {
|
||
forget(child);
|
||
}
|
||
};
|
||
let failure = null;
|
||
for (const uuid of requested) {
|
||
try {
|
||
const node = findNodeByUuid(scene, uuid);
|
||
if (!node || node === scene) {
|
||
continue;
|
||
}
|
||
forget(node);
|
||
if (typeof node.removeFromParent === "function") {
|
||
node.removeFromParent();
|
||
} else {
|
||
node.parent = null;
|
||
}
|
||
node.destroy?.();
|
||
removed.push(uuid);
|
||
} catch (error) {
|
||
failure = error instanceof Error ? error.message : String(error);
|
||
break;
|
||
}
|
||
}
|
||
const remaining = requested.filter((uuid) => findNodeByUuid(scene, uuid) !== null);
|
||
if (failure || remaining.length > 0) {
|
||
return {
|
||
ok: false,
|
||
message: `removeCreatedNodesForMcpUndo was incomplete${failure ? `: ${failure}` : ""} (remaining=${remaining.length}).`,
|
||
removed,
|
||
remaining
|
||
};
|
||
}
|
||
return { ok: true, removed, remaining, sceneUuid: scene.uuid };
|
||
},
|
||
/** Drop mappings by a pre-snapshotted list of uuids (safe after subtree was removed). */
|
||
forgetNidMappingsByUuids(...args) {
|
||
const list = args[0];
|
||
if (!Array.isArray(list)) {
|
||
return { ok: true };
|
||
}
|
||
for (const v of list) {
|
||
if (typeof v !== "string") {
|
||
continue;
|
||
}
|
||
const nid = uuidToNid.get(v);
|
||
if (nid !== void 0) {
|
||
uuidToNid.delete(v);
|
||
nidToUuid.delete(nid);
|
||
}
|
||
}
|
||
return { ok: true };
|
||
},
|
||
/** Restore `uuid` ↔ `nid` after editor undo brought a deleted node back (MCP `delete_node` undo). */
|
||
restoreNidMapping(...args) {
|
||
const uuid = args[0];
|
||
const nid = args[1];
|
||
if (typeof uuid !== "string" || typeof nid !== "number" || !Number.isInteger(nid)) {
|
||
return { ok: false, message: "restoreNidMapping: invalid uuid or nid" };
|
||
}
|
||
const prevUuid = nidToUuid.get(nid);
|
||
if (prevUuid !== void 0 && prevUuid !== uuid) {
|
||
uuidToNid.delete(prevUuid);
|
||
}
|
||
const prevNid = uuidToNid.get(uuid);
|
||
if (prevNid !== void 0 && prevNid !== nid) {
|
||
nidToUuid.delete(prevNid);
|
||
}
|
||
uuidToNid.set(uuid, nid);
|
||
nidToUuid.set(nid, uuid);
|
||
return { ok: true };
|
||
},
|
||
prepareAddComponent(...args) {
|
||
const raw = args[0];
|
||
const nid = raw?.nid;
|
||
const componentTypeIn = raw?.componentType;
|
||
if (typeof nid !== "number" || !Number.isInteger(nid)) {
|
||
return { ok: false, error: "invalid_args", message: "nid must be an integer." };
|
||
}
|
||
if (typeof componentTypeIn !== "string" || !componentTypeIn.trim()) {
|
||
return { ok: false, error: "invalid_args", message: "componentType must be a non-empty string." };
|
||
}
|
||
const uuid = nidToUuid.get(nid);
|
||
if (!uuid) {
|
||
return { ok: false, error: "nid_not_found", message: "Unknown nid \u2014 call getHierarchy again." };
|
||
}
|
||
const componentType = resolveComponentTypeInput(componentTypeIn);
|
||
return { ok: true, uuid, componentType };
|
||
},
|
||
prepareRemoveComponent(...args) {
|
||
const raw = args[0];
|
||
const nid = raw?.nid;
|
||
const componentIndex = raw?.componentIndex;
|
||
if (typeof nid !== "number" || !Number.isInteger(nid)) {
|
||
return { ok: false, error: "invalid_args", message: "nid must be an integer." };
|
||
}
|
||
if (typeof componentIndex !== "number" || !Number.isInteger(componentIndex) || componentIndex < 0) {
|
||
return { ok: false, error: "invalid_args", message: "componentIndex must be a non-negative integer." };
|
||
}
|
||
const uuid = nidToUuid.get(nid);
|
||
if (!uuid) {
|
||
return { ok: false, error: "nid_not_found", message: "Unknown nid \u2014 call getHierarchy again." };
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, error: "no_scene", message: "No active scene." };
|
||
}
|
||
const node = findNodeByUuid(scene, uuid);
|
||
if (!node) {
|
||
return { ok: false, error: "node_missing", message: "Node uuid no longer in scene; call getHierarchy again." };
|
||
}
|
||
let comps = [];
|
||
try {
|
||
comps = node.getComponents(cc.Component);
|
||
} catch {
|
||
comps = [];
|
||
}
|
||
if (componentIndex >= comps.length) {
|
||
return {
|
||
ok: false,
|
||
error: "invalid_args",
|
||
message: `componentIndex ${componentIndex} out of range (node has ${comps.length} component(s)).`
|
||
};
|
||
}
|
||
const component = comps[componentIndex];
|
||
const componentType = shortComponentName(component, cc);
|
||
const componentUuid = typeof component.uuid === "string" && component.uuid.trim().length > 0 ? component.uuid.trim() : void 0;
|
||
return { ok: true, uuid, componentUuid, componentIndex, componentType };
|
||
},
|
||
prepareDuplicateNode(...args) {
|
||
const raw = args[0];
|
||
const nid = raw?.nid;
|
||
if (typeof nid !== "number" || !Number.isInteger(nid)) {
|
||
return { ok: false, error: "invalid_args", message: "nid must be an integer." };
|
||
}
|
||
const uuid = nidToUuid.get(nid);
|
||
if (!uuid) {
|
||
return { ok: false, error: "nid_not_found", message: "Unknown nid \u2014 call getHierarchy again." };
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, error: "no_scene", message: "No active scene." };
|
||
}
|
||
const node = findNodeByUuid(scene, uuid);
|
||
if (!node) {
|
||
return { ok: false, error: "node_missing", message: "Node uuid no longer in scene; call getHierarchy again." };
|
||
}
|
||
const parent = node.parent;
|
||
if (!parent) {
|
||
return { ok: false, error: "invalid_args", message: "Cannot duplicate scene root." };
|
||
}
|
||
return { ok: true, uuid, parentUuid: parent.uuid };
|
||
},
|
||
/**
|
||
* Live scene-graph child UUIDs (order matches editor hierarchy). Used by Main after `duplicate-node`
|
||
* when the IPC return value is empty — `query-node` on the parent often omits children in the inode dump.
|
||
*/
|
||
listChildUuidsUnderParent(...args) {
|
||
const raw0 = args[0];
|
||
if (typeof raw0 !== "string" || raw0.length === 0) {
|
||
return { ok: false, message: "parentUuid must be a non-empty string." };
|
||
}
|
||
const parentUuid = raw0;
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, message: "No active scene." };
|
||
}
|
||
const parent = findNodeByUuid(scene, parentUuid);
|
||
if (!parent) {
|
||
return { ok: false, message: "Parent node not found in scene graph." };
|
||
}
|
||
const uuids = [];
|
||
for (const child of parent.children) {
|
||
try {
|
||
if (child && typeof child.uuid === "string" && child.uuid.length > 0) {
|
||
uuids.push(child.uuid);
|
||
}
|
||
} catch {
|
||
}
|
||
}
|
||
return { ok: true, uuids };
|
||
},
|
||
/**
|
||
* Fallback duplication when editor IPC `scene.duplicate-node` is a no-op in some Creator builds.
|
||
* Clones the node in the live scene graph via `cc.instantiate` and appends it under the same parent.
|
||
*/
|
||
duplicateNodeFallback(...args) {
|
||
const raw0 = args[0];
|
||
if (typeof raw0 !== "string" || raw0.length === 0) {
|
||
return { ok: false, message: "uuid must be a non-empty string." };
|
||
}
|
||
const uuid = raw0;
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, message: "No active scene." };
|
||
}
|
||
const node = findNodeByUuid(scene, uuid);
|
||
if (!node) {
|
||
return { ok: false, message: "Node uuid no longer in scene; call getHierarchy again." };
|
||
}
|
||
const parent = node.parent;
|
||
if (!parent) {
|
||
return { ok: false, message: "Cannot duplicate scene root." };
|
||
}
|
||
const inst = cc.instantiate;
|
||
if (typeof inst !== "function") {
|
||
return { ok: false, message: "cc.instantiate is not available in this runtime." };
|
||
}
|
||
let clone = null;
|
||
try {
|
||
clone = inst(node);
|
||
} catch (e) {
|
||
const msg = e instanceof Error ? e.message : String(e);
|
||
return { ok: false, message: `cc.instantiate failed: ${msg}` };
|
||
}
|
||
if (!clone || typeof clone.uuid !== "string" || clone.uuid.length === 0) {
|
||
return { ok: false, message: "instantiate returned unexpected value." };
|
||
}
|
||
try {
|
||
const oldIdx = parent.children.findIndex((n) => n.uuid === node.uuid);
|
||
const addChild = parent.addChild;
|
||
if (typeof addChild === "function") {
|
||
addChild.call(parent, clone);
|
||
} else {
|
||
clone.parent = parent;
|
||
}
|
||
const setSiblingIndex = clone.setSiblingIndex;
|
||
if (typeof setSiblingIndex === "function" && oldIdx >= 0) {
|
||
setSiblingIndex.call(clone, oldIdx + 1);
|
||
}
|
||
} catch {
|
||
}
|
||
return { ok: true, uuid: clone.uuid };
|
||
},
|
||
/**
|
||
* Fallback remove path for Main `undo add_component` when scene IPC `remove-component` by index
|
||
* does not mutate node components in some Creator builds.
|
||
*/
|
||
removeLastComponentByType(...args) {
|
||
const rawUuid = args[0];
|
||
const rawType = args[1];
|
||
if (typeof rawUuid !== "string" || rawUuid.length === 0) {
|
||
return { ok: false, message: "uuid must be a non-empty string." };
|
||
}
|
||
if (typeof rawType !== "string" || rawType.length === 0) {
|
||
return { ok: false, message: "componentType must be a non-empty string." };
|
||
}
|
||
const normalized = rawType.trim().replace(/^cc\./, "").toLowerCase();
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, message: "No active scene." };
|
||
}
|
||
const node = findNodeByUuid(scene, rawUuid);
|
||
if (!node) {
|
||
return { ok: false, message: "Node uuid no longer in scene; call getHierarchy again." };
|
||
}
|
||
let comps = [];
|
||
try {
|
||
comps = node.getComponents(cc.Component);
|
||
} catch {
|
||
comps = [];
|
||
}
|
||
const beforeCount = comps.length;
|
||
const candidates = comps.filter((comp) => shortComponentName(comp, cc).toLowerCase() === normalized);
|
||
if (candidates.length === 0) {
|
||
return { ok: true, removed: false, beforeCount, afterCount: beforeCount };
|
||
}
|
||
const target = candidates[candidates.length - 1];
|
||
try {
|
||
if (typeof node.removeComponent === "function") {
|
||
node.removeComponent(target);
|
||
} else if (typeof target.destroy === "function") {
|
||
target.destroy();
|
||
} else {
|
||
return { ok: false, message: "Neither node.removeComponent nor component.destroy is available." };
|
||
}
|
||
} catch (e) {
|
||
const message = e instanceof Error ? e.message : String(e);
|
||
return { ok: false, message: `removeLastComponentByType failed: ${message}` };
|
||
}
|
||
let after = beforeCount;
|
||
try {
|
||
after = node.getComponents(cc.Component).length;
|
||
} catch {
|
||
}
|
||
return { ok: true, removed: after < beforeCount, beforeCount, afterCount: after };
|
||
},
|
||
/**
|
||
* Fallback add path for Main `undo remove_component` when scene IPC `create-component`
|
||
* does not mutate node components in some Creator builds.
|
||
*/
|
||
addLastComponentByType(...args) {
|
||
const rawUuid = args[0];
|
||
const rawType = args[1];
|
||
if (typeof rawUuid !== "string" || rawUuid.length === 0) {
|
||
return { ok: false, message: "uuid must be a non-empty string." };
|
||
}
|
||
if (typeof rawType !== "string" || rawType.length === 0) {
|
||
return { ok: false, message: "componentType must be a non-empty string." };
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, message: "No active scene." };
|
||
}
|
||
const node = findNodeByUuid(scene, rawUuid);
|
||
if (!node) {
|
||
return { ok: false, message: "Node uuid no longer in scene; call getHierarchy again." };
|
||
}
|
||
const ctor = resolveComponentCtor(rawType);
|
||
if (!ctor) {
|
||
return { ok: false, message: `Unable to resolve component constructor: ${rawType}` };
|
||
}
|
||
let before = [];
|
||
try {
|
||
before = node.getComponents(cc.Component);
|
||
} catch {
|
||
before = [];
|
||
}
|
||
const beforeCount = before.length;
|
||
try {
|
||
const addComponent = node.addComponent;
|
||
if (typeof addComponent !== "function") {
|
||
return { ok: false, message: "node.addComponent is not available in this runtime." };
|
||
}
|
||
addComponent.call(node, ctor);
|
||
} catch (e) {
|
||
const msg = e instanceof Error ? e.message : String(e);
|
||
return { ok: false, message: `addLastComponentByType failed: ${msg}` };
|
||
}
|
||
let after = [];
|
||
try {
|
||
after = node.getComponents(cc.Component);
|
||
} catch {
|
||
after = before;
|
||
}
|
||
const afterCount = after.length;
|
||
return { ok: true, added: afterCount > beforeCount, beforeCount, afterCount };
|
||
},
|
||
/**
|
||
* Returns live component totals for a node and a specific component short-name type.
|
||
* Used by Main to verify undo effects when `query-node` dumps are stale/noisy.
|
||
*/
|
||
getComponentStatsByType(...args) {
|
||
const rawUuid = args[0];
|
||
const rawType = args[1];
|
||
if (typeof rawUuid !== "string" || rawUuid.length === 0) {
|
||
return { ok: false, message: "uuid must be a non-empty string." };
|
||
}
|
||
if (typeof rawType !== "string" || rawType.length === 0) {
|
||
return { ok: false, message: "componentType must be a non-empty string." };
|
||
}
|
||
const normalized = rawType.trim().replace(/^cc\./, "").toLowerCase();
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, message: "No active scene." };
|
||
}
|
||
const node = findNodeByUuid(scene, rawUuid);
|
||
if (!node) {
|
||
return { ok: false, message: "Node uuid no longer in scene; call getHierarchy again." };
|
||
}
|
||
let comps = [];
|
||
try {
|
||
comps = node.getComponents(cc.Component);
|
||
} catch {
|
||
comps = [];
|
||
}
|
||
const totalCount = comps.length;
|
||
const typeCount = comps.filter((comp) => shortComponentName(comp, cc).toLowerCase() === normalized).length;
|
||
return { ok: true, totalCount, typeCount };
|
||
},
|
||
/** Exact live truth for component UUID removal/undo verification. */
|
||
getComponentUuidState(...args) {
|
||
const rawNodeUuid = args[0];
|
||
const rawComponentUuid = args[1];
|
||
if (typeof rawNodeUuid !== "string" || rawNodeUuid.length === 0) {
|
||
return { ok: false, message: "nodeUuid must be a non-empty string." };
|
||
}
|
||
if (typeof rawComponentUuid !== "string" || rawComponentUuid.length === 0) {
|
||
return { ok: false, message: "componentUuid must be a non-empty string." };
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, message: "No active scene." };
|
||
}
|
||
const node = findNodeByUuid(scene, rawNodeUuid);
|
||
if (!node) {
|
||
return { ok: false, message: "Node uuid no longer in scene; call getHierarchy again." };
|
||
}
|
||
let components = [];
|
||
try {
|
||
components = node.getComponents(cc.Component);
|
||
} catch (error) {
|
||
return {
|
||
ok: false,
|
||
message: error instanceof Error ? error.message : String(error)
|
||
};
|
||
}
|
||
const componentIndex = components.findIndex((component) => component.uuid === rawComponentUuid);
|
||
if (componentIndex < 0) {
|
||
return { ok: true, present: false };
|
||
}
|
||
return {
|
||
ok: true,
|
||
present: true,
|
||
componentIndex,
|
||
componentType: shortComponentName(components[componentIndex], cc)
|
||
};
|
||
},
|
||
/**
|
||
* Fallback reorder path when editor IPC `scene.move-node` is unavailable.
|
||
*/
|
||
setSiblingIndexFallback(...args) {
|
||
const rawUuid = args[0];
|
||
const rawIndex = args[1];
|
||
if (typeof rawUuid !== "string" || rawUuid.length === 0) {
|
||
return { ok: false, message: "uuid must be a non-empty string." };
|
||
}
|
||
if (typeof rawIndex !== "number" || !Number.isInteger(rawIndex) || rawIndex < 0) {
|
||
return { ok: false, message: "index must be a non-negative integer." };
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, message: "No active scene." };
|
||
}
|
||
const node = findNodeByUuid(scene, rawUuid);
|
||
if (!node || !node.parent) {
|
||
return { ok: false, message: "Node or parent not found in scene graph." };
|
||
}
|
||
const siblings = node.parent.children;
|
||
const previousIndex = siblings.findIndex((n) => n.uuid === node.uuid);
|
||
if (previousIndex < 0) {
|
||
return { ok: false, message: "Node is not attached to parent children list." };
|
||
}
|
||
const maxIndex = Math.max(0, siblings.length - 1);
|
||
const index = Math.max(0, Math.min(maxIndex, rawIndex));
|
||
if (typeof node.setSiblingIndex !== "function") {
|
||
return { ok: false, message: "node.setSiblingIndex is not available in this runtime." };
|
||
}
|
||
try {
|
||
node.setSiblingIndex(index);
|
||
} catch (e) {
|
||
const message = e instanceof Error ? e.message : String(e);
|
||
return { ok: false, message: `setSiblingIndexFallback failed: ${message}` };
|
||
}
|
||
return { ok: true, appliedIndex: index, previousIndex };
|
||
},
|
||
setComponentScalarPropertyDirect(...args) {
|
||
const rawUuid = args[0];
|
||
const rawIndex = args[1];
|
||
const rawProperty = args[2];
|
||
const rawValue = args[3];
|
||
if (typeof rawUuid !== "string" || rawUuid.length === 0) {
|
||
return { ok: false, message: "uuid must be a non-empty string." };
|
||
}
|
||
if (typeof rawIndex !== "number" || !Number.isInteger(rawIndex) || rawIndex < 0) {
|
||
return { ok: false, message: "componentIndex must be a non-negative integer." };
|
||
}
|
||
if (typeof rawProperty !== "string" || rawProperty.trim().length === 0) {
|
||
return { ok: false, message: "property must be a non-empty string." };
|
||
}
|
||
const requestedValue = runtimeDirectSnapshot(rawValue);
|
||
if (requestedValue === void 0) {
|
||
return {
|
||
ok: false,
|
||
message: "runtime-direct fallback supports scalars, Color, Vec2, and Size values only."
|
||
};
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, message: "No active scene." };
|
||
}
|
||
const node = findNodeByUuid(scene, rawUuid);
|
||
if (!node) {
|
||
return { ok: false, message: "Node uuid no longer in scene; call getHierarchy again." };
|
||
}
|
||
let comps = [];
|
||
try {
|
||
comps = node.getComponents(cc.Component);
|
||
} catch {
|
||
comps = [];
|
||
}
|
||
if (rawIndex >= comps.length) {
|
||
return { ok: false, message: `componentIndex ${rawIndex} is out of range.` };
|
||
}
|
||
const component = comps[rawIndex];
|
||
const prop = rawProperty.trim();
|
||
const field = [prop, `_${prop}`].find((candidate) => candidate in component);
|
||
if (!field) {
|
||
return { ok: false, message: `component property is not available at runtime: ${prop}` };
|
||
}
|
||
const beforeValue = runtimeDirectSnapshot(component[field]);
|
||
if (beforeValue === void 0) {
|
||
return { ok: false, message: `component property is not supported by runtime-direct fallback: ${prop}` };
|
||
}
|
||
try {
|
||
component[field] = materializeRuntimeDirectValue(requestedValue);
|
||
} catch (e) {
|
||
const message = e instanceof Error ? e.message : String(e);
|
||
return { ok: false, message: `runtime-direct property assignment failed: ${message}` };
|
||
}
|
||
const afterValue = runtimeDirectSnapshot(component[field]);
|
||
if (afterValue === void 0) {
|
||
return { ok: false, message: `component property became unsupported after assignment: ${prop}` };
|
||
}
|
||
return {
|
||
ok: true,
|
||
beforeValue,
|
||
afterValue,
|
||
changed: JSON.stringify(beforeValue) !== JSON.stringify(afterValue)
|
||
};
|
||
},
|
||
/**
|
||
* Read asset-ref UUID from live component instance for known asset properties.
|
||
* Used by Main to verify typed direct asset writes and their local undo.
|
||
*/
|
||
getComponentAssetRefUuid(...args) {
|
||
const rawUuid = args[0];
|
||
const rawComponentType = args[1];
|
||
const rawProperty = args[2];
|
||
if (typeof rawUuid !== "string" || rawUuid.length === 0) {
|
||
return { ok: false, message: "uuid must be a non-empty string." };
|
||
}
|
||
if (typeof rawComponentType !== "string" || rawComponentType.length === 0) {
|
||
return { ok: false, message: "componentType must be a non-empty string." };
|
||
}
|
||
if (typeof rawProperty !== "string" || rawProperty.length === 0) {
|
||
return { ok: false, message: "property must be a non-empty string." };
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, message: "No active scene." };
|
||
}
|
||
const node = findNodeByUuid(scene, rawUuid);
|
||
if (!node) {
|
||
return { ok: false, message: "Node uuid no longer in scene; call getHierarchy again." };
|
||
}
|
||
let comps = [];
|
||
try {
|
||
comps = node.getComponents(cc.Component);
|
||
} catch {
|
||
comps = [];
|
||
}
|
||
const normType = rawComponentType.trim().replace(/^cc\./, "").toLowerCase();
|
||
const prop = rawProperty.trim();
|
||
const typed = comps.filter((c) => shortComponentName(c, cc).toLowerCase() === normType);
|
||
if (typed.length === 0) {
|
||
return { ok: false, message: `No component of type ${rawComponentType} on target node.` };
|
||
}
|
||
const target = typed[typed.length - 1];
|
||
const candidates = [prop, `_${prop}`];
|
||
for (const k of candidates) {
|
||
if (k in target) {
|
||
return readAssetReferenceUuid(target[k], `${rawComponentType}.${k}`);
|
||
}
|
||
}
|
||
return { ok: false, message: `Asset property ${rawComponentType}.${prop} is not available at runtime.` };
|
||
},
|
||
async setComponentAssetRefDirect(...args) {
|
||
const rawUuid = args[0];
|
||
const rawComponentType = args[1];
|
||
const rawProperty = args[2];
|
||
const rawAssetUuid = args[3];
|
||
if (typeof rawUuid !== "string" || rawUuid.length === 0) {
|
||
return { ok: false, message: "uuid must be a non-empty string." };
|
||
}
|
||
if (typeof rawComponentType !== "string" || rawComponentType.length === 0) {
|
||
return { ok: false, message: "componentType must be a non-empty string." };
|
||
}
|
||
if (typeof rawProperty !== "string" || rawProperty.length === 0) {
|
||
return { ok: false, message: "property must be a non-empty string." };
|
||
}
|
||
if (typeof rawAssetUuid !== "string" || rawAssetUuid.length === 0) {
|
||
return { ok: false, message: "assetUuid must be a non-empty string." };
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, message: "No active scene." };
|
||
}
|
||
const node = findNodeByUuid(scene, rawUuid);
|
||
if (!node) {
|
||
return { ok: false, message: "Node uuid no longer in scene; call getHierarchy again." };
|
||
}
|
||
let comps = [];
|
||
try {
|
||
comps = node.getComponents(cc.Component);
|
||
} catch {
|
||
comps = [];
|
||
}
|
||
const normType = rawComponentType.trim().replace(/^cc\./, "").toLowerCase();
|
||
const typed = comps.filter((c) => shortComponentName(c, cc).toLowerCase() === normType);
|
||
if (typed.length === 0) {
|
||
return { ok: false, message: `No component of type ${rawComponentType} on target node.` };
|
||
}
|
||
const comp = typed[typed.length - 1];
|
||
const prop = rawProperty.trim();
|
||
const propCandidates = [prop, `_${prop}`];
|
||
const field = propCandidates.find((candidate) => candidate in comp);
|
||
if (!field) {
|
||
return { ok: false, message: `Asset property ${rawComponentType}.${prop} is not available at runtime.` };
|
||
}
|
||
const beforeValue = comp[field];
|
||
const before = readAssetReferenceUuid(beforeValue, `${rawComponentType}.${field}`);
|
||
if (!before.ok) {
|
||
return before;
|
||
}
|
||
const beforeUuid = before.uuid;
|
||
const am = getCcAssetManager();
|
||
let asset;
|
||
try {
|
||
asset = await loadCcAssetByUuid(am, rawAssetUuid);
|
||
} catch (error) {
|
||
const message = error instanceof Error ? error.message : String(error);
|
||
return { ok: false, message: `Unable to load asset UUID ${rawAssetUuid}: ${message}` };
|
||
}
|
||
const kindError = validateAssetKind(asset, rawComponentType, prop, cc);
|
||
if (kindError) {
|
||
return { ok: false, message: kindError };
|
||
}
|
||
try {
|
||
comp[field] = asset;
|
||
} catch (e) {
|
||
const message = e instanceof Error ? e.message : String(e);
|
||
return { ok: false, message: `direct asset assignment failed: ${message}` };
|
||
}
|
||
const after = readAssetReferenceUuid(comp[field], `${rawComponentType}.${field}`);
|
||
if (!after.ok) {
|
||
try {
|
||
comp[field] = beforeValue;
|
||
} catch {
|
||
return { ok: false, message: `${after.message} Rollback to the prior asset reference also failed.` };
|
||
}
|
||
return { ok: false, message: `${after.message} The prior asset reference was restored.` };
|
||
}
|
||
const afterUuid = after.uuid;
|
||
if (afterUuid !== rawAssetUuid) {
|
||
try {
|
||
comp[field] = beforeValue;
|
||
} catch {
|
||
return {
|
||
ok: false,
|
||
message: `Runtime asset assignment did not stick (expected ${rawAssetUuid}, observed ${afterUuid ?? "null"}) and rollback to the prior asset reference failed.`
|
||
};
|
||
}
|
||
return {
|
||
ok: false,
|
||
message: `Runtime asset assignment did not stick: expected ${rawAssetUuid}, observed ${afterUuid ?? "null"}. The prior asset reference was restored.`
|
||
};
|
||
}
|
||
return { ok: true, beforeUuid, afterUuid, changed: beforeUuid !== afterUuid };
|
||
},
|
||
setComponentAssetRefFromScene(...args) {
|
||
const rawUuid = args[0];
|
||
const rawComponentType = args[1];
|
||
const rawProperty = args[2];
|
||
const rawAssetUuid = args[3];
|
||
if (typeof rawUuid !== "string" || rawUuid.length === 0) {
|
||
return { ok: false, message: "uuid must be a non-empty string." };
|
||
}
|
||
if (typeof rawComponentType !== "string" || rawComponentType.length === 0) {
|
||
return { ok: false, message: "componentType must be a non-empty string." };
|
||
}
|
||
if (typeof rawProperty !== "string" || rawProperty.length === 0) {
|
||
return { ok: false, message: "property must be a non-empty string." };
|
||
}
|
||
if (typeof rawAssetUuid !== "string" || rawAssetUuid.length === 0) {
|
||
return { ok: false, message: "assetUuid must be a non-empty string." };
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, message: "No active scene." };
|
||
}
|
||
const normType = rawComponentType.trim().replace(/^cc\./, "").toLowerCase();
|
||
const prop = rawProperty.trim();
|
||
const fieldCandidates = [prop, `_${prop}`];
|
||
const targetNode = findNodeByUuid(scene, rawUuid);
|
||
if (!targetNode) {
|
||
return { ok: false, message: "Node uuid no longer in scene; call getHierarchy again." };
|
||
}
|
||
let targetComps = [];
|
||
try {
|
||
targetComps = targetNode.getComponents(cc.Component);
|
||
} catch {
|
||
targetComps = [];
|
||
}
|
||
const targetTyped = targetComps.filter((c) => shortComponentName(c, cc).toLowerCase() === normType);
|
||
if (targetTyped.length === 0) {
|
||
return { ok: false, message: `No component of type ${rawComponentType} on target node.` };
|
||
}
|
||
const targetComp = targetTyped[targetTyped.length - 1];
|
||
let sourceAssetRef = null;
|
||
const walk = (node) => {
|
||
let comps = [];
|
||
try {
|
||
comps = node.getComponents(cc.Component);
|
||
} catch {
|
||
comps = [];
|
||
}
|
||
for (const comp of comps) {
|
||
if (shortComponentName(comp, cc).toLowerCase() !== normType) {
|
||
continue;
|
||
}
|
||
const c = comp;
|
||
for (const k of fieldCandidates) {
|
||
const v = c[k];
|
||
if (v && typeof v === "object" && !Array.isArray(v)) {
|
||
const u = v.uuid;
|
||
if (typeof u === "string" && u === rawAssetUuid) {
|
||
sourceAssetRef = v;
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
for (const child of node.children) {
|
||
if (walk(child)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
};
|
||
walk(scene);
|
||
if (!sourceAssetRef) {
|
||
return {
|
||
ok: false,
|
||
message: `Asset UUID ${rawAssetUuid} is not currently loaded on any ${rawComponentType}.${prop} in scene.`
|
||
};
|
||
}
|
||
const targetField = fieldCandidates.find((candidate) => candidate in targetComp);
|
||
if (!targetField) {
|
||
return { ok: false, message: `Asset property ${rawComponentType}.${prop} is not available at runtime.` };
|
||
}
|
||
const beforeValue = targetComp[targetField];
|
||
const before = readAssetReferenceUuid(
|
||
beforeValue,
|
||
`${rawComponentType}.${targetField}`
|
||
);
|
||
if (!before.ok) {
|
||
return before;
|
||
}
|
||
const beforeUuid = before.uuid;
|
||
try {
|
||
targetComp[targetField] = sourceAssetRef;
|
||
} catch (e) {
|
||
const message = e instanceof Error ? e.message : String(e);
|
||
return { ok: false, message: `setComponentAssetRefFromScene failed: ${message}` };
|
||
}
|
||
const after = readAssetReferenceUuid(
|
||
targetComp[targetField],
|
||
`${rawComponentType}.${targetField}`
|
||
);
|
||
if (!after.ok) {
|
||
try {
|
||
targetComp[targetField] = beforeValue;
|
||
} catch {
|
||
return { ok: false, message: `${after.message} Rollback to the prior asset reference also failed.` };
|
||
}
|
||
return { ok: false, message: `${after.message} The prior asset reference was restored.` };
|
||
}
|
||
const afterUuid = after.uuid;
|
||
if (afterUuid !== rawAssetUuid) {
|
||
try {
|
||
targetComp[targetField] = beforeValue;
|
||
} catch {
|
||
return {
|
||
ok: false,
|
||
message: `Scene asset assignment did not stick (expected ${rawAssetUuid}, observed ${afterUuid ?? "null"}) and rollback to the prior asset reference failed.`
|
||
};
|
||
}
|
||
return {
|
||
ok: false,
|
||
message: `Scene asset assignment did not stick: expected ${rawAssetUuid}, observed ${afterUuid ?? "null"}. The prior asset reference was restored.`
|
||
};
|
||
}
|
||
return { ok: true, beforeUuid, afterUuid, changed: beforeUuid !== afterUuid };
|
||
},
|
||
/**
|
||
* Clear (set to null) an asset-ref field on the last matching component instance.
|
||
* Used by Main to implement undo when the previous asset UUID is unknown/unobservable.
|
||
*/
|
||
clearComponentAssetRef(...args) {
|
||
const rawUuid = args[0];
|
||
const rawComponentType = args[1];
|
||
const rawProperty = args[2];
|
||
if (typeof rawUuid !== "string" || rawUuid.length === 0) {
|
||
return { ok: false, message: "uuid must be a non-empty string." };
|
||
}
|
||
if (typeof rawComponentType !== "string" || rawComponentType.length === 0) {
|
||
return { ok: false, message: "componentType must be a non-empty string." };
|
||
}
|
||
if (typeof rawProperty !== "string" || rawProperty.length === 0) {
|
||
return { ok: false, message: "property must be a non-empty string." };
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, message: "No active scene." };
|
||
}
|
||
const node = findNodeByUuid(scene, rawUuid);
|
||
if (!node) {
|
||
return { ok: false, message: "Node uuid no longer in scene; call getHierarchy again." };
|
||
}
|
||
let comps = [];
|
||
try {
|
||
comps = node.getComponents(cc.Component);
|
||
} catch {
|
||
comps = [];
|
||
}
|
||
const normType = rawComponentType.trim().replace(/^cc\./, "").toLowerCase();
|
||
const typed = comps.filter((c) => shortComponentName(c, cc).toLowerCase() === normType);
|
||
if (typed.length === 0) {
|
||
return { ok: false, message: `No component of type ${rawComponentType} on target node.` };
|
||
}
|
||
const comp = typed[typed.length - 1];
|
||
const prop = rawProperty.trim();
|
||
const candidates = [prop, `_${prop}`];
|
||
const field = candidates.find((candidate) => candidate in comp);
|
||
if (!field) {
|
||
return { ok: false, message: `Asset property ${rawComponentType}.${prop} is not available at runtime.` };
|
||
}
|
||
const beforeValue = comp[field];
|
||
const before = readAssetReferenceUuid(beforeValue, `${rawComponentType}.${field}`);
|
||
if (!before.ok) {
|
||
return before;
|
||
}
|
||
const beforeUuid = before.uuid;
|
||
try {
|
||
comp[field] = null;
|
||
} catch (e) {
|
||
const message = e instanceof Error ? e.message : String(e);
|
||
return { ok: false, message: `clearComponentAssetRef failed: ${message}` };
|
||
}
|
||
const after = readAssetReferenceUuid(comp[field], `${rawComponentType}.${field}`);
|
||
if (!after.ok) {
|
||
try {
|
||
comp[field] = beforeValue;
|
||
} catch {
|
||
return { ok: false, message: `${after.message} Rollback to the prior asset reference also failed.` };
|
||
}
|
||
return { ok: false, message: `${after.message} The prior asset reference was restored.` };
|
||
}
|
||
const afterUuid = after.uuid;
|
||
if (afterUuid !== null) {
|
||
try {
|
||
comp[field] = beforeValue;
|
||
} catch {
|
||
return {
|
||
ok: false,
|
||
message: `Asset clear did not stick (observed ${afterUuid}) and rollback to the prior reference failed.`
|
||
};
|
||
}
|
||
return {
|
||
ok: false,
|
||
message: `Asset clear did not stick (observed ${afterUuid}); the prior reference was restored.`
|
||
};
|
||
}
|
||
return { ok: true, beforeUuid, afterUuid, changed: beforeUuid !== afterUuid };
|
||
},
|
||
getNodeActiveState(...args) {
|
||
const rawUuid = args[0];
|
||
if (typeof rawUuid !== "string" || rawUuid.length === 0) {
|
||
return { ok: false, message: "uuid must be a non-empty string." };
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, message: "No active scene." };
|
||
}
|
||
const node = findNodeByUuid(scene, rawUuid);
|
||
if (!node) {
|
||
return { ok: false, message: "Node uuid no longer in scene; call getHierarchy again." };
|
||
}
|
||
return { ok: true, active: !!node.active };
|
||
},
|
||
prepareSetNodeSiblingIndex(...args) {
|
||
const raw = args[0];
|
||
const nid = raw?.nid;
|
||
const indexIn = raw?.index;
|
||
if (typeof nid !== "number" || !Number.isInteger(nid)) {
|
||
return { ok: false, error: "invalid_args", message: "nid must be an integer." };
|
||
}
|
||
if (typeof indexIn !== "number" || !Number.isInteger(indexIn) || indexIn < 0) {
|
||
return { ok: false, error: "invalid_args", message: "index must be a non-negative integer." };
|
||
}
|
||
const uuid = nidToUuid.get(nid);
|
||
if (!uuid) {
|
||
return { ok: false, error: "nid_not_found", message: "Unknown nid \u2014 call getHierarchy again." };
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, error: "no_scene", message: "No active scene." };
|
||
}
|
||
const node = findNodeByUuid(scene, uuid);
|
||
if (!node) {
|
||
return { ok: false, error: "node_missing", message: "Node uuid no longer in scene; call getHierarchy again." };
|
||
}
|
||
const parent = node.parent;
|
||
if (!parent) {
|
||
return { ok: false, error: "invalid_args", message: "Cannot reorder scene root." };
|
||
}
|
||
const siblings = parent.children;
|
||
const previousIndex = siblings.findIndex((n) => n.uuid === node.uuid);
|
||
if (previousIndex < 0) {
|
||
return { ok: false, error: "node_missing", message: "Node is not attached to parent children list." };
|
||
}
|
||
const maxIndex = Math.max(0, siblings.length - 1);
|
||
const index = Math.max(0, Math.min(maxIndex, indexIn));
|
||
return { ok: true, uuid, parentUuid: parent.uuid, index, previousIndex, noOp: previousIndex === index };
|
||
},
|
||
/**
|
||
* Validates whitelist + coerces `value`. Main reads property dump via `query-node` and calls `set-property`.
|
||
* `componentIndex` matches `inspectNode` / `getComponents` order.
|
||
*/
|
||
prepareSetComponentProperty(...args) {
|
||
const raw = args[0];
|
||
const nid = raw?.nid;
|
||
const componentIndex = raw?.componentIndex;
|
||
const property = raw?.property;
|
||
const valueIn = raw?.value;
|
||
if (typeof nid !== "number" || !Number.isFinite(nid) || !Number.isInteger(nid)) {
|
||
return { ok: false, error: "invalid_args", message: "nid must be an integer (use getHierarchy NIDs)." };
|
||
}
|
||
if (typeof componentIndex !== "number" || !Number.isInteger(componentIndex) || componentIndex < 0) {
|
||
return { ok: false, error: "invalid_args", message: "componentIndex must be a non-negative integer." };
|
||
}
|
||
if (typeof property !== "string" || !property.trim()) {
|
||
return { ok: false, error: "invalid_args", message: "property must be a non-empty string." };
|
||
}
|
||
const prop = property.trim();
|
||
const uuid = nidToUuid.get(nid);
|
||
if (!uuid) {
|
||
return {
|
||
ok: false,
|
||
error: "nid_not_found",
|
||
message: "Unknown nid \u2014 scene may have changed; call getHierarchy again."
|
||
};
|
||
}
|
||
const cc = getCC();
|
||
const scene = cc.director.getScene();
|
||
if (!scene) {
|
||
return { ok: false, error: "no_scene", message: "No active scene." };
|
||
}
|
||
if (scene.uuid !== currentSceneUuid) {
|
||
return {
|
||
ok: false,
|
||
error: "stale_session",
|
||
message: "Scene changed since last getHierarchy; refresh NID map first."
|
||
};
|
||
}
|
||
const node = findNodeByUuid(scene, uuid);
|
||
if (!node) {
|
||
return {
|
||
ok: false,
|
||
error: "node_missing",
|
||
message: "Node uuid no longer in scene; call getHierarchy again."
|
||
};
|
||
}
|
||
let comps = [];
|
||
try {
|
||
comps = node.getComponents(cc.Component);
|
||
} catch {
|
||
comps = [];
|
||
}
|
||
if (componentIndex >= comps.length) {
|
||
return {
|
||
ok: false,
|
||
error: "invalid_args",
|
||
message: `componentIndex ${componentIndex} out of range (node has ${comps.length} component(s)).`
|
||
};
|
||
}
|
||
const shortName = shortComponentName(comps[componentIndex], cc);
|
||
const schemasForType = COMPONENT_SCHEMAS[shortName] ?? {};
|
||
const schema = schemasForType[prop];
|
||
if (!schema) {
|
||
const allowedType = Object.keys(schemasForType);
|
||
return {
|
||
ok: false,
|
||
error: "not_whitelisted",
|
||
message: `\u5C5E\u6027\u672A\u5728\u5B89\u5168\u767D\u540D\u5355\u4E2D\u914D\u7F6E\u3002\u7EC4\u4EF6 ${shortName} \u5F53\u524D\u652F\u6301\u7684\u5C5E\u6027\u6709\uFF1A${allowedType.length > 0 ? allowedType.join(", ") : "(none)"}`
|
||
};
|
||
}
|
||
let coerced;
|
||
try {
|
||
coerced = schema.coercer(valueIn);
|
||
} catch (e) {
|
||
const message = e instanceof Error ? e.message : String(e);
|
||
return { ok: false, error: "invalid_args", message };
|
||
}
|
||
const dumpProp = schema.dumpProp ?? prop;
|
||
const path2 = `__comps__.${componentIndex}.${dumpProp}`;
|
||
return { ok: true, uuid: node.uuid, path: path2, componentType: shortName, value: coerced };
|
||
}
|
||
};
|
||
var cocosCjsExports = typeof exports === "object" && exports ? exports : null;
|
||
if (cocosCjsExports) {
|
||
cocosCjsExports.methods = methods;
|
||
}
|
||
// Annotate the CommonJS export names for ESM import in node:
|
||
0 && (module.exports = {
|
||
methods
|
||
});
|