Files
behavior3editor/src/misc/b3util.ts
T
2025-03-17 22:55:56 +08:00

778 lines
20 KiB
TypeScript

import * as fs from "fs";
import { NodeDef } from "../behavior3/src/behavior3";
import {
ImportDef,
isBoolType,
isEnumType,
isExprType,
isFloatType,
isIntType,
isJsonType,
isStringType,
NodeArg,
NodeModel,
TreeGraphData,
TreeModel,
VarDef,
} from "./b3type";
import Path from "./path";
import { readJson, readTree } from "./util";
export class NodeDefs extends Map<string, NodeDef> {
get(key: string): NodeDef {
return super.get(key) ?? unknownNodeDef;
}
}
export let nodeDefs: NodeDefs = new NodeDefs();
export let groupDefs: string[] = [];
export let usingGroups: Record<string, boolean> = {};
export let usingVars: Record<string, boolean> = {};
let workdir: string = "";
let alertError: (msg: string, duration?: number) => void = () => {};
const unknownNodeDef: NodeDef = {
name: "unknown",
desc: "",
type: "Action",
};
export const initWorkdir = (path: string, handler: typeof alertError) => {
workdir = path;
alertError = handler;
const nodeDefData = readJson(`${workdir}/node-config.b3-setting`) as NodeDef[];
const groups: Set<string> = new Set();
nodeDefs = new NodeDefs();
for (const v of nodeDefData) {
nodeDefs.set(v.name, v);
const group = v.type.match(/\((\w+)\)/)?.[1];
if (group) {
v.group = group;
groups.add(group);
}
}
groupDefs = Array.from(groups);
};
export const updateUsingGroups = (group: string[]) => {
usingGroups = {};
for (const g of group) {
usingGroups[g] = true;
}
};
export const updateUsingVars = (vars: VarDef[]) => {
usingVars = {};
for (const v of vars) {
usingVars[v.name] = true;
}
};
export const isValidVariableName = (name: string) => {
return /^[a-zA-Z_]/.test(name);
};
export const isSubtreeRoot = (data: TreeGraphData) => {
return data.path && data.id.toString() !== "1";
};
export const isNodeEqual = (node1: NodeModel, node2: NodeModel) => {
if (
node1.name === node2.name &&
node1.desc === node2.desc &&
node1.path === node2.path &&
node1.debug === node2.debug &&
node1.disabled === node2.disabled
) {
const def = nodeDefs.get(node1.name);
for (const arg of def.args ?? []) {
if (node1.args?.[arg.name] !== node2.args?.[arg.name]) {
return false;
}
}
if (def.input?.length) {
const len = Math.max(node1.input?.length ?? 0, node2.input?.length ?? 0);
for (let i = 0; i < len; i++) {
if (node1.input?.[i] !== node2.input?.[i]) {
return false;
}
}
}
if (def.output?.length) {
const len = Math.max(node1.output?.length ?? 0, node2.output?.length ?? 0);
for (let i = 0; i < len; i++) {
if (node1.output?.[i] !== node2.output?.[i]) {
return false;
}
}
}
return true;
}
return false;
};
const error = (data: NodeModel | TreeGraphData, msg: string) => {
console.error(`check ${data.id}|${data.name}: ${msg}`);
};
export const getNodeArgRawType = (arg: NodeArg) => {
return arg.type.match(/^\w+/)![0] as NodeArg["type"];
};
export const isNodeArgArray = (arg: NodeArg) => {
return arg.type.includes("[]");
};
export const isNodeArgOptional = (arg: NodeArg) => {
return arg.type.includes("?");
};
export const checkNodeArgValue = (
data: NodeModel | TreeGraphData,
arg: NodeArg,
value: unknown,
verbose?: boolean
) => {
let hasError = false;
const type = getNodeArgRawType(arg);
if (isFloatType(type)) {
const isNumber = typeof value === "number";
const isOptional = value === undefined && isNodeArgOptional(arg);
if (!(isNumber || isOptional)) {
if (verbose) {
error(data, `'${arg.name}=${JSON.stringify(value)}' is not a number`);
}
hasError = true;
}
} else if (isIntType(type)) {
const isInt = typeof value === "number" && value === Math.floor(value);
const isOptional = value === undefined && isNodeArgOptional(arg);
if (!(isInt || isOptional)) {
if (verbose) {
error(data, `'${arg.name}=${JSON.stringify(value)}' is not a int`);
}
hasError = true;
}
} else if (isStringType(type)) {
const isString = typeof value === "string" && value;
const isOptional = (value === undefined || value === "") && isNodeArgOptional(arg);
if (!(isString || isOptional)) {
if (verbose) {
error(data, `'${arg.name}=${JSON.stringify(value)}' is not a string`);
}
hasError = true;
}
} else if (isEnumType(type)) {
const isEnum = !!arg.options?.find((option) => option.value === value);
const isOptional = value === undefined && isNodeArgOptional(arg);
if (!(isEnum || isOptional)) {
if (verbose) {
error(data, `'${arg.name}=${JSON.stringify(value)}' is not a one of the option values`);
}
hasError = true;
}
} else if (isExprType(type)) {
const isExpr = typeof value === "string" && value;
const isOptional = (value === undefined || value === "") && isNodeArgOptional(arg);
if (!(isExpr || isOptional)) {
if (verbose) {
error(data, `'${arg.name}=${JSON.stringify(value)}' is not an expr string`);
}
hasError = true;
}
} else if (isJsonType(type)) {
const isJson = value !== undefined && value !== "";
const isOptional = isNodeArgOptional(arg);
if (!(isJson || isOptional)) {
if (verbose) {
error(data, `'${arg.name}=${value}' is not an invalid object`);
}
hasError = true;
}
} else if (isBoolType(type)) {
const isBool = typeof value === "boolean" || value === undefined;
if (!isBool) {
if (verbose) {
error(data, `'${arg.name}=${JSON.stringify(value)}' is not a boolean`);
}
hasError = true;
}
} else {
error(data, `unknown arg type '${arg.type}'`);
}
return !hasError;
};
export const checkNodeArg = (
data: NodeModel | TreeGraphData,
conf: NodeDef,
i: number,
verbose?: boolean
) => {
let hasError = false;
const arg = conf.args![i] as NodeArg;
const value = data.args?.[arg.name];
if (isNodeArgArray(arg)) {
if (!Array.isArray(value) || (!isNodeArgOptional(arg) && value.length === 0)) {
if (verbose) {
error(data, `'${arg.name}=${JSON.stringify(value)}' is not an array or empty array`);
}
hasError = true;
} else {
for (let j = 0; j < value.length; j++) {
if (!checkNodeArgValue(data, arg, value[j], verbose)) {
hasError = true;
}
}
}
} else if (!checkNodeArgValue(data, arg, value, verbose)) {
hasError = true;
}
if (arg.oneof !== undefined) {
const idx = conf.input?.findIndex((v) => v.startsWith(arg.oneof!)) ?? -1;
if (!checkOneof(data.args?.[arg.name], data.input?.[idx])) {
if (verbose) {
error(
data,
`only one is allowed for between argument '${arg.name}' and input '${data.input?.[idx]}'`
);
}
hasError = true;
}
}
return !hasError;
};
export const checkOneof = (argValue: unknown, inputValue: unknown) => {
argValue = argValue === undefined ? "" : argValue;
inputValue = inputValue ?? "";
return (argValue !== "" && inputValue === "") || (argValue === "" && inputValue !== "");
};
export const checkNodeData = (data: NodeModel | null | undefined) => {
if (!data) {
return false;
}
const conf = nodeDefs.get(data.name);
if (conf.name === unknownNodeDef.name) {
error(data, `undefined node: ${data.name}`);
return false;
}
let hasError = false;
if (conf.children !== undefined && conf.children !== -1) {
const count = data.children?.length || 0;
if (conf.children !== count) {
hasError = true;
error(data, `expect ${conf.children} children, but got ${count}`);
}
}
let hasVaridicInput = false;
if (conf.input) {
for (let i = 0; i < conf.input.length; i++) {
if (!data.input) {
data.input = [];
}
if (!data.input[i]) {
data.input[i] = "";
}
if (data.input[i] && !isValidVariableName(data.input[i])) {
error(
data,
`input field '${data.input[i]}' is not a valid variable name,` +
`should start with a letter or underscore`
);
hasError = true;
}
if (!isValidInputOrOutput(conf.input, data.input, i)) {
error(data, `intput field '${conf.input[i]}' is required`);
hasError = true;
}
if (i === conf.input.length - 1 && conf.input.at(-1)?.endsWith("...")) {
hasVaridicInput = true;
}
}
}
if (data.input && !hasVaridicInput) {
data.input.length = conf.input?.length || 0;
}
let hasVaridicOutput = false;
if (conf.output) {
for (let i = 0; i < conf.output.length; i++) {
if (!data.output) {
data.output = [];
}
if (!data.output[i]) {
data.output[i] = "";
}
if (data.output[i] && !isValidVariableName(data.output[i])) {
error(
data,
`output field '${data.output[i]}' is not a valid variable name,` +
`should start with a letter or underscore`
);
hasError = true;
}
if (!isValidInputOrOutput(conf.output, data.output, i)) {
error(data, `output field '${conf.output[i]}' is required`);
hasError = true;
}
if (i === conf.output.length - 1 && conf.output.at(-1)?.endsWith("...")) {
hasVaridicOutput = true;
}
}
}
if (data.output && !hasVaridicOutput) {
data.output.length = conf.output?.length || 0;
}
if (conf.args) {
const args: { [k: string]: unknown } = {};
for (let i = 0; i < conf.args.length; i++) {
const key = conf.args[i].name;
if (data.args && data.args[key] === undefined && conf.args[i].default !== undefined) {
data.args[key] = conf.args[i].default;
}
const value = data.args?.[key];
if (value !== undefined) {
args[key] = value;
}
if (!checkNodeArg(data, conf, i, true)) {
hasError = true;
}
}
data.args = args;
}
if (data.children) {
for (const child of data.children) {
if (!checkNodeData(child)) {
hasError = true;
}
}
} else {
data.children = [];
}
return !hasError;
};
export const copyFromNode = (data: TreeGraphData, node: NodeModel) => {
data.name = node.name;
data.debug = node.debug;
data.disabled = node.disabled;
data.desc = node.desc;
data.path = node.path;
data.args = node.args;
data.input = node.input;
data.output = node.output;
};
const parsingStack: string[] = [];
export const createNode = (data: TreeGraphData, includeChildren: boolean = true) => {
const node: NodeModel = {
id: Number(data.id),
name: data.name,
desc: data.desc,
path: data.path,
debug: data.debug,
disabled: data.disabled,
};
if (data.input) {
node.input = [];
for (const v of data.input) {
node.input.push(v ?? "");
}
}
if (data.output) {
node.output = [];
for (const v of data.output) {
node.output.push(v ?? "");
}
}
if (data.args) {
node.args = {};
for (const k in data.args) {
const v = data.args[k];
if (v !== undefined) {
node.args[k] = v;
}
}
}
if (data.children && !isSubtreeRoot(data) && includeChildren) {
node.children = [];
for (const child of data.children) {
node.children.push(createNode(child));
}
}
return node;
};
const enum StatusFlag {
SUCCESS = 2,
FAILURE = 1,
RUNNING = 0,
SUCCESS_ZERO = 5,
FAILURE_ZERO = 4,
}
const toStatusFlag = (data: TreeGraphData) => {
let status = 0;
data.def.status?.forEach((s) => {
switch (s) {
case "success":
status |= 1 << StatusFlag.SUCCESS;
break;
case "failure":
status |= 1 << StatusFlag.FAILURE;
break;
case "running":
status |= 1 << StatusFlag.RUNNING;
break;
}
});
return status;
};
const appendStatusFlag = (status: number, childStatus: number) => {
const childSuccess = (childStatus >> StatusFlag.SUCCESS) & 1;
const childFailure = (childStatus >> StatusFlag.FAILURE) & 1;
if (childSuccess === 0) {
status |= 1 << StatusFlag.SUCCESS_ZERO;
}
if (childFailure === 0) {
status |= 1 << StatusFlag.FAILURE_ZERO;
}
status |= childStatus;
return status;
};
const buildStatusFlag = (data: TreeGraphData, childStatus: number) => {
let status = data.status!;
if (data.def.status?.length) {
const childSuccess = (childStatus >> StatusFlag.SUCCESS) & 1;
const childFailure = (childStatus >> StatusFlag.FAILURE) & 1;
const childRunning = (childStatus >> StatusFlag.RUNNING) & 1;
const childHasZeroSuccess = (childStatus >> StatusFlag.SUCCESS_ZERO) & 1;
const childHasZeroFailure = (childStatus >> StatusFlag.FAILURE_ZERO) & 1;
data.def.status?.forEach((s) => {
switch (s) {
case "!success":
status |= childFailure << StatusFlag.SUCCESS;
break;
case "!failure":
status |= childSuccess << StatusFlag.FAILURE;
break;
case "|success":
status |= childSuccess << StatusFlag.SUCCESS;
break;
case "|failure":
status |= childFailure << StatusFlag.FAILURE;
break;
case "|running":
status |= childRunning << StatusFlag.RUNNING;
break;
case "&success":
if (childHasZeroSuccess) {
status &= ~(1 << StatusFlag.SUCCESS);
} else {
status |= childSuccess << StatusFlag.SUCCESS;
}
break;
case "&failure":
if (childHasZeroFailure) {
status &= ~(1 << StatusFlag.FAILURE);
} else {
status |= childFailure << StatusFlag.FAILURE;
}
break;
}
});
data.status = status;
} else {
data.status = status | childStatus;
}
};
export const refreshTreeDataId = (data: TreeGraphData, id?: number) => {
if (!id) {
id = 1;
}
const status = toStatusFlag(data);
data.id = (id++).toString();
data.status = status;
if (data.children) {
let childStatus = 0;
data.children.forEach((child) => {
child.parent = data.id;
id = refreshTreeDataId(child, id);
if (child.status && !child.disabled) {
childStatus = appendStatusFlag(childStatus, child.status);
}
});
buildStatusFlag(data, childStatus);
}
return id;
};
export const checkChildrenLimit = (data: TreeGraphData) => {
const conf = data.def;
if (conf.children !== undefined && conf.children !== -1) {
return (data.children?.length || 0) === conf.children;
}
return true;
};
export const isVariadic = (def: string[], i: number) => {
if (i === -1) {
i = def.length - 1;
}
return def[i].endsWith("...") && i === def.length - 1;
};
const isValidInputOrOutput = (def: string[], data: string[] | undefined, index: number) => {
return def[index].includes("?") || data?.[index] || isVariadic(def, index);
};
export const checkTreeData = (data: TreeGraphData) => {
const conf = data.def;
if (conf.input) {
for (let i = 0; i < conf.input.length; i++) {
if (!isValidInputOrOutput(conf.input, data.input, i)) {
return false;
}
}
}
if (conf.output) {
for (let i = 0; i < conf.output.length; i++) {
if (!isValidInputOrOutput(conf.output, data.output, i)) {
return false;
}
}
}
if (!checkChildrenLimit(data)) {
return false;
}
if (conf.args) {
for (let i = 0; i < conf.args.length; i++) {
if (!checkNodeArg(data, conf, i, false)) {
return false;
}
}
}
return true;
};
export const createTreeData = (
node: NodeModel,
parent?: string,
calcSize?: (d: TreeGraphData) => number[]
) => {
let treeData: TreeGraphData = {
id: node.id.toFixed(),
name: node.name,
desc: node.desc,
args: node.args,
input: node.input,
output: node.output,
debug: node.debug,
disabled: node.disabled,
def: nodeDefs.get(node.name),
parent: parent,
};
treeData.def.args?.forEach((arg) => {
treeData.args ||= {};
if (treeData.args[arg.name] === undefined && arg.default !== undefined) {
treeData.args[arg.name] = arg.default;
}
});
if (calcSize) {
treeData.size = calcSize(treeData);
}
if (!parent) {
parsingStack.length = 0;
}
if (node.children) {
treeData.children = [];
node.children.forEach((child) => {
treeData.children!.push(createTreeData(child, treeData.id, calcSize));
});
} else if (node.path) {
if (parsingStack.indexOf(node.path) >= 0) {
treeData.path = node.path;
if (calcSize) {
treeData.size = calcSize(treeData);
}
alertError(`循环引用节点:${node.path}`, 4);
return treeData;
}
parsingStack.push(node.path);
try {
const subtreePath = workdir + "/" + node.path;
treeData = createTreeData(readTree(subtreePath).root, treeData.id, calcSize);
treeData.lastModified = fs.statSync(subtreePath).mtimeMs;
treeData.path = node.path;
treeData.debug = node.debug;
treeData.disabled = node.disabled;
treeData.parent = parent;
treeData.id = node.id.toFixed();
if (calcSize) {
treeData.size = calcSize(treeData);
}
} catch (e) {
alertError(`解析子树失败:${node.path}`);
console.log("parse subtree:", e);
}
parsingStack.pop();
}
return treeData;
};
export const createBuildData = (path: string) => {
try {
const str = fs.readFileSync(path, "utf8");
const treeModel: TreeModel = JSON.parse(str);
const data = createTreeData(treeModel.root);
refreshTreeDataId(data, treeModel.firstid ?? 1);
treeModel.name = Path.basenameWithoutExt(path);
treeModel.root = createFileData(data, true);
return treeModel as TreeModel;
} catch (e) {
console.log("build error:", path, e);
}
return null;
};
export const createFileData = (data: TreeGraphData, includeSubtree?: boolean) => {
const nodeData: NodeModel = {
id: Number(data.id),
name: data.name,
desc: data.desc || undefined,
args: data.args || undefined,
input: data.input || undefined,
output: data.output || undefined,
debug: data.debug || undefined,
disabled: data.disabled || undefined,
path: data.path || undefined,
};
const conf = nodeDefs.get(data.name);
if (!conf.input?.length) {
nodeData.input = undefined;
}
if (!conf.output?.length) {
nodeData.output = undefined;
}
if (!conf.args?.length) {
nodeData.args = undefined;
}
if (data.children?.length && (includeSubtree || !isSubtreeRoot(data))) {
nodeData.children = [];
data.children.forEach((child) => {
nodeData.children!.push(createFileData(child, includeSubtree));
});
}
return nodeData;
};
export const createNewTree = (path: string) => {
const tree: TreeModel = {
name: Path.basenameWithoutExt(path),
root: {
id: 1,
name: "Sequence",
},
};
return tree;
};
export const isTreeFile = (path: string) => {
return path.toLocaleLowerCase().endsWith(".json");
};
export const loadVarDef = (list: ImportDef[]) => {
for (const entry of list) {
// TODO: check file date
// TODO: use cache
if (entry.vars.length) {
continue;
}
const filter: Map<string, boolean> = new Map();
const vars: VarDef[] = [];
const load = (path: string) => {
if (filter.has(path)) {
return;
}
filter.set(path, true);
try {
const model: TreeModel = readTree(`${workdir}/${path}`);
model.declare?.vars?.forEach((v) => vars.push(v));
model.declare?.imports?.forEach((v) => load(v));
} catch (e) {
alertError(`parsing error: ${path}`);
}
};
load(entry.path);
filter.clear();
entry.vars = vars
.filter((v) => {
if (!filter.has(v.name)) {
filter.set(v.name, true);
return true;
}
return false;
})
.sort((a, b) => a.name.localeCompare(b.name));
}
};
export const loadSubtreeVarDef = (data: TreeGraphData) => {
const list: ImportDef[] = [];
const traverse = (v: TreeGraphData) => {
if (v.path) {
list.push({ path: v.path, vars: [] });
}
if (v.children) {
v.children.forEach(traverse);
}
};
traverse(data);
loadVarDef(list);
const filter: Map<string, boolean> = new Map();
const result: VarDef[] = [];
list.forEach((entry) => {
entry.vars.forEach((v) => {
if (!filter.has(v.name)) {
filter.set(v.name, true);
result.push(v);
}
});
});
return result;
};
export const parseExpr = (expr: string) => {
const result = expr.split(/[^a-zA-Z0-9_.]/);
return result.map((v) => v.split(".")[0]).filter((v) => v);
};