diff --git a/package-lock.json b/package-lock.json index 2bb6ce8..3b4b5e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "behavior3editor", - "version": "1.8.12", + "version": "1.9.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "behavior3editor", - "version": "1.8.12", + "version": "1.9.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 9728ea1..cd9abf7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "behavior3editor", - "version": "1.8.12", + "version": "1.9.0", "main": "dist-electron/main/index.js", "description": "行为树编辑器", "author": "zhandouxiaojiji & codetypes", diff --git a/src/behavior3 b/src/behavior3 index 7bc20e0..fc60899 160000 --- a/src/behavior3 +++ b/src/behavior3 @@ -1 +1 @@ -Subproject commit 7bc20e0313e42fd41c52744782c014a40c58bf78 +Subproject commit fc608994fdb772aa498e6bd525e25c3ac1bf9f3f diff --git a/src/components/inspector.tsx b/src/components/inspector.tsx index cddb564..9765b88 100644 --- a/src/components/inspector.tsx +++ b/src/components/inspector.tsx @@ -11,6 +11,7 @@ import { Divider, Flex, Form, + FormInstance, Input, InputNumber, Select, @@ -26,7 +27,7 @@ import Markdown from "react-markdown"; import { useDebounceCallback } from "usehooks-ts"; import { useShallow } from "zustand/react/shallow"; import { ExpressionEvaluator } from "../behavior3/src/behavior3"; -import { EditNode, EditTree, useWorkspace } from "../contexts/workspace-context"; +import { EditNode, EditorStore, EditTree, useWorkspace } from "../contexts/workspace-context"; import { hasArgOptions, ImportDecl, @@ -43,12 +44,14 @@ import { import { checkNodeArgValue, checkOneof, + getNodeArgOptions, getNodeArgRawType, isNodeArgArray, isNodeArgOptional, isValidChildren, isValidVariableName, isVariadic, + NodeDefs, parseExpr, } from "../misc/b3util"; import { message } from "../misc/hooks"; @@ -554,6 +557,223 @@ const validateArg = ( 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 | null; + editing: EditorStore | undefined; +}) => { + const options: OptionType[] = []; + const filter: Record = {}; + 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] ?? ""; + } + 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] ?? ""; + } + 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 workspace = useWorkspace( useShallow((state) => ({ @@ -578,7 +798,13 @@ const NodeInspector: FC = () => { 100 ); + const [nodeArgs, setNodeArgs] = useState>( + workspace.editingNode.data.args ?? {} + ); + const submit = () => { + setNodeArgs(createNodeFromForm(form, workspace).args ?? {}); + if (form.isFieldsValidating()) { setTimeout(() => { submit(); @@ -596,124 +822,24 @@ const NodeInspector: FC = () => { // set form values useEffect(() => { - const data = workspace.editingNode.data; - 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]); - } - }); + updateFormWithNode(form, workspace); validateFieldsLater(); }, [workspace.editingNode]); // auto complete for node - const nodeOptions = useMemo(() => { - const options: OptionType[] = []; - workspace.nodeDefs.forEach((e) => { - options.push({ label: `${e.name}(${e.desc})`, value: e.name }); - }); - return options; - }, [workspace.nodeDefs]); + const nodeOptions = useMemo( + () => + Array.from(workspace.nodeDefs.entries()).map(([name, def]) => { + return { label: `${name}(${def.desc})`, value: name }; + }), + [workspace.nodeDefs] + ); // auto complete for input and output - const inoutVarOptions = useMemo(() => { - const options: OptionType[] = []; - const filter: Record = {}; - 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] ?? ""; - } - 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] ?? ""; - } - 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]); + const inoutVarOptions = useMemo( + () => createInOutOptions(workspace), + [workspace.editing, workspace.usingVars] + ); // auto complete for subtree const subtreeOptions = useMemo(() => { @@ -734,87 +860,9 @@ const NodeInspector: FC = () => { const def = workspace.nodeDefs.get(editingNode.data.name); const disabled = editingNode.disabled; - // update value - // 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 = []; - } - + const finish = () => { workspace.editing?.dispatch?.("updateNode", { - data: data, + data: createNodeFromForm(form, workspace), } as EditNode); }; @@ -833,7 +881,7 @@ const NodeInspector: FC = () => { prefix: editingNode.prefix, disabled: editingNode.disabled, }); - finish(form.getFieldsValue()); + finish(); } else { submit(); } @@ -841,7 +889,7 @@ const NodeInspector: FC = () => { const changeSubtree = () => { if (form.getFieldValue("path") !== editingNode.data.path) { - finish(form.getFieldsValue()); + finish(); } else { submit(); } @@ -1182,7 +1230,7 @@ const NodeInspector: FC = () => { () => ({ validator(_, value) { return validateArg( - editingNode.data, + createNodeFromForm(form, workspace), arg, value, workspace.usingVars @@ -1215,12 +1263,14 @@ const NodeInspector: FC = () => { disabled={disabled} onBlur={submit} onChange={submit} - options={(arg.options ?? []).map((option) => { - return { - value: option.value, - label: `${option.name}(${option.value})`, - }; - })} + options={(getNodeArgOptions(arg, nodeArgs) ?? []).map( + (option) => { + return { + value: option.value, + label: `${option.name}(${option.value})`, + }; + } + )} filterOption={(value, option) => { value = value.toUpperCase(); return !!option?.label @@ -1280,7 +1330,7 @@ const NodeInspector: FC = () => { ({ getFieldValue, setFieldValue, isFieldValidating, validateFields }) => ({ async validator(_, value) { return validateArg( - editingNode.data, + createNodeFromForm(form, workspace), arg, value, workspace.usingVars @@ -1344,7 +1394,7 @@ const NodeInspector: FC = () => { disabled={disabled} onBlur={submit} onChange={submit} - options={(arg.options ?? []).map((option) => { + options={(getNodeArgOptions(arg, nodeArgs) ?? []).map((option) => { return { value: option.value, label: `${option.name}(${option.value})`, diff --git a/src/index.scss b/src/index.scss index 910d21a..68d564d 100644 --- a/src/index.scss +++ b/src/index.scss @@ -137,6 +137,14 @@ body { 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 { font-size: small; } diff --git a/src/misc/b3type.ts b/src/misc/b3type.ts index 373f52a..6c8f71e 100644 --- a/src/misc/b3type.ts +++ b/src/misc/b3type.ts @@ -1,6 +1,6 @@ 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"]; diff --git a/src/misc/b3util.ts b/src/misc/b3util.ts index 4df600c..20806a7 100644 --- a/src/misc/b3util.ts +++ b/src/misc/b3util.ts @@ -69,9 +69,28 @@ export const initWorkdir = (path: string, handler: typeof alertError) => { const nodeDefData = readJson(`${workdir}/node-config.b3-setting`) as NodeDef[]; const groups: Set = new Set(); nodeDefs = new NodeDefs(); - for (const v of nodeDefData) { - nodeDefs.set(v.name, v); - v.group?.forEach((g) => groups.add(g)); + for (const node of nodeDefData) { + node.args?.forEach((arg) => { + 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(); }; @@ -207,6 +226,19 @@ export const isNodeArgOptional = (arg: NodeArg) => { return arg.type.includes("?"); }; +export const getNodeArgOptions = (arg: NodeArg, args: Record) => { + 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 = ( data: NodeData, arg: NodeArg, @@ -264,7 +296,8 @@ export const checkNodeArgValue = ( } 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); if (!(found || isOptional)) { error(`'${arg.name}=${JSON.stringify(value)}' is not a one of the option values`);