refactor(editor): 优化布局管理和行为树文件处理

This commit is contained in:
YHH
2025-11-04 23:53:26 +08:00
parent f9afa22406
commit e03b106652
15 changed files with 958 additions and 243 deletions

View File

@@ -3,7 +3,26 @@
flex-direction: column;
height: 100%;
width: 100%;
background: var(--bg-secondary);
background: #1e1e1e;
}
/* 全屏模式 */
.behavior-tree-editor-panel.fullscreen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
background: #1e1e1e;
}
/* 全屏模式下的工具栏 - 确保不透明 */
.behavior-tree-editor-panel.fullscreen .behavior-tree-editor-toolbar {
background: #252526;
opacity: 1;
position: relative;
z-index: 10000;
}
/* 工具栏 */
@@ -11,8 +30,8 @@
display: flex;
align-items: center;
padding: 8px 12px;
background: var(--bg-primary);
border-bottom: 1px solid var(--border-color);
background: #252526;
border-bottom: 1px solid #3e3e42;
gap: 8px;
flex-wrap: wrap;
}
@@ -26,7 +45,7 @@
.behavior-tree-editor-toolbar .toolbar-divider {
width: 1px;
height: 24px;
background: var(--border-color);
background: #3e3e42;
margin: 0 4px;
}
@@ -39,17 +58,17 @@
border: 1px solid transparent;
border-radius: 4px;
cursor: pointer;
color: var(--text-primary);
color: #cccccc;
transition: all 0.2s;
}
.behavior-tree-editor-toolbar .toolbar-btn:hover:not(:disabled) {
background: var(--bg-hover);
border-color: var(--border-color);
background: #2a2d2e;
border-color: #3e3e42;
}
.behavior-tree-editor-toolbar .toolbar-btn:active:not(:disabled) {
background: var(--bg-active);
background: #37373d;
}
.behavior-tree-editor-toolbar .toolbar-btn:disabled {
@@ -165,7 +184,7 @@
.behavior-tree-editor-toolbar .file-name {
font-size: 13px;
color: var(--text-secondary);
color: #9d9d9d;
font-weight: 500;
}
@@ -192,8 +211,8 @@
width: 350px;
display: flex;
flex-direction: column;
background: var(--bg-primary);
border: 1px solid var(--border-color);
background: #252526;
border: 1px solid #3e3e42;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.05);
z-index: 100;
@@ -205,11 +224,11 @@
justify-content: space-between;
padding: 12px;
background: #2d2d2d;
border-bottom: 1px solid var(--border-color);
border-bottom: 1px solid #3e3e42;
border-radius: 8px 8px 0 0;
font-weight: 500;
font-size: 13px;
color: var(--text-primary);
color: #cccccc;
}
.blackboard-header .close-btn {
@@ -221,14 +240,14 @@
border: 1px solid transparent;
border-radius: 4px;
cursor: pointer;
color: var(--text-secondary);
color: #9d9d9d;
transition: all 0.2s;
}
.blackboard-header .close-btn:hover {
background: var(--bg-hover);
border-color: var(--border-color);
color: var(--text-primary);
background: #2a2d2e;
border-color: #3e3e42;
color: #cccccc;
}
.blackboard-content {
@@ -247,18 +266,18 @@
justify-content: center;
width: 32px;
height: 48px;
background: var(--bg-primary);
border: 1px solid var(--border-color);
background: #252526;
border: 1px solid #3e3e42;
border-radius: 8px 0 0 8px;
cursor: pointer;
color: var(--text-secondary);
color: #9d9d9d;
transition: all 0.2s;
z-index: 99;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.2);
}
.blackboard-toggle-btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
background: #2a2d2e;
color: #cccccc;
width: 36px;
}

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useRef } from 'react';
import { useState, useEffect, useRef, useCallback, useLayoutEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Save, FolderOpen, Download, Play, Pause, Square, SkipForward, Clipboard, ChevronRight, ChevronLeft, Copy } from 'lucide-react';
import { Save, FolderOpen, Download, Play, Pause, Square, SkipForward, Clipboard, ChevronRight, ChevronLeft, Copy, Home, Maximize2, Minimize2 } from 'lucide-react';
import { invoke } from '@tauri-apps/api/core';
import { open, message } from '@tauri-apps/plugin-dialog';
import { Core } from '@esengine/ecs-framework';
@@ -16,6 +16,7 @@ import { createLogger } from '@esengine/ecs-framework';
import { LocalBlackboardTypeGenerator } from '../../../../generators/LocalBlackboardTypeGenerator';
import { GlobalBlackboardTypeGenerator } from '../../../../generators/GlobalBlackboardTypeGenerator';
import { useExecutionController } from '../../../hooks/useExecutionController';
import { behaviorTreeFileService } from '../../../../services/BehaviorTreeFileService';
import './BehaviorTreeEditorPanel.css';
const logger = createLogger('BehaviorTreeEditorPanel');
@@ -31,11 +32,12 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
const {
isOpen,
pendingFilePath,
setPendingFilePath,
nodes,
connections,
exportToJSON,
exportToRuntimeAsset,
importFromJSON,
blackboardVariables,
setBlackboardVariables,
updateBlackboardVariable,
@@ -45,8 +47,7 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
setIsExecuting,
saveNodesDataSnapshot,
restoreNodesData,
setIsOpen,
reset
resetView
} = useBehaviorTreeStore();
const [currentFilePath, setCurrentFilePath] = useState<string | null>(null);
@@ -58,8 +59,10 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
const [isBlackboardOpen, setIsBlackboardOpen] = useState(true);
const [globalVariables, setGlobalVariables] = useState<Record<string, any>>({});
const [hasUnsavedGlobalChanges, setHasUnsavedGlobalChanges] = useState(false);
const [isFullscreen, setIsFullscreen] = useState(false);
const isInitialMount = useRef(true);
const initialStateSnapshot = useRef<{ nodes: number; variables: number }>({ nodes: 0, variables: 0 });
const processingFileRef = useRef<string | null>(null);
const {
executionMode,
@@ -148,31 +151,37 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
}
}, [nodes, blackboardVariables]);
useEffect(() => {
const handleOpenFile = async (data: any) => {
if (data.filePath && data.filePath.endsWith('.btree')) {
try {
const json = await invoke<string>('read_behavior_tree_file', { filePath: data.filePath });
importFromJSON(json);
setCurrentFilePath(data.filePath);
setHasUnsavedChanges(false);
isInitialMount.current = true;
initialStateSnapshot.current = {
nodes: nodes.length,
variables: Object.keys(blackboardVariables).length
};
logger.info('行为树已加载', data.filePath);
showToast(`已打开 ${data.filePath.split(/[\\/]/).pop()?.replace('.btree', '')}`, 'success');
} catch (error) {
logger.error('加载行为树失败', error);
showToast(`加载失败: ${error}`, 'error');
}
}
};
const loadFile = useCallback(async (filePath: string) => {
const result = await behaviorTreeFileService.loadFile(filePath);
const unsubscribe = messageHub?.subscribe('behavior-tree:open-file', handleOpenFile);
return () => unsubscribe?.();
}, [messageHub, importFromJSON, nodes.length, blackboardVariables]);
if (result.success && result.fileName) {
setCurrentFilePath(filePath);
setHasUnsavedChanges(false);
isInitialMount.current = true;
initialStateSnapshot.current = { nodes: 0, variables: 0 };
showToast(`已打开 ${result.fileName}`, 'success');
} else if (result.error) {
showToast(`加载失败: ${result.error}`, 'error');
}
}, [showToast]);
// 使用 useLayoutEffect 处理 pendingFilePath同步执行DOM 更新前)
// 这是文件加载的唯一入口,避免重复
useLayoutEffect(() => {
if (!pendingFilePath) return;
// 防止 React StrictMode 导致的重复执行
if (processingFileRef.current === pendingFilePath) {
return;
}
processingFileRef.current = pendingFilePath;
loadFile(pendingFilePath).then(() => {
setPendingFilePath(null);
processingFileRef.current = null;
});
}, [pendingFilePath, loadFile, setPendingFilePath]);
const loadGlobalBlackboard = async (path: string) => {
try {
@@ -223,15 +232,20 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
});
if (selected) {
const json = await invoke<string>('read_behavior_tree_file', { filePath: selected as string });
importFromJSON(json);
setCurrentFilePath(selected as string);
setHasUnsavedChanges(false);
isInitialMount.current = true;
logger.info('行为树已加载', selected);
const result = await behaviorTreeFileService.loadFile(selected as string);
if (result.success && result.fileName) {
setCurrentFilePath(selected as string);
setHasUnsavedChanges(false);
isInitialMount.current = true;
initialStateSnapshot.current = { nodes: 0, variables: 0 };
showToast(`已打开 ${result.fileName}`, 'success');
} else if (result.error) {
showToast(`加载失败: ${result.error}`, 'error');
}
}
} catch (error) {
logger.error('加载失败', error);
showToast(`加载失败: ${error}`, 'error');
}
};
@@ -373,12 +387,12 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
const handleCopyBehaviorTree = () => {
const buildNodeTree = (nodeId: string, depth: number = 0): string => {
const node = nodes.find(n => n.id === nodeId);
const node = nodes.find((n) => n.id === nodeId);
if (!node) return '';
const indent = ' '.repeat(depth);
const childrenText = node.children.length > 0
? `\n${node.children.map(childId => buildNodeTree(childId, depth + 1)).join('\n')}`
? `\n${node.children.map((childId) => buildNodeTree(childId, depth + 1)).join('\n')}`
: '';
const propertiesText = Object.keys(node.data).length > 0
@@ -388,7 +402,7 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
return `${indent}- ${node.template.displayName} (${node.template.type})${propertiesText}${childrenText}`;
};
const rootNode = nodes.find(n => n.id === ROOT_NODE_ID);
const rootNode = nodes.find((n) => n.id === ROOT_NODE_ID);
if (!rootNode) {
showToast('未找到根节点', 'error');
return;
@@ -408,26 +422,26 @@ ${buildNodeTree(ROOT_NODE_ID)}
${Object.entries(blackboardVariables).map(([key, value]) => ` - ${key}: ${JSON.stringify(value)}`).join('\n') || ' 无'}
全部节点详情:
${nodes.filter(n => n.id !== ROOT_NODE_ID).map(node => {
const incoming = connections.filter(c => c.to === node.id);
const outgoing = connections.filter(c => c.from === node.id);
return `
${nodes.filter((n) => n.id !== ROOT_NODE_ID).map((node) => {
const incoming = connections.filter((c) => c.to === node.id);
const outgoing = connections.filter((c) => c.from === node.id);
return `
[${node.template.displayName}]
类型: ${node.template.type}
分类: ${node.template.category}
类名: ${node.template.className || '无'}
ID: ${node.id}
子节点: ${node.children.length}
输入连接: ${incoming.length}${incoming.length > 0 ? '\n ' + incoming.map(c => {
const fromNode = nodes.find(n => n.id === c.from);
return `${fromNode?.template.displayName || '未知'}`;
}).join('\n ') : ''}
输出连接: ${outgoing.length}${outgoing.length > 0 ? '\n ' + outgoing.map(c => {
const toNode = nodes.find(n => n.id === c.to);
return `${toNode?.template.displayName || '未知'}`;
}).join('\n ') : ''}
输入连接: ${incoming.length}${incoming.length > 0 ? '\n ' + incoming.map((c) => {
const fromNode = nodes.find((n) => n.id === c.from);
return `${fromNode?.template.displayName || '未知'}`;
}).join('\n ') : ''}
输出连接: ${outgoing.length}${outgoing.length > 0 ? '\n ' + outgoing.map((c) => {
const toNode = nodes.find((n) => n.id === c.to);
return `${toNode?.template.displayName || '未知'}`;
}).join('\n ') : ''}
属性: ${JSON.stringify(node.data, null, 4)}`;
}).join('\n')}
}).join('\n')}
`.trim();
navigator.clipboard.writeText(treeStructure).then(() => {
@@ -590,12 +604,20 @@ ${nodes.filter(n => n.id !== ROOT_NODE_ID).map(node => {
setHasUnsavedGlobalChanges(true);
};
const toggleFullscreen = () => {
const newFullscreenState = !isFullscreen;
setIsFullscreen(newFullscreenState);
// 通知主界面切换全屏状态
messageHub?.publish('editor:fullscreen', { fullscreen: newFullscreenState });
};
if (!isOpen) {
return null;
}
return (
<div className="behavior-tree-editor-panel">
<div className={`behavior-tree-editor-panel ${isFullscreen ? 'fullscreen' : ''}`}>
<div className="behavior-tree-editor-toolbar">
{/* 文件操作 */}
<div className="toolbar-section">
@@ -654,6 +676,12 @@ ${nodes.filter(n => n.id !== ROOT_NODE_ID).map(node => {
{/* 视图控制 */}
<div className="toolbar-section">
<button onClick={resetView} className="toolbar-btn" title="重置视图">
<Home size={16} />
</button>
<button onClick={toggleFullscreen} className="toolbar-btn" title={isFullscreen ? '退出全屏' : '全屏编辑'}>
{isFullscreen ? <Minimize2 size={16} /> : <Maximize2 size={16} />}
</button>
<button onClick={() => setIsBlackboardOpen(!isBlackboardOpen)} className="toolbar-btn" title="黑板">
<Clipboard size={16} />
</button>