Files
esengine/packages/editor-app/src/stores/BuildSettingsStore.ts
T
YHH beaa1d09de feat: 预制体系统与架构改进 (#303)
* feat(prefab): 实现预制体系统和编辑器 UX 改进

## 预制体系统
- 新增 PrefabSerializer: 预制体序列化/反序列化
- 新增 PrefabInstanceComponent: 追踪预制体实例来源和修改
- 新增 PrefabService: 预制体核心服务
- 新增 PrefabLoader: 预制体资产加载器
- 新增预制体命令: Create/Instantiate/Apply/Revert/BreakLink

## 预制体编辑模式
- 支持双击 .prefab 文件进入编辑模式
- 预制体编辑模式工具栏 (保存/退出)
- 预制体实例指示器和操作菜单

## 编辑器 UX 改进
- SceneHierarchy 快捷键: F2 重命名, Ctrl+D 复制, ↑↓ 导航
- 支持双击实体名称内联编辑
- 删除实体时显示子节点数量警告
- 右键菜单添加重命名/复制选项及快捷键提示
- 布局持久化和重置功能

## Bug 修复
- 修复 editor-runtime 组件类重复导致的 TransformComponent 不识别问题
- 修复 .prefab-name 样式覆盖导致预制体工具栏文字不可见
- 修复 Inspector 资源字段高度不正确问题

* feat(editor): 改进编辑器 UX 交互体验

- ContentBrowser: 加载动画 spinner、搜索高亮、改进空状态设计
- SceneHierarchy: 选中项自动滚动到视图、搜索清除按钮
- PropertyInspector: 输入框本地状态管理、Enter/Escape 键处理
- EntityInspector: 组件折叠状态持久化、属性搜索清除按钮
- Viewport: 变换操作实时数值显示
- 国际化: 添加相关文本 (en/zh)

* fix(build): 修复 Web 构建资产加载和编辑器 UX 改进

构建系统修复:
- 修复 asset-catalog.json 字段名不匹配 (entries vs assets)
- 修复 BrowserFileSystemService 支持两种目录格式
- 修复 bundle 策略检测逻辑 (空对象判断)
- 修复 module.json 中 assetExtensions 声明和类型推断

行为树修复:
- 修复 BehaviorTreeExecutionSystem 使用 loadAsset 替代 loadAssetByPath
- 修复 BehaviorTreeAssetType 常量与 module.json 类型名一致 (behavior-tree)

编辑器 UX 改进:
- 构建完成对话框添加"打开文件夹"按钮
- 构建完成对话框样式优化 (圆形图标背景、按钮布局)
- SceneHierarchy 响应式布局 (窄窗口自动隐藏 Type 列)
- SceneHierarchy 隐藏滚动条

错误追踪:
- 添加全局错误处理器写入日志文件 (%TEMP%/esengine-editor-crash.log)
- 添加 append_to_log Tauri 命令

* feat(render): 修复 UI 渲染和点击特效系统

## UI 渲染修复
- 修复 GUID 验证 bug,使用统一的 isValidGUID() 函数
- 修复 UI 渲染顺序随机问题,Rust 端使用 IndexMap 替代 HashMap
- Web 运行时添加 assetPathResolver 支持 GUID 解析
- UIInteractableComponent.blockEvents 默认值改为 false

## 点击特效系统
- 新增 ClickFxComponent 和 ClickFxSystem
- 支持在点击位置播放粒子效果
- 支持多种触发模式和粒子轮换

## Camera 系统重构
- CameraSystem 从 ecs-engine-bindgen 移至 camera 包
- 新增 CameraManager 统一管理相机

## 编辑器改进
- 改进属性面板 UI 交互
- 粒子编辑器面板优化
- Transform 命令系统

* feat(render): 实现 Sorting Layer 系统和 Overlay 渲染层

- 新增 SortingLayerManager 管理排序层级 (Background, Default, Foreground, UI, Overlay)
- 实现 ISortable 接口,统一 Sprite、UI、Particle 的排序属性
- 修复粒子 Overlay 层被 UI 遮挡问题:添加独立的 Overlay Pass 在 UI 之后渲染
- 更新粒子资产格式:从 sortingOrder 改为 sortingLayer + orderInLayer
- 更新粒子编辑器面板支持新的排序属性
- 优化 UI 渲染系统使用新的排序层级

* feat(ci): 集成 SignPath 代码签名服务

- 添加 SignPath 自动签名工作流(Windows)
- 配置 release-editor.yml 支持代码签名
- 将构建改为草稿模式,等待签名完成后发布
- 添加证书文件到 .gitignore 防止泄露

* fix(asset): 修复 Web 构建资产路径解析和全局单例移除

## 资产路径修复
- 修复 Tauri 本地服务器 `/asset?path=...` 路径解析,正确与 root 目录连接
- BrowserPathResolver 支持两种模式:
  - 'proxy': 使用 /asset?path=... 格式(编辑器 Run in Browser)
  - 'direct': 使用直接路径 /assets/path.png(独立 Web 构建)
- BrowserRuntime 使用 'direct' 模式,无需 Tauri 代理

## 架构改进 - 移除全局单例
- 移除 globalAssetManager 导出,改用 AssetManagerToken 依赖注入
- 移除 globalPathResolver 导出,改用 PathResolutionService
- 移除 globalPathResolutionService 导出
- ParticleUpdateSystem/ClickFxSystem 通过 setAssetManager() 注入依赖
- EngineService 使用 new AssetManager() 替代全局实例

## 新增服务
- PathResolutionService: 统一路径解析接口
- RuntimeModeService: 运行时模式查询服务
- SerializationContext: EntityRef 序列化上下文

## 其他改进
- 完善 ServiceToken 注释说明本地定义的意图
- 导出 BrowserPathResolveMode 类型

* fix(build): 添加 world-streaming composite 设置修复类型检查

* fix(build): 移除 world-streaming 引用避免 composite 冲突

* fix(build): 将 const enum 改为 enum 兼容 isolatedModules

* fix(build): 添加缺失的 IAssetManager 导入
2025-12-13 19:44:08 +08:00

515 lines
18 KiB
TypeScript

/**
* Build Settings Store - 构建设置状态管理
* Build Settings State Management
*
* 使用 Zustand 替代 BuildSettingsPanel 中的大量 useEffect 和 useState
* Using Zustand to replace numerous useEffect and useState in BuildSettingsPanel
*/
import { create } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';
import type {
BuildService,
BuildProgress,
BuildConfig,
WebBuildConfig,
WeChatBuildConfig,
ProjectService,
BuildSettingsConfig
} from '@esengine/editor-core';
import { BuildPlatform, BuildStatus } from '@esengine/editor-core';
import { EngineService } from '../services/EngineService';
// ============= Types =============
export type PlatformType =
| 'windows'
| 'macos'
| 'linux'
| 'android'
| 'ios'
| 'web'
| 'wechat-minigame';
export interface BuildProfile {
id: string;
name: string;
platform: PlatformType;
isActive?: boolean;
}
export interface SceneEntry {
path: string;
enabled: boolean;
}
export interface BuildSettings {
scenes: SceneEntry[];
scriptingDefines: string[];
companyName: string;
productName: string;
version: string;
developmentBuild: boolean;
sourceMap: boolean;
compressionMethod: 'Default' | 'LZ4' | 'LZ4HC';
buildMode: 'split-bundles' | 'single-bundle' | 'single-file';
}
export interface BuildResult {
success: boolean;
outputPath: string;
duration: number;
warnings: string[];
error?: string;
}
interface BuildSettingsState {
// 配置状态 | Profile state
profiles: BuildProfile[];
selectedPlatform: PlatformType;
selectedProfile: BuildProfile | null;
settings: BuildSettings;
// UI 状态 | UI state
expandedSections: Record<string, boolean>;
// 构建状态 | Build state
isBuilding: boolean;
buildProgress: BuildProgress | null;
buildResult: BuildResult | null;
showBuildProgress: boolean;
// 服务引用 | Service references
_buildService: BuildService | null;
_projectService: ProjectService | null;
_projectPath: string | null;
// 内部状态 | Internal state
_initialized: boolean;
_saveTimeout: NodeJS.Timeout | null;
_progressInterval: NodeJS.Timeout | null;
}
interface BuildSettingsActions {
// 初始化 | Initialization
initialize: (params: {
projectPath: string;
buildService?: BuildService;
projectService?: ProjectService;
availableScenes?: string[];
}) => void;
cleanup: () => void;
// 配置操作 | Profile actions
setSelectedPlatform: (platform: PlatformType) => void;
setSelectedProfile: (profile: BuildProfile | null) => void;
addProfile: () => void;
// 设置操作 | Settings actions
updateSettings: (partial: Partial<BuildSettings>) => void;
setSceneEnabled: (index: number, enabled: boolean) => void;
addScene: (path: string) => void;
addDefine: (define: string) => void;
removeDefine: (index: number) => void;
// UI 操作 | UI actions
toggleSection: (section: string) => void;
// 构建操作 | Build actions
startBuild: () => Promise<void>;
cancelBuild: () => void;
closeBuildProgress: () => void;
}
export type BuildSettingsStore = BuildSettingsState & BuildSettingsActions;
// ============= Constants =============
const DEFAULT_SETTINGS: BuildSettings = {
scenes: [],
scriptingDefines: [],
companyName: 'DefaultCompany',
productName: 'MyGame',
version: '0.1.0',
developmentBuild: false,
sourceMap: false,
compressionMethod: 'Default',
buildMode: 'split-bundles',
};
const DEFAULT_PROFILES: BuildProfile[] = [
{ id: 'web-dev', name: 'Web - Development', platform: 'web', isActive: true },
{ id: 'web-prod', name: 'Web - Production', platform: 'web' },
{ id: 'wechat', name: 'WeChat Mini Game', platform: 'wechat-minigame' },
];
// ============= Helper Functions =============
const getPlatformEnum = (platformType: PlatformType): BuildPlatform => {
const platformMap: Record<PlatformType, BuildPlatform> = {
'web': BuildPlatform.Web,
'wechat-minigame': BuildPlatform.WeChatMiniGame,
'windows': BuildPlatform.Desktop,
'macos': BuildPlatform.Desktop,
'linux': BuildPlatform.Desktop,
'android': BuildPlatform.Android,
'ios': BuildPlatform.iOS
};
return platformMap[platformType];
};
// ============= Store =============
export const useBuildSettingsStore = create<BuildSettingsStore>()(
subscribeWithSelector((set, get) => ({
// 初始状态 | Initial state
profiles: DEFAULT_PROFILES,
selectedPlatform: 'web',
selectedProfile: DEFAULT_PROFILES[0] ?? null,
settings: DEFAULT_SETTINGS,
expandedSections: {
sceneList: true,
scriptingDefines: true,
platformSettings: true,
playerSettings: true,
},
isBuilding: false,
buildProgress: null,
buildResult: null,
showBuildProgress: false,
_buildService: null,
_projectService: null,
_projectPath: null,
_initialized: false,
_saveTimeout: null,
_progressInterval: null,
// ===== 初始化 | Initialization =====
initialize: ({ projectPath, buildService, projectService, availableScenes }) => {
const state = get();
if (state._initialized) return;
set({
_buildService: buildService || null,
_projectService: projectService || null,
_projectPath: projectPath,
_initialized: true,
});
// 从 projectService 加载已保存的设置
// Load saved settings from projectService
if (projectService) {
const savedSettings = projectService.getBuildSettings();
if (savedSettings) {
set(prev => ({
settings: {
...prev.settings,
scriptingDefines: savedSettings.scriptingDefines || [],
companyName: savedSettings.companyName || prev.settings.companyName,
productName: savedSettings.productName || prev.settings.productName,
version: savedSettings.version || prev.settings.version,
developmentBuild: savedSettings.developmentBuild ?? prev.settings.developmentBuild,
sourceMap: savedSettings.sourceMap ?? prev.settings.sourceMap,
compressionMethod: savedSettings.compressionMethod || prev.settings.compressionMethod,
buildMode: savedSettings.buildMode || prev.settings.buildMode
}
}));
}
// 初始化场景列表
// Initialize scene list
if (availableScenes && availableScenes.length > 0) {
const savedScenes = savedSettings?.scenes || [];
set(prev => ({
settings: {
...prev.settings,
scenes: availableScenes.map(path => ({
path,
enabled: savedScenes.length > 0 ? savedScenes.includes(path) : true
}))
}
}));
}
}
},
cleanup: () => {
const state = get();
// 清理定时器 | Clear timers
if (state._saveTimeout) {
clearTimeout(state._saveTimeout);
}
if (state._progressInterval) {
clearInterval(state._progressInterval);
}
set({
_buildService: null,
_projectService: null,
_projectPath: null,
_initialized: false,
_saveTimeout: null,
_progressInterval: null,
isBuilding: false,
buildProgress: null,
buildResult: null,
showBuildProgress: false,
});
},
// ===== 配置操作 | Profile actions =====
setSelectedPlatform: (platform) => {
const { profiles } = get();
const profile = profiles.find(p => p.platform === platform);
set({
selectedPlatform: platform,
selectedProfile: profile || null
});
},
setSelectedProfile: (profile) => {
set({
selectedProfile: profile,
selectedPlatform: profile?.platform || get().selectedPlatform
});
},
addProfile: () => {
const { selectedPlatform, profiles } = get();
const newProfile: BuildProfile = {
id: `profile-${Date.now()}`,
name: `${selectedPlatform} - New Profile`,
platform: selectedPlatform,
};
set({
profiles: [...profiles, newProfile],
selectedProfile: newProfile
});
},
// ===== 设置操作 | Settings actions =====
updateSettings: (partial) => {
set(prev => ({
settings: { ...prev.settings, ...partial }
}));
// 防抖保存 | Debounced save
const state = get();
if (state._saveTimeout) {
clearTimeout(state._saveTimeout);
}
const timeout = setTimeout(() => {
const { _projectService, settings } = get();
if (_projectService) {
const configToSave: BuildSettingsConfig = {
scenes: settings.scenes.filter(s => s.enabled).map(s => s.path),
scriptingDefines: settings.scriptingDefines,
companyName: settings.companyName,
productName: settings.productName,
version: settings.version,
developmentBuild: settings.developmentBuild,
sourceMap: settings.sourceMap,
compressionMethod: settings.compressionMethod,
buildMode: settings.buildMode
};
_projectService.updateBuildSettings(configToSave);
}
}, 500);
set({ _saveTimeout: timeout });
},
setSceneEnabled: (index, enabled) => {
set(prev => ({
settings: {
...prev.settings,
scenes: prev.settings.scenes.map((s, i) =>
i === index ? { ...s, enabled } : s
)
}
}));
// 触发保存 | Trigger save
get().updateSettings({});
},
addScene: (path) => {
const { settings } = get();
if (settings.scenes.some(s => s.path === path)) return;
set(prev => ({
settings: {
...prev.settings,
scenes: [...prev.settings.scenes, { path, enabled: true }]
}
}));
get().updateSettings({});
},
addDefine: (define) => {
set(prev => ({
settings: {
...prev.settings,
scriptingDefines: [...prev.settings.scriptingDefines, define]
}
}));
get().updateSettings({});
},
removeDefine: (index) => {
set(prev => ({
settings: {
...prev.settings,
scriptingDefines: prev.settings.scriptingDefines.filter((_, i) => i !== index)
}
}));
get().updateSettings({});
},
// ===== UI 操作 | UI actions =====
toggleSection: (section) => {
set(prev => ({
expandedSections: {
...prev.expandedSections,
[section]: !prev.expandedSections[section]
}
}));
},
// ===== 构建操作 | Build actions =====
startBuild: async () => {
const { selectedProfile, settings, _buildService, _projectPath } = get();
if (!selectedProfile || !_projectPath || !_buildService) {
console.warn('Cannot start build: missing profile, path, or service');
return;
}
set({
isBuilding: true,
buildProgress: null,
buildResult: null,
showBuildProgress: true,
});
// 启动进度轮询(但不用 setInterval,用 buildService 的回调)
// Start progress polling (but use buildService callback, not setInterval)
try {
const platform = getPlatformEnum(selectedProfile.platform);
const baseConfig = {
platform,
outputPath: `${_projectPath}/build/${selectedProfile.platform}`,
isRelease: !settings.developmentBuild,
sourceMap: settings.sourceMap,
scenes: settings.scenes.filter(s => s.enabled).map(s => s.path)
};
let buildConfig: BuildConfig;
if (platform === BuildPlatform.Web) {
// 从 AssetLoaderFactory 获取插件注册的扩展名
// Get plugin-registered extensions from AssetLoaderFactory
let assetExtensions: string[] | undefined;
let assetTypeMap: Record<string, string> | undefined;
try {
const assetManager = EngineService.getInstance().getAssetManager();
if (assetManager) {
const loaderFactory = assetManager.getLoaderFactory();
assetExtensions = loaderFactory.getAllSupportedExtensions();
assetTypeMap = loaderFactory.getExtensionTypeMap();
}
} catch (e) {
console.warn('Failed to get asset extensions from loader factory:', e);
}
const webConfig: WebBuildConfig = {
...baseConfig,
platform: BuildPlatform.Web,
buildMode: settings.buildMode,
generateHtml: true,
minify: !settings.developmentBuild,
generateAssetCatalog: true,
assetLoadingStrategy: 'on-demand',
assetExtensions,
assetTypeMap
};
buildConfig = webConfig;
} else if (platform === BuildPlatform.WeChatMiniGame) {
const wechatConfig: WeChatBuildConfig = {
...baseConfig,
platform: BuildPlatform.WeChatMiniGame,
appId: '',
useSubpackages: false,
mainPackageLimit: 4096,
usePlugins: false
};
buildConfig = wechatConfig;
} else {
buildConfig = baseConfig;
}
// 使用回调更新进度,而不是轮询
// Use callback to update progress, not polling
const result = await _buildService.build(buildConfig, (progress) => {
set({ buildProgress: progress });
});
set({
buildResult: {
success: result.success,
outputPath: result.outputPath,
duration: result.duration,
warnings: result.warnings,
error: result.error
}
});
} catch (error) {
console.error('Build failed:', error);
set({
buildResult: {
success: false,
outputPath: '',
duration: 0,
warnings: [],
error: error instanceof Error ? error.message : String(error)
}
});
} finally {
set({ isBuilding: false });
}
},
cancelBuild: () => {
const { _buildService } = get();
if (_buildService) {
_buildService.cancelBuild();
}
},
closeBuildProgress: () => {
const { isBuilding } = get();
if (!isBuilding) {
set({
showBuildProgress: false,
buildProgress: null,
buildResult: null,
});
}
},
}))
);
// ============= Selectors =============
/** 获取当前平台的配置列表 | Get profiles for current platform */
export const selectProfilesForPlatform = (state: BuildSettingsStore): BuildProfile[] => {
return state.profiles.filter(p => p.platform === state.selectedPlatform);
};
/** 获取启用的场景列表 | Get enabled scenes */
export const selectEnabledScenes = (state: BuildSettingsStore): string[] => {
return state.settings.scenes.filter(s => s.enabled).map(s => s.path);
};