Feature/ecs behavior tree (#188)
* feat(behavior-tree): 完全 ECS 化的行为树系统 * feat(editor-app): 添加行为树可视化编辑器 * chore: 移除 Cocos Creator 扩展目录 * feat(editor-app): 行为树编辑器功能增强 * fix(editor-app): 修复 TypeScript 类型错误 * feat(editor-app): 使用 FlexLayout 重构面板系统并优化资产浏览器 * feat(editor-app): 改进编辑器UI样式并修复行为树执行顺序 * feat(behavior-tree,editor-app): 添加装饰器系统并优化编辑器性能 * feat(behavior-tree,editor-app): 添加属性绑定系统 * feat(editor-app,behavior-tree): 优化编辑器UI并改进行为树功能 * feat(editor-app,behavior-tree): 添加全局黑板系统并增强资产浏览器功能 * feat(behavior-tree,editor-app): 添加运行时资产导出系统 * feat(behavior-tree,editor-app): 添加SubTree系统和资产选择器 * feat(behavior-tree,editor-app): 优化系统架构并改进编辑器文件管理 * fix(behavior-tree,editor-app): 修复SubTree节点错误显示空节点警告 * fix(editor-app): 修复局部黑板类型定义文件扩展名错误
This commit is contained in:
1
packages/editor-app/src-tauri/Cargo.lock
generated
1
packages/editor-app/src-tauri/Cargo.lock
generated
@@ -754,6 +754,7 @@ dependencies = [
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-updater",
|
||||
"tokio",
|
||||
|
||||
@@ -16,6 +16,7 @@ tauri-build = { version = "2.0", features = [] }
|
||||
tauri = { version = "2.0", features = ["protocol-asset"] }
|
||||
tauri-plugin-shell = "2.0"
|
||||
tauri-plugin-dialog = "2.0"
|
||||
tauri-plugin-fs = "2.0"
|
||||
tauri-plugin-updater = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
@@ -74,3 +74,55 @@ pub async fn get_profiler_status(
|
||||
let server_lock = state.server.lock().await;
|
||||
Ok(server_lock.is_some())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn read_behavior_tree_file(file_path: String) -> Result<String, String> {
|
||||
use std::fs;
|
||||
|
||||
// 使用 Rust 标准库直接读取文件,绕过 Tauri 的 scope 限制
|
||||
fs::read_to_string(&file_path)
|
||||
.map_err(|e| format!("Failed to read file {}: {}", file_path, e))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn write_behavior_tree_file(file_path: String, content: String) -> Result<(), String> {
|
||||
use std::fs;
|
||||
|
||||
// 使用 Rust 标准库直接写入文件
|
||||
fs::write(&file_path, content)
|
||||
.map_err(|e| format!("Failed to write file {}: {}", file_path, e))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn read_global_blackboard(project_path: String) -> Result<String, String> {
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
let config_path = Path::new(&project_path).join(".ecs").join("global-blackboard.json");
|
||||
|
||||
if !config_path.exists() {
|
||||
return Ok(String::from(r#"{"version":"1.0","variables":[]}"#));
|
||||
}
|
||||
|
||||
fs::read_to_string(&config_path)
|
||||
.map_err(|e| format!("Failed to read global blackboard: {}", e))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn write_global_blackboard(project_path: String, content: String) -> Result<(), String> {
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
let ecs_dir = Path::new(&project_path).join(".ecs");
|
||||
let config_path = ecs_dir.join("global-blackboard.json");
|
||||
|
||||
// 创建 .ecs 目录(如果不存在)
|
||||
if !ecs_dir.exists() {
|
||||
fs::create_dir_all(&ecs_dir)
|
||||
.map_err(|e| format!("Failed to create .ecs directory: {}", e))?;
|
||||
}
|
||||
|
||||
fs::write(&config_path, content)
|
||||
.map_err(|e| format!("Failed to write global blackboard: {}", e))
|
||||
}
|
||||
|
||||
|
||||
@@ -97,6 +97,68 @@ async fn open_scene_dialog(app: AppHandle) -> Result<Option<String>, String> {
|
||||
Ok(file.map(|path| path.to_string()))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn open_behavior_tree_dialog(app: AppHandle) -> Result<Option<String>, String> {
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
|
||||
let file = app.dialog()
|
||||
.file()
|
||||
.set_title("Select Behavior Tree")
|
||||
.add_filter("Behavior Tree Files", &["btree"])
|
||||
.blocking_pick_file();
|
||||
|
||||
Ok(file.map(|path| path.to_string()))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn scan_behavior_trees(project_path: String) -> Result<Vec<String>, String> {
|
||||
use std::path::Path;
|
||||
use std::fs;
|
||||
|
||||
let behaviors_path = Path::new(&project_path).join(".ecs").join("behaviors");
|
||||
|
||||
if !behaviors_path.exists() {
|
||||
fs::create_dir_all(&behaviors_path)
|
||||
.map_err(|e| format!("Failed to create behaviors directory: {}", e))?;
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut btree_files = Vec::new();
|
||||
scan_directory_recursive(&behaviors_path, &behaviors_path, &mut btree_files)?;
|
||||
|
||||
Ok(btree_files)
|
||||
}
|
||||
|
||||
fn scan_directory_recursive(
|
||||
base_path: &std::path::Path,
|
||||
current_path: &std::path::Path,
|
||||
results: &mut Vec<String>
|
||||
) -> Result<(), String> {
|
||||
use std::fs;
|
||||
|
||||
let entries = fs::read_dir(current_path)
|
||||
.map_err(|e| format!("Failed to read directory: {}", e))?;
|
||||
|
||||
for entry in entries {
|
||||
let entry = entry.map_err(|e| format!("Failed to read entry: {}", e))?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
scan_directory_recursive(base_path, &path, results)?;
|
||||
} else if path.extension().and_then(|s| s.to_str()) == Some("btree") {
|
||||
if let Ok(relative) = path.strip_prefix(base_path) {
|
||||
let relative_str = relative.to_string_lossy()
|
||||
.replace('\\', "/")
|
||||
.trim_end_matches(".btree")
|
||||
.to_string();
|
||||
results.push(relative_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn scan_directory(path: String, pattern: String) -> Result<Vec<String>, String> {
|
||||
use glob::glob;
|
||||
@@ -147,6 +209,8 @@ struct DirectoryEntry {
|
||||
name: String,
|
||||
path: String,
|
||||
is_dir: bool,
|
||||
size: Option<u64>,
|
||||
modified: Option<u64>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -172,10 +236,36 @@ fn list_directory(path: String) -> Result<Vec<DirectoryEntry>, String> {
|
||||
Ok(entry) => {
|
||||
let entry_path = entry.path();
|
||||
if let Some(name) = entry_path.file_name() {
|
||||
let is_dir = entry_path.is_dir();
|
||||
|
||||
// 获取文件元数据
|
||||
let (size, modified) = match fs::metadata(&entry_path) {
|
||||
Ok(metadata) => {
|
||||
let size = if is_dir {
|
||||
None
|
||||
} else {
|
||||
Some(metadata.len())
|
||||
};
|
||||
|
||||
let modified = metadata.modified()
|
||||
.ok()
|
||||
.and_then(|time| {
|
||||
time.duration_since(std::time::UNIX_EPOCH)
|
||||
.ok()
|
||||
.map(|d| d.as_secs())
|
||||
});
|
||||
|
||||
(size, modified)
|
||||
}
|
||||
Err(_) => (None, None),
|
||||
};
|
||||
|
||||
entries.push(DirectoryEntry {
|
||||
name: name.to_string_lossy().to_string(),
|
||||
path: entry_path.to_string_lossy().to_string(),
|
||||
is_dir: entry_path.is_dir(),
|
||||
is_dir,
|
||||
size,
|
||||
modified,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -283,6 +373,133 @@ async fn get_profiler_status(
|
||||
Ok(server_lock.is_some())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn read_behavior_tree_file(file_path: String) -> Result<String, String> {
|
||||
use std::fs;
|
||||
|
||||
// 使用 Rust 标准库直接读取文件,绕过 Tauri 的 scope 限制
|
||||
fs::read_to_string(&file_path)
|
||||
.map_err(|e| format!("Failed to read file {}: {}", file_path, e))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn write_behavior_tree_file(file_path: String, content: String) -> Result<(), String> {
|
||||
use std::fs;
|
||||
|
||||
// 使用 Rust 标准库直接写入文件
|
||||
fs::write(&file_path, content)
|
||||
.map_err(|e| format!("Failed to write file {}: {}", file_path, e))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn write_binary_file(file_path: String, content: Vec<u8>) -> Result<(), String> {
|
||||
use std::fs;
|
||||
|
||||
// 写入二进制文件
|
||||
fs::write(&file_path, content)
|
||||
.map_err(|e| format!("Failed to write binary file {}: {}", file_path, e))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn read_global_blackboard(project_path: String) -> Result<String, String> {
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
let config_path = Path::new(&project_path).join(".ecs").join("global-blackboard.json");
|
||||
|
||||
if !config_path.exists() {
|
||||
return Ok(String::from(r#"{"version":"1.0","variables":[]}"#));
|
||||
}
|
||||
|
||||
fs::read_to_string(&config_path)
|
||||
.map_err(|e| format!("Failed to read global blackboard: {}", e))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn write_global_blackboard(project_path: String, content: String) -> Result<(), String> {
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
let ecs_dir = Path::new(&project_path).join(".ecs");
|
||||
let config_path = ecs_dir.join("global-blackboard.json");
|
||||
|
||||
// 创建 .ecs 目录(如果不存在)
|
||||
if !ecs_dir.exists() {
|
||||
fs::create_dir_all(&ecs_dir)
|
||||
.map_err(|e| format!("Failed to create .ecs directory: {}", e))?;
|
||||
}
|
||||
|
||||
fs::write(&config_path, content)
|
||||
.map_err(|e| format!("Failed to write global blackboard: {}", e))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn open_file_with_default_app(file_path: String) -> Result<(), String> {
|
||||
use std::process::Command;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
Command::new("cmd")
|
||||
.args(["/C", "start", "", &file_path])
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to open file: {}", e))?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Command::new("open")
|
||||
.arg(&file_path)
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to open file: {}", e))?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
Command::new("xdg-open")
|
||||
.arg(&file_path)
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to open file: {}", e))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn show_in_folder(file_path: String) -> Result<(), String> {
|
||||
use std::process::Command;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
Command::new("explorer")
|
||||
.args(["/select,", &file_path])
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to show in folder: {}", e))?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
Command::new("open")
|
||||
.args(["-R", &file_path])
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to show in folder: {}", e))?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::path::Path;
|
||||
let path = Path::new(&file_path);
|
||||
let parent = path.parent()
|
||||
.ok_or_else(|| "Failed to get parent directory".to_string())?;
|
||||
|
||||
Command::new("xdg-open")
|
||||
.arg(parent)
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to show in folder: {}", e))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let project_paths: Arc<Mutex<HashMap<String, String>>> = Arc::new(Mutex::new(HashMap::new()));
|
||||
let project_paths_clone = Arc::clone(&project_paths);
|
||||
@@ -294,6 +511,7 @@ fn main() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.register_uri_scheme_protocol("project", move |_app, request| {
|
||||
let project_paths = Arc::clone(&project_paths_clone);
|
||||
@@ -357,14 +575,23 @@ fn main() {
|
||||
open_project_dialog,
|
||||
save_scene_dialog,
|
||||
open_scene_dialog,
|
||||
open_behavior_tree_dialog,
|
||||
scan_directory,
|
||||
scan_behavior_trees,
|
||||
read_file_content,
|
||||
list_directory,
|
||||
set_project_base_path,
|
||||
toggle_devtools,
|
||||
start_profiler_server,
|
||||
stop_profiler_server,
|
||||
get_profiler_status
|
||||
get_profiler_status,
|
||||
read_behavior_tree_file,
|
||||
write_behavior_tree_file,
|
||||
write_binary_file,
|
||||
read_global_blackboard,
|
||||
write_global_blackboard,
|
||||
open_file_with_default_app,
|
||||
show_in_folder
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
||||
@@ -46,7 +46,8 @@
|
||||
"decorations": true,
|
||||
"transparent": false,
|
||||
"center": true,
|
||||
"skipTaskbar": false
|
||||
"skipTaskbar": false,
|
||||
"dragDropEnabled": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
@@ -72,8 +73,22 @@
|
||||
"updater:default",
|
||||
"updater:allow-check",
|
||||
"updater:allow-download",
|
||||
"updater:allow-install"
|
||||
]
|
||||
"updater:allow-install",
|
||||
"fs:default",
|
||||
"fs:allow-read-text-file",
|
||||
"fs:allow-write-text-file",
|
||||
"fs:allow-read-dir",
|
||||
"fs:allow-exists"
|
||||
],
|
||||
"scope": {
|
||||
"allow": [
|
||||
"$HOME/**",
|
||||
"$APPDATA/**",
|
||||
"$DESKTOP/**",
|
||||
"$DOCUMENT/**",
|
||||
"$DOWNLOAD/**"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -82,6 +97,9 @@
|
||||
"shell": {
|
||||
"open": true
|
||||
},
|
||||
"fs": {
|
||||
"requireLiteralLeadingDot": false
|
||||
},
|
||||
"updater": {
|
||||
"active": true,
|
||||
"endpoints": [
|
||||
|
||||
Reference in New Issue
Block a user