Support multi source options of node's arg

This commit is contained in:
codetypes committed 2026-01-13 11:49:42 +08:00
1 parent 2f4176857e
commit 6f9097f73d
7 files changed
+304 -213

No files matched your search

+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "behavior3editor", "name": "behavior3editor",
"version": "1.8.12", "version": "1.9.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "behavior3editor", "name": "behavior3editor",
"version": "1.8.12", "version": "1.9.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "behavior3editor", "name": "behavior3editor",
"version": "1.8.12", "version": "1.9.0",
"main": "dist-electron/main/index.js", "main": "dist-electron/main/index.js",
"description": "行为树编辑器", "description": "行为树编辑器",
"author": "zhandouxiaojiji & codetypes", "author": "zhandouxiaojiji & codetypes",
+254 -204
View File
@@ -11,6 +11,7 @@ import {
Divider, Divider,
Flex, Flex,
Form, Form,
FormInstance,
Input, Input,
InputNumber, InputNumber,
Select, Select,
@@ -26,7 +27,7 @@ import Markdown from "react-markdown";
import { useDebounceCallback } from "usehooks-ts"; import { useDebounceCallback } from "usehooks-ts";
import { useShallow } from "zustand/react/shallow"; import { useShallow } from "zustand/react/shallow";
import { ExpressionEvaluator } from "../behavior3/src/behavior3"; import { ExpressionEvaluator } from "../behavior3/src/behavior3";
import { EditNode, EditTree, useWorkspace } from "../contexts/workspace-context"; import { EditNode, EditorStore, EditTree, useWorkspace } from "../contexts/workspace-context";
import { import {
hasArgOptions, hasArgOptions,
ImportDecl, ImportDecl,
@@ -43,12 +44,14 @@ import {
import { import {
checkNodeArgValue, checkNodeArgValue,
checkOneof, checkOneof,
getNodeArgOptions,
getNodeArgRawType, getNodeArgRawType,
isNodeArgArray, isNodeArgArray,
isNodeArgOptional, isNodeArgOptional,
isValidChildren, isValidChildren,
isValidVariableName, isValidVariableName,
isVariadic, isVariadic,
NodeDefs,
parseExpr, parseExpr,
} from "../misc/b3util"; } from "../misc/b3util";
import { message } from "../misc/hooks"; import { message } from "../misc/hooks";
@@ -554,6 +557,223 @@ const validateArg = (
return Promise.resolve(value); return Promise.resolve(value);
}; };
const updateFormWithNode = (
form: FormInstance,
{
editingNode,
nodeDefs,
}: {
editingNode: EditNode;
nodeDefs: NodeDefs;
}
) => {
const data = editingNode.data;
const def = nodeDefs.get(data.name);
const t = i18n.t;
form.resetFields();
form.setFieldValue("id", editingNode.prefix + data.id);
form.setFieldValue("name", data.name);
form.setFieldValue("type", def.type);
form.setFieldValue("desc", data.desc || def.desc);
form.setFieldValue("debug", data.debug);
form.setFieldValue("disabled", data.disabled);
form.setFieldValue("path", data.path);
form.setFieldValue(
"group",
def.group?.map((g) => ({ label: g, value: g }))
);
if (def.children === undefined || def.children === -1) {
form.setFieldValue("children", t("node.children.unlimited"));
} else {
form.setFieldValue("children", def.children);
}
form.setFieldValue("json-data", JSON.stringify(data, null, 2));
def.args?.forEach((arg) => {
const type = getNodeArgRawType(arg);
const value = data.args?.[arg.name];
const name = `args.${arg.name}`;
if (isNodeArgArray(arg)) {
form.setFieldValue(
name,
(Array.isArray(value) ? value : []).map((item) => {
if (isJsonType(type)) {
return item === null ? "null" : JSON.stringify(item ?? arg.default, null, 2);
} else {
return item;
}
})
);
} else if (isJsonType(type)) {
form.setFieldValue(
name,
value === null ? "null" : JSON.stringify(value ?? arg.default, null, 2)
);
} else {
form.setFieldValue(name, value ?? arg.default);
}
});
def.input?.forEach((_, i) => {
if (isVariadic(def.input!, i)) {
form.setFieldValue(`input.${i}`, data.input?.slice(i) ?? []);
} else {
form.setFieldValue(`input.${i}`, data.input?.[i]);
}
});
def.output?.forEach((_, i) => {
if (isVariadic(def.output!, i)) {
form.setFieldValue(`output.${i}`, data.output?.slice(i) ?? []);
} else {
form.setFieldValue(`output.${i}`, data.output?.[i]);
}
});
};
const createNodeFromForm = (
form: FormInstance,
{
editingNode,
nodeDefs,
}: {
editingNode: EditNode;
nodeDefs: NodeDefs;
}
) => {
const def = nodeDefs.get(editingNode.data.name);
const data = {} as NodeData;
const values = form.getFieldsValue();
data.$id = editingNode.data.$id;
data.id = editingNode.data.id;
data.name = values.name;
data.debug = values.debug || undefined;
data.disabled = values.disabled || undefined;
data.desc = values.desc && values.desc !== def.desc ? values.desc : undefined;
data.path = values.path || undefined;
if (def.args?.length) {
def.args?.forEach((arg) => {
const value = values[`args.${arg.name}`];
if (value !== null && value !== undefined && value !== "") {
data.args ||= {};
const type = getNodeArgRawType(arg);
if (isNodeArgArray(arg)) {
const arr: unknown[] = [];
if (Array.isArray(value)) {
value.forEach((item) => {
if (isJsonType(type)) {
try {
arr.push(item === "null" ? null : JSON.parse(item));
} catch {
/** ignore */
}
} else if (item !== null && item !== undefined) {
arr.push(item);
}
});
}
data.args[arg.name] = !isNodeArgOptional(arg) ? arr : arr.length ? arr : undefined;
} else if (isJsonType(type)) {
try {
data.args[arg.name] = value === "null" ? null : JSON.parse(value);
} catch {
/** ignore */
}
} else {
data.args[arg.name] = value;
}
}
});
} else {
data.args = {};
}
if (def.input?.length) {
def.input?.forEach((_, i) => {
data.input ||= [];
if (isVariadic(def.input!, i)) {
const arr = (values[`input.${i}`] ?? []) as string[];
data.input.push(...arr.filter((v) => typeof v === "string"));
} else {
const v = values[`input.${i}`];
data.input.push(v ?? "");
}
});
} else {
data.input = [];
}
if (def.output?.length) {
def.output?.forEach((_, i) => {
data.output ||= [];
if (isVariadic(def.output!, i)) {
const arr = (values[`output.${i}`] ?? []) as string[];
data.output.push(...arr.filter((v) => typeof v === "string"));
} else {
const v = values[`output.${i}`];
data.output.push(v ?? "");
}
});
} else {
data.output = [];
}
return data;
};
const createInOutOptions = ({
nodeDefs,
usingVars,
editing,
}: {
nodeDefs: NodeDefs;
usingVars: Record<string, VarDecl> | null;
editing: EditorStore | undefined;
}) => {
const options: OptionType[] = [];
const filter: Record<string, boolean> = {};
const collect = (node?: NodeData) => {
if (node) {
const def = nodeDefs.get(node.name);
node.input?.forEach((v, i) => {
let desc: string;
const inputDef = def.input;
if (inputDef?.length && i >= inputDef.length && isVariadic(inputDef, -1)) {
desc = inputDef[inputDef.length - 1];
} else {
desc = inputDef?.[i] ?? "<unknown>";
}
if (!filter[v]) {
options.push({ label: `${v}(${desc})`, value: v });
filter[v] = true;
}
});
node.output?.forEach((v, i) => {
let desc: string;
const outputDef = def.output;
if (outputDef?.length && i >= outputDef.length && isVariadic(outputDef, -1)) {
desc = outputDef[outputDef.length - 1];
} else {
desc = outputDef?.[i] ?? "<unknown>";
}
if (!filter[v]) {
options.push({ label: `${v}(${desc})`, value: v });
filter[v] = true;
}
});
node.children?.forEach((child) => collect(child));
}
};
if (usingVars) {
Object.values(usingVars).forEach((v) => {
if (!filter[v.name]) {
options.push({ label: `${v.name}(${v.desc})`, value: v.name });
filter[v.name] = true;
}
});
} else {
collect(editing?.data.root);
}
return options;
};
const NodeInspector: FC = () => { const NodeInspector: FC = () => {
const workspace = useWorkspace( const workspace = useWorkspace(
useShallow((state) => ({ useShallow((state) => ({
@@ -578,7 +798,13 @@ const NodeInspector: FC = () => {
100 100
); );
const [nodeArgs, setNodeArgs] = useState<Record<string, unknown>>(
workspace.editingNode.data.args ?? {}
);
const submit = () => { const submit = () => {
setNodeArgs(createNodeFromForm(form, workspace).args ?? {});
if (form.isFieldsValidating()) { if (form.isFieldsValidating()) {
setTimeout(() => { setTimeout(() => {
submit(); submit();
@@ -596,124 +822,24 @@ const NodeInspector: FC = () => {
// set form values // set form values
useEffect(() => { useEffect(() => {
const data = workspace.editingNode.data; updateFormWithNode(form, workspace);
const def = workspace.nodeDefs.get(workspace.editingNode.data.name);
form.resetFields();
form.setFieldValue("id", workspace.editingNode.prefix + data.id);
form.setFieldValue("name", data.name);
form.setFieldValue("type", def.type);
form.setFieldValue("desc", data.desc || def.desc);
form.setFieldValue("debug", data.debug);
form.setFieldValue("disabled", data.disabled);
form.setFieldValue("path", data.path);
form.setFieldValue(
"group",
def.group?.map((g) => ({ label: g, value: g }))
);
if (def.children === undefined || def.children === -1) {
form.setFieldValue("children", t("node.children.unlimited"));
} else {
form.setFieldValue("children", def.children);
}
form.setFieldValue("json-data", JSON.stringify(data, null, 2));
def.args?.forEach((arg) => {
const type = getNodeArgRawType(arg);
const value = data.args?.[arg.name];
const name = `args.${arg.name}`;
if (isNodeArgArray(arg)) {
form.setFieldValue(
name,
(Array.isArray(value) ? value : []).map((item) => {
if (isJsonType(type)) {
return item === null ? "null" : JSON.stringify(item ?? arg.default, null, 2);
} else {
return item;
}
})
);
} else if (isJsonType(type)) {
form.setFieldValue(
name,
value === null ? "null" : JSON.stringify(value ?? arg.default, null, 2)
);
} else {
form.setFieldValue(name, value ?? arg.default);
}
});
def.input?.forEach((_, i) => {
if (isVariadic(def.input!, i)) {
form.setFieldValue(`input.${i}`, data.input?.slice(i) ?? []);
} else {
form.setFieldValue(`input.${i}`, data.input?.[i]);
}
});
def.output?.forEach((_, i) => {
if (isVariadic(def.output!, i)) {
form.setFieldValue(`output.${i}`, data.output?.slice(i) ?? []);
} else {
form.setFieldValue(`output.${i}`, data.output?.[i]);
}
});
validateFieldsLater(); validateFieldsLater();
}, [workspace.editingNode]); }, [workspace.editingNode]);
// auto complete for node // auto complete for node
const nodeOptions = useMemo(() => { const nodeOptions = useMemo(
const options: OptionType[] = []; () =>
workspace.nodeDefs.forEach((e) => { Array.from(workspace.nodeDefs.entries()).map(([name, def]) => {
options.push({ label: `${e.name}(${e.desc})`, value: e.name }); return { label: `${name}(${def.desc})`, value: name };
}); }),
return options; [workspace.nodeDefs]
}, [workspace.nodeDefs]); );
// auto complete for input and output // auto complete for input and output
const inoutVarOptions = useMemo(() => { const inoutVarOptions = useMemo(
const options: OptionType[] = []; () => createInOutOptions(workspace),
const filter: Record<string, boolean> = {}; [workspace.editing, workspace.usingVars]
const collect = (node?: NodeData) => { );
if (node) {
const def = workspace.nodeDefs.get(node.name);
node.input?.forEach((v, i) => {
let desc: string;
const inputDef = def.input;
if (inputDef?.length && i >= inputDef.length && isVariadic(inputDef, -1)) {
desc = inputDef[inputDef.length - 1];
} else {
desc = inputDef?.[i] ?? "<unknown>";
}
if (!filter[v]) {
options.push({ label: `${v}(${desc})`, value: v });
filter[v] = true;
}
});
node.output?.forEach((v, i) => {
let desc: string;
const outputDef = def.output;
if (outputDef?.length && i >= outputDef.length && isVariadic(outputDef, -1)) {
desc = outputDef[outputDef.length - 1];
} else {
desc = outputDef?.[i] ?? "<unknown>";
}
if (!filter[v]) {
options.push({ label: `${v}(${desc})`, value: v });
filter[v] = true;
}
});
node.children?.forEach((child) => collect(child));
}
};
if (workspace.usingVars) {
Object.values(workspace.usingVars).forEach((v) => {
if (!filter[v.name]) {
options.push({ label: `${v.name}(${v.desc})`, value: v.name });
filter[v.name] = true;
}
});
} else {
collect(workspace.editing?.data.root);
}
return options;
}, [workspace.editing, workspace.usingVars]);
// auto complete for subtree // auto complete for subtree
const subtreeOptions = useMemo(() => { const subtreeOptions = useMemo(() => {
@@ -734,87 +860,9 @@ const NodeInspector: FC = () => {
const def = workspace.nodeDefs.get(editingNode.data.name); const def = workspace.nodeDefs.get(editingNode.data.name);
const disabled = editingNode.disabled; const disabled = editingNode.disabled;
// update value const finish = () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const finish = (values: any) => {
const data = {} as NodeData;
data.$id = editingNode.data.$id;
data.id = editingNode.data.id;
data.name = values.name;
data.debug = values.debug || undefined;
data.disabled = values.disabled || undefined;
data.desc = values.desc && values.desc !== def.desc ? values.desc : undefined;
data.path = values.path || undefined;
if (def.args?.length) {
def.args?.forEach((arg) => {
const value = values[`args.${arg.name}`];
if (value !== null && value !== undefined && value !== "") {
data.args ||= {};
const type = getNodeArgRawType(arg);
if (isNodeArgArray(arg)) {
const arr: unknown[] = [];
if (Array.isArray(value)) {
value.forEach((item) => {
if (isJsonType(type)) {
try {
arr.push(item === "null" ? null : JSON.parse(item));
} catch {
/** ignore */
}
} else if (item !== null && item !== undefined) {
arr.push(item);
}
});
}
data.args[arg.name] = !isNodeArgOptional(arg) ? arr : arr.length ? arr : undefined;
} else if (isJsonType(type)) {
try {
data.args[arg.name] = value === "null" ? null : JSON.parse(value);
} catch {
/** ignore */
}
} else {
data.args[arg.name] = value;
}
}
});
} else {
data.args = {};
}
if (def.input?.length) {
def.input?.forEach((_, i) => {
data.input ||= [];
if (isVariadic(def.input!, i)) {
const arr = (values[`input.${i}`] ?? []) as string[];
data.input.push(...arr.filter((v) => typeof v === "string"));
} else {
const v = values[`input.${i}`];
data.input.push(v ?? "");
}
});
} else {
data.input = [];
}
if (def.output?.length) {
def.output?.forEach((_, i) => {
data.output ||= [];
if (isVariadic(def.output!, i)) {
const arr = (values[`output.${i}`] ?? []) as string[];
data.output.push(...arr.filter((v) => typeof v === "string"));
} else {
const v = values[`output.${i}`];
data.output.push(v ?? "");
}
});
} else {
data.output = [];
}
workspace.editing?.dispatch?.("updateNode", { workspace.editing?.dispatch?.("updateNode", {
data: data, data: createNodeFromForm(form, workspace),
} as EditNode); } as EditNode);
}; };
@@ -833,7 +881,7 @@ const NodeInspector: FC = () => {
prefix: editingNode.prefix, prefix: editingNode.prefix,
disabled: editingNode.disabled, disabled: editingNode.disabled,
}); });
finish(form.getFieldsValue()); finish();
} else { } else {
submit(); submit();
} }
@@ -841,7 +889,7 @@ const NodeInspector: FC = () => {
const changeSubtree = () => { const changeSubtree = () => {
if (form.getFieldValue("path") !== editingNode.data.path) { if (form.getFieldValue("path") !== editingNode.data.path) {
finish(form.getFieldsValue()); finish();
} else { } else {
submit(); submit();
} }
@@ -1182,7 +1230,7 @@ const NodeInspector: FC = () => {
() => ({ () => ({
validator(_, value) { validator(_, value) {
return validateArg( return validateArg(
editingNode.data, createNodeFromForm(form, workspace),
arg, arg,
value, value,
workspace.usingVars workspace.usingVars
@@ -1215,12 +1263,14 @@ const NodeInspector: FC = () => {
disabled={disabled} disabled={disabled}
onBlur={submit} onBlur={submit}
onChange={submit} onChange={submit}
options={(arg.options ?? []).map((option) => { options={(getNodeArgOptions(arg, nodeArgs) ?? []).map(
return { (option) => {
value: option.value, return {
label: `${option.name}(${option.value})`, value: option.value,
}; label: `${option.name}(${option.value})`,
})} };
}
)}
filterOption={(value, option) => { filterOption={(value, option) => {
value = value.toUpperCase(); value = value.toUpperCase();
return !!option?.label return !!option?.label
@@ -1280,7 +1330,7 @@ const NodeInspector: FC = () => {
({ getFieldValue, setFieldValue, isFieldValidating, validateFields }) => ({ ({ getFieldValue, setFieldValue, isFieldValidating, validateFields }) => ({
async validator(_, value) { async validator(_, value) {
return validateArg( return validateArg(
editingNode.data, createNodeFromForm(form, workspace),
arg, arg,
value, value,
workspace.usingVars workspace.usingVars
@@ -1344,7 +1394,7 @@ const NodeInspector: FC = () => {
disabled={disabled} disabled={disabled}
onBlur={submit} onBlur={submit}
onChange={submit} onChange={submit}
options={(arg.options ?? []).map((option) => { options={(getNodeArgOptions(arg, nodeArgs) ?? []).map((option) => {
return { return {
value: option.value, value: option.value,
label: `${option.name}(${option.value})`, label: `${option.name}(${option.value})`,
+8
View File
@@ -137,6 +137,14 @@ body {
outline: none; outline: none;
} }
.ant-select-dropdown .ant-select-item-option-selected {
background-color: var(--b3-color-selected) !important;
}
.ant-select-dropdown .ant-select-item-option-active {
background-color: var(--b3-color-selected) !important;
}
.ant-form-item .ant-form-item-explain { .ant-form-item .ant-form-item-explain {
font-size: small; font-size: small;
} }
+1 -1
View File
@@ -1,6 +1,6 @@
import { NodeDef } from "../behavior3/src/behavior3"; import { NodeDef } from "../behavior3/src/behavior3";
export const VERSION = "1.8.10"; export const VERSION = "1.9.0";
export const keyWords = ["true", "false", "null", "undefined", "NaN", "Infinity"]; export const keyWords = ["true", "false", "null", "undefined", "NaN", "Infinity"];
+37 -4
View File
@@ -69,9 +69,28 @@ export const initWorkdir = (path: string, handler: typeof alertError) => {
const nodeDefData = readJson(`${workdir}/node-config.b3-setting`) as NodeDef[]; const nodeDefData = readJson(`${workdir}/node-config.b3-setting`) as NodeDef[];
const groups: Set<string> = new Set(); const groups: Set<string> = new Set();
nodeDefs = new NodeDefs(); nodeDefs = new NodeDefs();
for (const v of nodeDefData) { for (const node of nodeDefData) {
nodeDefs.set(v.name, v); node.args?.forEach((arg) => {
v.group?.forEach((g) => groups.add(g)); if (arg.options && !arg.options[0].source) {
arg.options = [
{
source: arg.options as unknown as Array<{ name: string; value: unknown }>,
},
];
}
arg.options?.forEach((option) => {
Object.keys(option.match ?? {}).forEach((key) => {
if (!node.args?.find((v) => v.name === key)) {
console.error(
`match key '${key}' in arg '${arg.name}' of ` +
`node '${node.name}' is not found in args`
);
}
});
});
});
nodeDefs.set(node.name, node);
node.group?.forEach((g) => groups.add(g));
} }
groupDefs = Array.from(groups).sort(); groupDefs = Array.from(groups).sort();
}; };
@@ -207,6 +226,19 @@ export const isNodeArgOptional = (arg: NodeArg) => {
return arg.type.includes("?"); return arg.type.includes("?");
}; };
export const getNodeArgOptions = (arg: NodeArg, args: Record<string, unknown>) => {
if (!arg.options) {
return;
}
const defaultMatch = arg.options.find((option) => !option.match);
if (defaultMatch) {
return defaultMatch.source;
}
return arg.options.find((entry) =>
Object.entries(entry.match!).every(([key, value]) => value.includes(args[key]))
)?.source;
};
export const checkNodeArgValue = ( export const checkNodeArgValue = (
data: NodeData, data: NodeData,
arg: NodeArg, arg: NodeArg,
@@ -264,7 +296,8 @@ export const checkNodeArgValue = (
} }
if (hasArgOptions(arg)) { if (hasArgOptions(arg)) {
const found = !!arg.options?.find((option) => option.value === value); const options = getNodeArgOptions(arg, data.args ?? {});
const found = !!options?.find((option) => option.value === value);
const isOptional = value === undefined && isNodeArgOptional(arg); const isOptional = value === undefined && isNodeArgOptional(arg);
if (!(found || isOptional)) { if (!(found || isOptional)) {
error(`'${arg.name}=${JSON.stringify(value)}' is not a one of the option values`); error(`'${arg.name}=${JSON.stringify(value)}' is not a one of the option values`);