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检测到的代码问题
This commit is contained in:
YHH
2025-12-03 22:15:22 +08:00
committed by GitHub
parent caf7622aa0
commit 63f006ab62
496 changed files with 77601 additions and 4067 deletions

View File

@@ -0,0 +1,514 @@
/**
* ShaderAssetInspectorProvider - Inspector provider for .shader files.
* 着色器资产检视器提供者 - 用于 .shader 文件的检视器。
*/
import React, { useState, useEffect, useCallback, useRef } from 'react';
import type { IInspectorProvider, InspectorContext } from '@esengine/editor-core';
import {
Save, RotateCcw, Play, AlertTriangle, CheckCircle,
Code, ChevronDown, ChevronRight, BarChart3, FileCode
} from 'lucide-react';
import { ShaderAnalyzer, ShaderAnalysis } from '../analysis/ShaderAnalyzer';
import '../styles/ShaderInspector.css';
/**
* Asset file info interface.
* 资产文件信息接口。
*/
interface AssetFileInfo {
name: string;
path: string;
extension?: string;
size?: number;
modified?: number;
isDirectory: boolean;
}
/**
* Asset file target with content.
* 带内容的资产文件目标。
*/
interface AssetFileTarget {
type: 'asset-file';
data: AssetFileInfo;
content?: string;
}
/**
* Shader data structure (internal format for editing).
* 着色器数据结构(用于编辑的内部格式)。
*/
interface ShaderData {
version?: number;
name: string;
vertex: string;
fragment: string;
}
/**
* Shader file format (wrapper format).
* 着色器文件格式(包装格式)。
*/
interface ShaderFileFormat {
version: number;
shader: {
name: string;
vertexSource: string;
fragmentSource: string;
};
}
interface ShaderInspectorViewProps {
fileInfo: AssetFileInfo;
content: string;
onSave?: (path: string, content: string) => Promise<void>;
}
/**
* Shader Inspector View Component.
* 着色器检视器视图组件。
*/
function ShaderInspectorView({ fileInfo, content, onSave }: ShaderInspectorViewProps) {
const [shader, setShader] = useState<ShaderData | null>(null);
const [isDirty, setIsDirty] = useState(false);
const [error, setError] = useState<string | null>(null);
const [activeTab, setActiveTab] = useState<'vertex' | 'fragment'>('fragment');
const [vertexAnalysis, setVertexAnalysis] = useState<ShaderAnalysis | null>(null);
const [fragmentAnalysis, setFragmentAnalysis] = useState<ShaderAnalysis | null>(null);
const [analysisExpanded, setAnalysisExpanded] = useState(true);
const [compileStatus, setCompileStatus] = useState<'none' | 'success' | 'error'>('none');
const [compileError, setCompileError] = useState<string | null>(null);
const analyzer = useRef(new ShaderAnalyzer());
// Parse shader content.
// 解析着色器内容。
useEffect(() => {
try {
const parsed = JSON.parse(content) as ShaderFileFormat;
// Convert from file format to internal format.
// 从文件格式转换为内部格式。
setShader({
version: parsed.version,
name: parsed.shader.name,
vertex: parsed.shader.vertexSource,
fragment: parsed.shader.fragmentSource
});
setError(null);
setIsDirty(false);
setCompileStatus('none');
} catch (e) {
setError('Failed to parse shader file');
setShader(null);
}
}, [content]);
// Analyze shader when source changes.
// 当源代码改变时分析着色器。
useEffect(() => {
if (shader) {
setVertexAnalysis(analyzer.current.analyze(shader.vertex || '', true));
setFragmentAnalysis(analyzer.current.analyze(shader.fragment || '', false));
}
}, [shader?.vertex, shader?.fragment]);
const handleSave = useCallback(async () => {
if (!shader || !onSave) return;
try {
// Convert internal format back to file format.
// 将内部格式转换回文件格式。
const fileData: ShaderFileFormat = {
version: shader.version || 1,
shader: {
name: shader.name,
vertexSource: shader.vertex,
fragmentSource: shader.fragment
}
};
const jsonContent = JSON.stringify(fileData, null, 2);
await onSave(fileInfo.path, jsonContent);
setIsDirty(false);
} catch (e) {
console.error('[ShaderInspector] Failed to save:', e);
}
}, [shader, fileInfo.path, onSave]);
const handleReset = useCallback(() => {
try {
const parsed = JSON.parse(content) as ShaderFileFormat;
setShader({
version: parsed.version,
name: parsed.shader.name,
vertex: parsed.shader.vertexSource,
fragment: parsed.shader.fragmentSource
});
setIsDirty(false);
setCompileStatus('none');
} catch (e) {
// ignore
}
}, [content]);
const handleSourceChange = (type: 'vertex' | 'fragment', value: string) => {
if (!shader) return;
setShader({ ...shader, [type]: value });
setIsDirty(true);
setCompileStatus('none');
};
const handleCompile = async () => {
if (!shader) return;
setCompileStatus('none');
setCompileError(null);
try {
// Dynamic import to avoid circular dependencies.
// 动态导入避免循环依赖。
const { getMaterialManager, Shader } = await import('@esengine/material-system');
const materialManager = getMaterialManager();
if (!materialManager) {
setCompileError('MaterialManager not available');
setCompileStatus('error');
return;
}
// Create test shader.
// 创建测试着色器。
const testShader = new Shader(
`test_${Date.now()}`,
shader.vertex,
shader.fragment
);
const shaderId = await materialManager.registerShader(testShader);
if (shaderId > 0) {
setCompileStatus('success');
materialManager.removeShader(shaderId);
} else {
setCompileError('Compilation failed');
setCompileStatus('error');
}
} catch (err: any) {
setCompileError(err.message || 'Compilation failed');
setCompileStatus('error');
}
};
const currentAnalysis = activeTab === 'vertex' ? vertexAnalysis : fragmentAnalysis;
if (error) {
return (
<div className="entity-inspector shader-inspector">
<div className="inspector-header">
<FileCode size={16} style={{ color: '#60a5fa' }} />
<span className="entity-name">{fileInfo.name}</span>
</div>
<div className="inspector-content">
<div className="shader-error">{error}</div>
</div>
</div>
);
}
if (!shader) {
return (
<div className="entity-inspector shader-inspector">
<div className="inspector-header">
<FileCode size={16} style={{ color: '#60a5fa' }} />
<span className="entity-name">{fileInfo.name}</span>
</div>
<div className="inspector-content">
<div className="shader-loading">Loading...</div>
</div>
</div>
);
}
return (
<div className="entity-inspector shader-inspector">
{/* Header */}
<div className="inspector-header">
<FileCode size={16} style={{ color: '#60a5fa' }} />
<span className="entity-name">{shader.name || fileInfo.name}</span>
{isDirty && <span className="shader-dirty-indicator">*</span>}
</div>
{/* Toolbar */}
<div className="shader-toolbar">
<button
className="shader-toolbar-btn"
onClick={handleCompile}
title="Compile shader"
>
<Play size={14} />
<span>Compile</span>
</button>
<button
className="shader-toolbar-btn"
onClick={handleSave}
disabled={!isDirty || !onSave}
title="Save"
>
<Save size={14} />
<span>Save</span>
</button>
<button
className="shader-toolbar-btn"
onClick={handleReset}
disabled={!isDirty}
title="Reset"
>
<RotateCcw size={14} />
<span>Reset</span>
</button>
</div>
{/* Compile Status */}
{compileStatus === 'success' && (
<div className="shader-status success">
<CheckCircle size={14} />
<span>Compilation successful!</span>
</div>
)}
{compileStatus === 'error' && (
<div className="shader-status error">
<AlertTriangle size={14} />
<span>{compileError}</span>
</div>
)}
<div className="inspector-content">
{/* Basic Properties */}
<div className="inspector-section">
<div className="section-title">Properties</div>
<div className="property-field">
<label className="property-label">Name</label>
<input
type="text"
className="shader-input"
value={shader.name}
onChange={(e) => {
setShader({ ...shader, name: e.target.value });
setIsDirty(true);
}}
/>
</div>
<div className="property-field">
<label className="property-label">Version</label>
<input
type="number"
className="shader-input"
value={shader.version || 1}
onChange={(e) => {
setShader({ ...shader, version: parseInt(e.target.value, 10) || 1 });
setIsDirty(true);
}}
/>
</div>
</div>
{/* Shader Source Tabs */}
<div className="inspector-section">
<div className="section-title">Source Code</div>
<div className="shader-tabs">
<button
className={`shader-tab ${activeTab === 'vertex' ? 'active' : ''}`}
onClick={() => setActiveTab('vertex')}
>
<Code size={12} />
Vertex
</button>
<button
className={`shader-tab ${activeTab === 'fragment' ? 'active' : ''}`}
onClick={() => setActiveTab('fragment')}
>
<Code size={12} />
Fragment
</button>
</div>
<div className="shader-code-wrapper">
<textarea
className="shader-code-editor"
value={activeTab === 'vertex' ? shader.vertex : shader.fragment}
onChange={e => handleSourceChange(activeTab, e.target.value)}
spellCheck={false}
placeholder={`Enter ${activeTab} shader code...`}
/>
</div>
</div>
{/* Analysis Section */}
<div className="inspector-section">
<div
className="section-title section-title-collapsible"
onClick={() => setAnalysisExpanded(!analysisExpanded)}
>
{analysisExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
<BarChart3 size={14} />
<span>Analysis</span>
</div>
{analysisExpanded && currentAnalysis && (
<div className="shader-analysis">
{/* Complexity Badge */}
<div className="analysis-row">
<span className="analysis-label">Complexity</span>
<span className={`complexity-badge ${currentAnalysis.complexity.level}`}>
{currentAnalysis.complexity.level.toUpperCase()}
</span>
</div>
{/* Metrics */}
<div className="analysis-row">
<span className="analysis-label">Instructions</span>
<span className="analysis-value">~{currentAnalysis.complexity.instructionCount}</span>
</div>
<div className="analysis-row">
<span className="analysis-label">Texture Samples</span>
<span className="analysis-value">{currentAnalysis.complexity.textureSamples}</span>
</div>
<div className="analysis-row">
<span className="analysis-label">Branches</span>
<span className="analysis-value">{currentAnalysis.complexity.branches}</span>
</div>
<div className="analysis-row">
<span className="analysis-label">Loops</span>
<span className="analysis-value">{currentAnalysis.complexity.loops}</span>
</div>
<div className="analysis-row">
<span className="analysis-label">Math Ops</span>
<span className="analysis-value">{currentAnalysis.complexity.mathOps}</span>
</div>
{/* Uniforms */}
{currentAnalysis.uniforms.length > 0 && (
<div className="analysis-group">
<div className="analysis-group-title">
Uniforms ({currentAnalysis.uniforms.length})
</div>
{currentAnalysis.uniforms.map((u, i) => (
<div key={i} className="analysis-item">
<span className="item-type">{u.type}</span>
<span className="item-name">{u.name}</span>
{u.arraySize && <span className="item-array">[{u.arraySize}]</span>}
</div>
))}
</div>
)}
{/* Attributes (vertex only) */}
{activeTab === 'vertex' && currentAnalysis.attributes.length > 0 && (
<div className="analysis-group">
<div className="analysis-group-title">
Attributes ({currentAnalysis.attributes.length})
</div>
{currentAnalysis.attributes.map((a, i) => (
<div key={i} className="analysis-item">
<span className="item-type">{a.type}</span>
<span className="item-name">{a.name}</span>
{a.location !== undefined && (
<span className="item-location">loc={a.location}</span>
)}
</div>
))}
</div>
)}
{/* Varyings */}
{currentAnalysis.varyings.length > 0 && (
<div className="analysis-group">
<div className="analysis-group-title">
Varyings ({currentAnalysis.varyings.length})
</div>
{currentAnalysis.varyings.map((v, i) => (
<div key={i} className="analysis-item">
<span className={`item-qualifier ${v.qualifier}`}>{v.qualifier}</span>
<span className="item-type">{v.type}</span>
<span className="item-name">{v.name}</span>
</div>
))}
</div>
)}
{/* Tips */}
{currentAnalysis.complexity.tips.length > 0 && (
<div className="analysis-group">
<div className="analysis-group-title">Performance Tips</div>
{currentAnalysis.complexity.tips.map((tip, i) => (
<div key={i} className="analysis-tip">
<AlertTriangle size={12} />
<span>{tip}</span>
</div>
))}
</div>
)}
{/* Warnings */}
{currentAnalysis.warnings.length > 0 && (
<div className="analysis-group">
<div className="analysis-group-title">Warnings</div>
{currentAnalysis.warnings.map((warning, i) => (
<div key={i} className="analysis-warning">
<AlertTriangle size={12} />
<span>{warning}</span>
</div>
))}
</div>
)}
{/* Info */}
<div className="analysis-group">
<div className="analysis-group-title">Info</div>
<div className="analysis-row">
<span className="analysis-label">GLSL Version</span>
<span className="analysis-value">{currentAnalysis.version}</span>
</div>
<div className="analysis-row">
<span className="analysis-label">Precision</span>
<span className="analysis-value">{currentAnalysis.precision}</span>
</div>
</div>
</div>
)}
</div>
</div>
</div>
);
}
/**
* Shader Asset Inspector Provider.
* 着色器资产检视器提供者。
*/
export class ShaderAssetInspectorProvider implements IInspectorProvider<AssetFileTarget> {
readonly id = 'shader-asset-inspector';
readonly name = 'Shader Asset Inspector';
readonly priority = 100;
private saveHandler?: (path: string, content: string) => Promise<void>;
setSaveHandler(handler: (path: string, content: string) => Promise<void>): void {
this.saveHandler = handler;
}
canHandle(target: unknown): target is AssetFileTarget {
if (typeof target !== 'object' || target === null) return false;
const t = target as any;
return t.type === 'asset-file' &&
t.data?.extension?.toLowerCase() === 'shader' &&
typeof t.content === 'string';
}
render(target: AssetFileTarget, _context: InspectorContext): React.ReactElement {
return (
<ShaderInspectorView
fileInfo={target.data}
content={target.content!}
onSave={this.saveHandler}
/>
);
}
}