fix: support multi-tree BehaviorTree XML imports

This commit is contained in:
TuxMonkey
2026-07-21 22:16:29 +08:00
parent 5cc4c73e47
commit 82374aabd2
10 changed files with 431 additions and 127 deletions

View File

@@ -1,30 +1,40 @@
import { Search, X } from 'lucide-react'
import { Search, SquareFunction, X, Zap } from 'lucide-react'
import { useMemo, useState } from 'react'
import { CATEGORY_LABELS, NODE_DEFINITIONS } from '../lib/catalog'
import type { NodeCategory, NodeType } from '../types'
import type { CustomNodeModel, NodeCategory, NodeType } from '../types'
interface NodePaletteProps {
customModels: CustomNodeModel[]
onAddNode: (type: NodeType) => void
onAddCustomNode: (modelId: string) => void
}
const CATEGORY_ORDER: NodeCategory[] = ['control', 'decorator', 'action', 'condition', 'subtree']
export function NodePalette({ onAddNode }: NodePaletteProps) {
export function NodePalette({ customModels, onAddNode, onAddCustomNode }: NodePaletteProps) {
const [query, setQuery] = useState('')
const groups = useMemo(() => {
const normalized = query.trim().toLowerCase()
return CATEGORY_ORDER.map((category) => ({
category,
items: NODE_DEFINITIONS.filter((item) => item.category === category)
.filter((item) => !normalized || `${item.label} ${item.description} ${item.type}`.toLowerCase().includes(normalized)),
})).filter((group) => group.items.length)
}, [query])
const normalizedQuery = query.trim().toLowerCase()
const groups = useMemo(() => CATEGORY_ORDER.map((category) => ({
category,
items: NODE_DEFINITIONS.filter((item) => item.category === category)
.filter((item) => !normalizedQuery || `${item.label} ${item.description} ${item.type}`.toLowerCase().includes(normalizedQuery)),
})).filter((group) => group.items.length), [normalizedQuery])
const visibleModels = useMemo(() => customModels.filter((model) => {
if (!normalizedQuery) return true
return `${model.id} ${model.category} ${model.ports.map((port) => port.name).join(' ')}`.toLowerCase().includes(normalizedQuery)
}), [customModels, normalizedQuery])
const beginDrag = (event: React.DragEvent, type: NodeType) => {
event.dataTransfer.setData('application/arborflow-node', type)
event.dataTransfer.effectAllowed = 'copy'
}
const beginCustomDrag = (event: React.DragEvent, modelId: string) => {
event.dataTransfer.setData('application/arborflow-model', modelId)
event.dataTransfer.effectAllowed = 'copy'
}
return (
<aside className="left-panel">
<div className="panel-heading">
@@ -32,14 +42,37 @@ export function NodePalette({ onAddNode }: NodePaletteProps) {
<span className="eyebrow">LIBRARY</span>
<h2>节点</h2>
</div>
<span className="count-label">{NODE_DEFINITIONS.length}</span>
<span className="count-label">{NODE_DEFINITIONS.length + customModels.length}</span>
</div>
<div className="palette-search">
<Search size={15} />
<Search size={16} />
<input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="搜索节点" aria-label="搜索节点" />
{query && <button className="bare-icon" onClick={() => setQuery('')} title="清除搜索"><X size={14} /></button>}
{query && <button className="bare-icon" onClick={() => setQuery('')} title="清除搜索"><X size={15} /></button>}
</div>
<div className="palette-scroll">
{visibleModels.length > 0 && (
<section className="palette-group custom-model-group">
<h3>XML 自定义节点</h3>
<div className="palette-items">
{visibleModels.map((model) => {
const Icon = model.category === 'condition' ? SquareFunction : Zap
return (
<button
className={`palette-item category-${model.category}`}
draggable
key={model.id}
onDragStart={(event) => beginCustomDrag(event, model.id)}
onClick={() => onAddCustomNode(model.id)}
title={`${CATEGORY_LABELS[model.category]} · ${model.ports.length} 个端口`}
>
<span className="palette-item-icon"><Icon size={16} /></span>
<span className="palette-item-name">{model.id}</span>
</button>
)
})}
</div>
</section>
)}
{groups.map(({ category, items }) => (
<section className="palette-group" key={category}>
<h3>{CATEGORY_LABELS[category]}</h3>
@@ -63,7 +96,7 @@ export function NodePalette({ onAddNode }: NodePaletteProps) {
</div>
</section>
))}
{!groups.length && <div className="panel-empty">没有匹配节点</div>}
{!groups.length && !visibleModels.length && <div className="panel-empty">没有匹配节点</div>}
</div>
</aside>
)