Files
esengine/packages/editor-app/src/components/ModuleListSetting.tsx
YHH 63f006ab62 feat: 添加跨平台运行时、资产系统和UI适配功能 (#256)
* feat(platform-common): 添加WASM加载器和环境检测API

* feat(rapier2d): 新增Rapier2D WASM绑定包

* feat(physics-rapier2d): 添加跨平台WASM加载器

* feat(asset-system): 添加运行时资产目录和bundle格式

* feat(asset-system-editor): 新增编辑器资产管理包

* feat(editor-core): 添加构建系统和模块管理

* feat(editor-app): 重构浏览器预览使用import maps

* feat(platform-web): 添加BrowserRuntime和资产读取

* feat(engine): 添加材质系统和着色器管理

* feat(material): 新增材质系统和着色器编辑器

* feat(tilemap): 增强tilemap编辑器和动画系统

* feat(modules): 添加module.json配置

* feat(core): 添加module.json和类型定义更新

* chore: 更新依赖和构建配置

* refactor(plugins): 更新插件模板使用ModuleManifest

* chore: 添加第三方依赖库

* chore: 移除BehaviourTree-ai和ecs-astar子模块

* docs: 更新README和文档主题样式

* fix: 修复Rust文档测试和添加rapier2d WASM绑定

* fix(tilemap-editor): 修复画布高DPI屏幕分辨率适配问题

* feat(ui): 添加UI屏幕适配系统(CanvasScaler/SafeArea)

* fix(ecs-engine-bindgen): 添加缺失的ecs-framework-math依赖

* fix: 添加缺失的包依赖修复CI构建

* fix: 修复CodeQL检测到的代码问题

* fix: 修复构建错误和缺失依赖

* fix: 修复类型检查错误

* fix(material-system): 修复tsconfig配置支持TypeScript项目引用

* fix(editor-core): 修复Rollup构建配置添加tauri external

* fix: 修复CodeQL检测到的代码问题

* fix: 修复CodeQL检测到的代码问题
2025-12-03 22:15:22 +08:00

306 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Module List Setting Component.
* 模块列表设置组件。
*
* Renders a list of engine modules with checkboxes to enable/disable.
* 渲染引擎模块列表,带复选框以启用/禁用。
*/
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { ChevronDown, ChevronRight, Package, AlertCircle } from 'lucide-react';
import type { ModuleManifest, ModuleCategory } from '@esengine/editor-core';
import './styles/ModuleListSetting.css';
/**
* Module entry with enabled state.
* 带启用状态的模块条目。
*/
interface ModuleEntry extends ModuleManifest {
enabled: boolean;
canDisable: boolean;
disableReason?: string;
}
/**
* Props for ModuleListSetting.
*/
interface ModuleListSettingProps {
/** Module manifests (static) | 模块清单列表(静态) */
modules?: ModuleManifest[];
/** Function to get modules dynamically (sizes from module.json) | 动态获取模块的函数(大小来自 module.json */
getModules?: () => ModuleManifest[];
/**
* Module IDs list. Meaning depends on useBlacklist.
* 模块 ID 列表。含义取决于 useBlacklist。
* - useBlacklist=false: enabled modules (whitelist)
* - useBlacklist=true: disabled modules (blacklist)
*/
value: string[];
/** Callback when modules change | 模块变更回调 */
onModulesChange: (moduleIds: string[]) => void;
/**
* Use blacklist mode: value contains disabled modules instead of enabled.
* 使用黑名单模式value 包含禁用的模块而不是启用的。
* Default: false (whitelist mode)
*/
useBlacklist?: boolean;
/** Validate if module can be disabled | 验证模块是否可禁用 */
validateDisable?: (moduleId: string) => Promise<{ canDisable: boolean; reason?: string }>;
}
/**
* Format bytes to human readable string.
*/
function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
/**
* Module List Setting Component.
* 模块列表设置组件。
*/
export const ModuleListSetting: React.FC<ModuleListSettingProps> = ({
modules: staticModules,
getModules,
value,
onModulesChange,
useBlacklist = false,
validateDisable
}) => {
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set(['Core', 'Rendering']));
const [validationError, setValidationError] = useState<{ moduleId: string; message: string } | null>(null);
const [loading, setLoading] = useState<string | null>(null);
// Get modules from function or static prop
// 从函数或静态 prop 获取模块
const modules = useMemo(() => {
if (getModules) {
return getModules();
}
return staticModules || [];
}, [getModules, staticModules]);
// Build module entries with enabled state | 构建带启用状态的模块条目
// In blacklist mode: enabled = NOT in value list
// In whitelist mode: enabled = IN value list
const moduleEntries: ModuleEntry[] = useMemo(() => {
return modules.map(mod => {
let enabled: boolean;
if (mod.isCore) {
enabled = true; // Core modules always enabled
} else if (useBlacklist) {
enabled = !value.includes(mod.id); // Blacklist: NOT in list = enabled
} else {
enabled = value.includes(mod.id); // Whitelist: IN list = enabled
}
return {
...mod,
enabled,
canDisable: !mod.isCore,
disableReason: mod.isCore ? 'Core module cannot be disabled' : undefined
};
});
}, [modules, value, useBlacklist]);
// Group by category | 按分类分组
const groupedModules = useMemo(() => {
const groups = new Map<string, ModuleEntry[]>();
const categoryOrder: ModuleCategory[] = ['Core', 'Rendering', 'Physics', 'AI', 'Audio', 'Networking', 'Other'];
// Initialize groups | 初始化分组
for (const cat of categoryOrder) {
groups.set(cat, []);
}
// Group modules | 分组模块
for (const mod of moduleEntries) {
const cat = mod.category || 'Other';
if (!groups.has(cat)) {
groups.set(cat, []);
}
groups.get(cat)!.push(mod);
}
// Filter empty groups | 过滤空分组
const result = new Map<string, ModuleEntry[]>();
for (const [cat, mods] of groups) {
if (mods.length > 0) {
result.set(cat, mods);
}
}
return result;
}, [moduleEntries]);
// Calculate total size (JS + WASM) | 计算总大小JS + WASM
const { totalJsSize, totalWasmSize, totalSize } = useMemo(() => {
let js = 0;
let wasm = 0;
for (const m of moduleEntries) {
if (m.enabled) {
js += m.jsSize || 0;
wasm += m.wasmSize || 0;
}
}
return { totalJsSize: js, totalWasmSize: wasm, totalSize: js + wasm };
}, [moduleEntries]);
// Toggle category expansion | 切换分类展开
const toggleCategory = useCallback((category: string) => {
setExpandedCategories(prev => {
const next = new Set(prev);
if (next.has(category)) {
next.delete(category);
} else {
next.add(category);
}
return next;
});
}, []);
// Handle module toggle | 处理模块切换
const handleModuleToggle = useCallback(async (module: ModuleEntry, enabled: boolean) => {
if (module.isCore) return;
// If disabling, validate first | 如果禁用,先验证
if (!enabled && validateDisable) {
setLoading(module.id);
try {
const result = await validateDisable(module.id);
if (!result.canDisable) {
setValidationError({
moduleId: module.id,
message: result.reason || `Cannot disable ${module.displayName}`
});
setLoading(null);
return;
}
} finally {
setLoading(null);
}
}
// Update module list based on mode
let newValue: string[];
if (useBlacklist) {
// Blacklist mode: value contains disabled modules
if (enabled) {
// Remove from blacklist (and also remove dependencies)
const toRemove = new Set([module.id]);
// Also enable dependencies if they were disabled
for (const depId of module.dependencies) {
toRemove.add(depId);
}
newValue = value.filter(id => !toRemove.has(id));
} else {
// Add to blacklist
newValue = [...value, module.id];
}
} else {
// Whitelist mode: value contains enabled modules
if (enabled) {
// Add to whitelist (and dependencies)
newValue = [...value];
const toEnable = [module.id, ...module.dependencies];
for (const id of toEnable) {
if (!newValue.includes(id)) {
newValue.push(id);
}
}
} else {
// Remove from whitelist
newValue = value.filter(id => id !== module.id);
}
}
onModulesChange(newValue);
}, [value, useBlacklist, onModulesChange, validateDisable]);
return (
<div className="module-list-setting">
{/* Module categories | 模块分类 */}
<div className="module-list-categories">
{Array.from(groupedModules.entries()).map(([category, mods]) => (
<div key={category} className="module-category-group">
<div
className="module-category-header"
onClick={() => toggleCategory(category)}
>
{expandedCategories.has(category) ? (
<ChevronDown size={14} />
) : (
<ChevronRight size={14} />
)}
<span className="module-category-name">{category}</span>
<span className="module-category-count">
{mods.filter(m => m.enabled).length}/{mods.length}
</span>
</div>
{expandedCategories.has(category) && (
<div className="module-category-items">
{mods.map(mod => (
<div
key={mod.id}
className={`module-item ${mod.enabled ? 'enabled' : ''} ${loading === mod.id ? 'loading' : ''}`}
>
<label className="module-checkbox-label">
<input
type="checkbox"
checked={mod.enabled}
disabled={mod.isCore || loading === mod.id}
onChange={(e) => handleModuleToggle(mod, e.target.checked)}
/>
<Package size={14} className="module-icon" />
<span className="module-name">{mod.displayName}</span>
{mod.isCore && (
<span className="module-badge core">Core</span>
)}
</label>
{(mod.jsSize || mod.wasmSize) ? (
<span className="module-size">
{mod.isCore ? '' : '+'}
{formatBytes((mod.jsSize || 0) + (mod.wasmSize || 0))}
{(mod.wasmSize ?? 0) > 0 && (
<span className="module-wasm-indicator" title="Includes WASM"></span>
)}
</span>
) : null}
</div>
))}
</div>
)}
</div>
))}
</div>
{/* Size footer | 大小页脚 */}
<div className="module-list-footer">
<span className="module-list-size-label">Runtime size:</span>
<span className="module-list-size-value">
{formatBytes(totalSize)}
{totalWasmSize > 0 && (
<span className="module-size-breakdown">
(JS: {formatBytes(totalJsSize)} + WASM: {formatBytes(totalWasmSize)})
</span>
)}
</span>
</div>
{/* Validation error toast | 验证错误提示 */}
{validationError && (
<div className="module-validation-error">
<AlertCircle size={14} />
<span>{validationError.message}</span>
<button onClick={() => setValidationError(null)}>Dismiss</button>
</div>
)}
</div>
);
};
export default ModuleListSetting;