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,449 @@
/**
* Shader Analyzer.
* 着色器分析器。
*
* Parses GLSL shader code and extracts information about uniforms, attributes, varyings, and complexity.
* 解析 GLSL 着色器代码并提取 uniforms、attributes、varyings 和复杂度信息。
*/
/**
* Uniform variable info.
* Uniform 变量信息。
*/
export interface UniformInfo {
name: string;
type: string;
arraySize?: number;
description?: string;
}
/**
* Attribute variable info.
* Attribute 变量信息。
*/
export interface AttributeInfo {
name: string;
type: string;
location?: number;
}
/**
* Varying variable info.
* Varying 变量信息。
*/
export interface VaryingInfo {
name: string;
type: string;
qualifier: 'in' | 'out';
}
/**
* Shader complexity metrics.
* 着色器复杂度指标。
*/
export interface ShaderComplexity {
/** Total instruction count estimate. | 估计的总指令数。 */
instructionCount: number;
/** Texture sample count. | 纹理采样数。 */
textureSamples: number;
/** Branch count (if/else). | 分支数if/else。 */
branches: number;
/** Loop count. | 循环数。 */
loops: number;
/** Math operation count. | 数学运算数。 */
mathOps: number;
/** Complexity level. | 复杂度等级。 */
level: 'low' | 'medium' | 'high' | 'very-high';
/** Performance tips. | 性能建议。 */
tips: string[];
}
/**
* Shader analysis result.
* 着色器分析结果。
*/
export interface ShaderAnalysis {
/** GLSL version. | GLSL 版本。 */
version: string;
/** Precision. | 精度。 */
precision: string;
/** Uniforms. | 统一变量。 */
uniforms: UniformInfo[];
/** Attributes (vertex shader). | 属性(顶点着色器)。 */
attributes: AttributeInfo[];
/** Varyings (in/out). | 可变量(输入/输出)。 */
varyings: VaryingInfo[];
/** Complexity metrics. | 复杂度指标。 */
complexity: ShaderComplexity;
/** Syntax errors. | 语法错误。 */
errors: string[];
/** Warnings. | 警告。 */
warnings: string[];
}
/**
* GLSL type to size mapping.
* GLSL 类型到大小的映射。
*/
const TYPE_SIZES: Record<string, number> = {
'float': 1,
'int': 1,
'uint': 1,
'bool': 1,
'vec2': 2,
'vec3': 3,
'vec4': 4,
'ivec2': 2,
'ivec3': 3,
'ivec4': 4,
'uvec2': 2,
'uvec3': 3,
'uvec4': 4,
'bvec2': 2,
'bvec3': 3,
'bvec4': 4,
'mat2': 4,
'mat3': 9,
'mat4': 16,
'sampler2D': 1,
'samplerCube': 1,
'sampler3D': 1,
};
/**
* Shader Analyzer class.
* 着色器分析器类。
*/
export class ShaderAnalyzer {
/**
* Analyze shader source code.
* 分析着色器源代码。
*/
analyze(source: string, isVertex: boolean = false): ShaderAnalysis {
const result: ShaderAnalysis = {
version: '',
precision: '',
uniforms: [],
attributes: [],
varyings: [],
complexity: {
instructionCount: 0,
textureSamples: 0,
branches: 0,
loops: 0,
mathOps: 0,
level: 'low',
tips: []
},
errors: [],
warnings: []
};
try {
// Remove comments for analysis.
// 移除注释用于分析。
const cleanSource = this.removeComments(source);
// Parse version.
// 解析版本。
result.version = this.parseVersion(cleanSource);
// Parse precision.
// 解析精度。
result.precision = this.parsePrecision(cleanSource);
// Parse uniforms.
// 解析 uniforms。
result.uniforms = this.parseUniforms(cleanSource);
// Parse attributes (vertex shader only).
// 解析属性(仅顶点着色器)。
if (isVertex) {
result.attributes = this.parseAttributes(cleanSource);
}
// Parse varyings (in/out).
// 解析可变量(输入/输出)。
result.varyings = this.parseVaryings(cleanSource, isVertex);
// Analyze complexity.
// 分析复杂度。
result.complexity = this.analyzeComplexity(cleanSource);
// Check for common issues.
// 检查常见问题。
this.checkWarnings(cleanSource, result);
} catch (error) {
result.errors.push(`Analysis error: ${error}`);
}
return result;
}
/**
* Remove comments from source.
* 从源代码中移除注释。
*/
private removeComments(source: string): string {
// Remove single-line comments (non-greedy, stop at newline).
// 移除单行注释(非贪婪,遇到换行停止)。
let result = source.replace(/\/\/[^\n\r]*/g, '');
// Remove multi-line comments.
// 移除多行注释。
result = result.replace(/\/\*[\s\S]*?\*\//g, '');
return result;
}
/**
* Parse GLSL version.
* 解析 GLSL 版本。
*/
private parseVersion(source: string): string {
const match = source.match(/#version\s+(\d+\s*\w*)/);
return match ? match[1].trim() : 'unknown';
}
/**
* Parse precision qualifier.
* 解析精度限定符。
*/
private parsePrecision(source: string): string {
const match = source.match(/precision\s+(lowp|mediump|highp)\s+float/);
return match ? match[1] : 'not specified';
}
/**
* Parse uniform declarations.
* 解析 uniform 声明。
*/
private parseUniforms(source: string): UniformInfo[] {
const uniforms: UniformInfo[] = [];
// Match: uniform type name; or uniform type name[size];
const regex = /uniform\s+(\w+)\s+(\w+)(?:\[(\d+)\])?;/g;
let match;
while ((match = regex.exec(source)) !== null) {
const info: UniformInfo = {
name: match[2],
type: match[1]
};
if (match[3]) {
info.arraySize = parseInt(match[3], 10);
}
uniforms.push(info);
}
return uniforms;
}
/**
* Parse attribute declarations.
* 解析 attribute 声明。
*/
private parseAttributes(source: string): AttributeInfo[] {
const attributes: AttributeInfo[] = [];
// GLSL 300 es style: layout(location = n) in type name;
const layoutRegex = /layout\s*\(\s*location\s*=\s*(\d+)\s*\)\s*in\s+(\w+)\s+(\w+);/g;
let match: RegExpExecArray | null;
while ((match = layoutRegex.exec(source)) !== null) {
attributes.push({
name: match[3],
type: match[2],
location: parseInt(match[1], 10)
});
}
// Old style: attribute type name; or in type name;
const attrRegex = /(?:attribute|in)\s+(\w+)\s+(\w+);/g;
let attrMatch: RegExpExecArray | null;
while ((attrMatch = attrRegex.exec(source)) !== null) {
// Skip if already added via layout.
if (!attributes.find(a => a.name === attrMatch![2])) {
attributes.push({
name: attrMatch[2],
type: attrMatch[1]
});
}
}
return attributes;
}
/**
* Parse varying declarations (in/out).
* 解析可变量声明(输入/输出)。
*/
private parseVaryings(source: string, isVertex: boolean): VaryingInfo[] {
const varyings: VaryingInfo[] = [];
// Parse 'out' declarations.
// 解析 'out' 声明。
const outRegex = /(?<!layout\s*\([^)]*\)\s*)out\s+(\w+)\s+(\w+);/g;
let match;
while ((match = outRegex.exec(source)) !== null) {
varyings.push({
name: match[2],
type: match[1],
qualifier: 'out'
});
}
// Parse 'in' declarations (skip attributes in vertex shader).
// 解析 'in' 声明(跳过顶点着色器中的属性)。
if (!isVertex) {
const inRegex = /(?<!layout\s*\([^)]*\)\s*)in\s+(\w+)\s+(\w+);/g;
while ((match = inRegex.exec(source)) !== null) {
varyings.push({
name: match[2],
type: match[1],
qualifier: 'in'
});
}
}
return varyings;
}
/**
* Analyze shader complexity.
* 分析着色器复杂度。
*/
private analyzeComplexity(source: string): ShaderComplexity {
const complexity: ShaderComplexity = {
instructionCount: 0,
textureSamples: 0,
branches: 0,
loops: 0,
mathOps: 0,
level: 'low',
tips: []
};
// Count texture samples.
// 统计纹理采样。
const textureCalls = source.match(/texture\s*\(/g) || [];
complexity.textureSamples = textureCalls.length;
// Count branches.
// 统计分支。
const ifStatements = source.match(/\bif\s*\(/g) || [];
const ternary = source.match(/\?/g) || [];
complexity.branches = ifStatements.length + ternary.length;
// Count loops.
// 统计循环。
const forLoops = source.match(/\bfor\s*\(/g) || [];
const whileLoops = source.match(/\bwhile\s*\(/g) || [];
complexity.loops = forLoops.length + whileLoops.length;
// Count math operations.
// 统计数学运算。
const mathFuncs = [
'sin', 'cos', 'tan', 'asin', 'acos', 'atan',
'pow', 'exp', 'log', 'sqrt', 'inversesqrt',
'abs', 'floor', 'ceil', 'fract', 'mod',
'min', 'max', 'clamp', 'mix', 'step', 'smoothstep',
'length', 'distance', 'dot', 'cross', 'normalize', 'reflect', 'refract'
];
for (const func of mathFuncs) {
const matches = source.match(new RegExp(`\\b${func}\\s*\\(`, 'g')) || [];
complexity.mathOps += matches.length;
}
// Estimate instruction count.
// 估计指令数。
const lines = source.split('\n').filter(l => l.trim() && !l.trim().startsWith('//'));
complexity.instructionCount = lines.length +
complexity.textureSamples * 4 +
complexity.mathOps * 2 +
complexity.branches * 2;
// Determine complexity level.
// 确定复杂度等级。
if (complexity.instructionCount > 200 || complexity.textureSamples > 8 || complexity.loops > 3) {
complexity.level = 'very-high';
} else if (complexity.instructionCount > 100 || complexity.textureSamples > 4 || complexity.loops > 1) {
complexity.level = 'high';
} else if (complexity.instructionCount > 50 || complexity.textureSamples > 2) {
complexity.level = 'medium';
} else {
complexity.level = 'low';
}
// Generate tips.
// 生成建议。
if (complexity.textureSamples > 4) {
complexity.tips.push('Consider reducing texture samples for better performance.');
}
if (complexity.loops > 2) {
complexity.tips.push('Nested loops can significantly impact performance.');
}
if (complexity.branches > 5) {
complexity.tips.push('Many branches can cause performance issues on some GPUs.');
}
if (source.includes('discard')) {
complexity.tips.push('Using discard can prevent early-z optimization.');
}
return complexity;
}
/**
* Check for common issues and warnings.
* 检查常见问题和警告。
*/
private checkWarnings(source: string, result: ShaderAnalysis): void {
// Check for missing precision.
// 检查缺少精度。
if (result.precision === 'not specified') {
result.warnings.push('No precision qualifier specified. Consider adding "precision highp float;"');
}
// Check for unused uniforms.
// 检查未使用的 uniforms。
for (const uniform of result.uniforms) {
const usageRegex = new RegExp(`\\b${uniform.name}\\b`, 'g');
const matches = source.match(usageRegex) || [];
if (matches.length <= 1) { // Only the declaration.
result.warnings.push(`Uniform "${uniform.name}" may be unused.`);
}
}
// Check for expensive operations in loops.
// 检查循环中的昂贵操作。
if (result.complexity.loops > 0 && result.complexity.textureSamples > 0) {
// Simple heuristic: if we have loops and texture samples, warn.
result.warnings.push('Texture sampling in loops can be expensive.');
}
}
/**
* Get type size in floats.
* 获取类型大小(以 float 为单位)。
*/
getTypeSize(type: string): number {
return TYPE_SIZES[type] || 1;
}
/**
* Calculate total uniform buffer size.
* 计算 uniform 缓冲区总大小。
*/
calculateUniformBufferSize(uniforms: UniformInfo[]): number {
let size = 0;
for (const uniform of uniforms) {
const typeSize = this.getTypeSize(uniform.type);
const count = uniform.arraySize || 1;
size += typeSize * count;
}
return size;
}
}
// Export singleton instance.
// 导出单例实例。
export const shaderAnalyzer = new ShaderAnalyzer();

View File

@@ -0,0 +1,396 @@
/**
* Shader Editor Panel Styles.
* 着色器编辑器面板样式。
*/
.shader-editor-panel {
display: flex;
flex-direction: column;
height: 100%;
background: var(--bg-primary, #1e1e1e);
color: var(--text-primary, #e0e0e0);
font-size: 12px;
}
.shader-editor-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: var(--text-secondary, #888);
gap: 12px;
}
.shader-editor-hint {
font-size: 11px;
color: var(--text-tertiary, #666);
}
/* Header */
.shader-editor-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
border-bottom: 1px solid var(--border-color, #333);
background: var(--bg-secondary, #252526);
}
.shader-editor-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
}
.shader-editor-dirty {
color: var(--warning-color, #fbbf24);
}
.shader-editor-actions {
display: flex;
gap: 8px;
}
.shader-editor-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border: 1px solid var(--border-color, #444);
border-radius: 4px;
background: var(--button-bg, #333);
color: var(--text-primary, #e0e0e0);
font-size: 11px;
cursor: pointer;
transition: all 0.15s ease;
}
.shader-editor-btn:hover:not(:disabled) {
background: var(--button-hover-bg, #444);
border-color: var(--accent-color, #0078d4);
}
.shader-editor-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Status bar */
.shader-editor-status {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
font-size: 11px;
}
.shader-editor-status.error {
background: var(--error-bg, #3a2020);
color: var(--error-color, #f87171);
}
.shader-editor-status.success {
background: var(--success-bg, #1a3a1a);
color: var(--success-color, #4ade80);
}
/* Main content */
.shader-editor-content {
display: flex;
flex: 1;
overflow: hidden;
}
/* Code section */
.shader-editor-code-section {
flex: 1;
display: flex;
flex-direction: column;
border-right: 1px solid var(--border-color, #333);
min-width: 0;
}
/* Tabs */
.shader-editor-tabs {
display: flex;
border-bottom: 1px solid var(--border-color, #333);
background: var(--bg-secondary, #252526);
}
.shader-editor-tab {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 16px;
border: none;
border-bottom: 2px solid transparent;
background: transparent;
color: var(--text-secondary, #888);
font-size: 11px;
cursor: pointer;
transition: all 0.15s ease;
}
.shader-editor-tab:hover {
color: var(--text-primary, #e0e0e0);
}
.shader-editor-tab.active {
color: var(--accent-color, #0078d4);
border-bottom-color: var(--accent-color, #0078d4);
}
/* Textarea wrapper */
.shader-editor-textarea-wrapper {
flex: 1;
display: flex;
position: relative;
overflow: hidden;
}
.shader-editor-line-numbers {
display: flex;
flex-direction: column;
padding: 12px 8px;
background: var(--bg-tertiary, #1a1a1a);
color: var(--text-tertiary, #666);
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
line-height: 1.5;
text-align: right;
user-select: none;
border-right: 1px solid var(--border-color, #333);
overflow-y: auto;
}
.shader-editor-line-numbers span {
min-width: 24px;
}
.shader-editor-textarea {
flex: 1;
padding: 12px;
border: none;
background: var(--bg-primary, #1e1e1e);
color: var(--text-primary, #e0e0e0);
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
line-height: 1.5;
resize: none;
outline: none;
tab-size: 4;
white-space: pre;
overflow: auto;
}
.shader-editor-textarea::placeholder {
color: var(--text-tertiary, #555);
}
/* Analysis section */
.shader-editor-analysis-section {
width: 280px;
display: flex;
flex-direction: column;
background: var(--bg-secondary, #252526);
overflow: hidden;
}
.shader-editor-section-header {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border-bottom: 1px solid var(--border-color, #333);
cursor: pointer;
user-select: none;
font-weight: 500;
}
.shader-editor-section-header:hover {
background: var(--bg-hover, #2a2a2a);
}
.shader-editor-analysis-content {
flex: 1;
overflow-y: auto;
padding: 8px;
}
/* Analysis sections */
.analysis-section {
margin-bottom: 12px;
padding: 8px;
border-radius: 4px;
background: var(--bg-tertiary, #1e1e1e);
}
.analysis-section-title {
font-size: 11px;
font-weight: 500;
color: var(--text-secondary, #888);
margin-bottom: 8px;
text-transform: uppercase;
}
/* Complexity badge */
.complexity-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 10px;
font-weight: 600;
margin-bottom: 8px;
}
.complexity-badge.low {
background: var(--success-bg, #1a3a1a);
color: var(--success-color, #4ade80);
}
.complexity-badge.medium {
background: var(--info-bg, #1a2a3a);
color: var(--info-color, #60a5fa);
}
.complexity-badge.high {
background: var(--warning-bg, #3a3a1a);
color: var(--warning-color, #fbbf24);
}
.complexity-badge.very-high {
background: var(--error-bg, #3a2020);
color: var(--error-color, #f87171);
}
/* Metrics */
.analysis-metrics {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
}
.metric {
display: flex;
justify-content: space-between;
padding: 4px 0;
font-size: 11px;
}
.metric-label {
color: var(--text-secondary, #888);
}
.metric-value {
color: var(--text-primary, #e0e0e0);
font-weight: 500;
}
/* Analysis list */
.analysis-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.analysis-item {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 6px;
border-radius: 3px;
background: var(--bg-primary, #1e1e1e);
font-size: 11px;
font-family: 'Consolas', 'Monaco', monospace;
}
.item-type {
color: var(--accent-color, #0078d4);
}
.item-name {
color: var(--text-primary, #e0e0e0);
}
.item-array {
color: var(--text-tertiary, #666);
}
.item-location {
color: var(--text-tertiary, #666);
font-size: 10px;
}
.item-qualifier {
padding: 1px 4px;
border-radius: 2px;
font-size: 9px;
font-weight: 500;
}
.item-qualifier.in {
background: var(--success-bg, #1a3a1a);
color: var(--success-color, #4ade80);
}
.item-qualifier.out {
background: var(--info-bg, #1a2a3a);
color: var(--info-color, #60a5fa);
}
/* Tips and warnings */
.analysis-tips,
.analysis-warnings {
display: flex;
flex-direction: column;
gap: 6px;
}
.analysis-tip,
.analysis-warning {
display: flex;
align-items: flex-start;
gap: 6px;
padding: 6px 8px;
border-radius: 4px;
font-size: 11px;
line-height: 1.4;
}
.analysis-tip {
background: var(--warning-bg, #3a3a1a);
color: var(--warning-color, #fbbf24);
}
.analysis-warning {
background: var(--warning-bg, #3a3a1a);
color: var(--warning-color, #fbbf24);
}
.analysis-tip svg,
.analysis-warning svg {
flex-shrink: 0;
margin-top: 2px;
}
/* Info */
.analysis-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.info-item {
display: flex;
justify-content: space-between;
font-size: 11px;
}
.info-label {
color: var(--text-secondary, #888);
}
.info-value {
color: var(--text-primary, #e0e0e0);
}

View File

@@ -0,0 +1,430 @@
/**
* Shader Editor Panel.
* 着色器编辑器面板。
*
* Provides shader code editing, analysis, and preview.
* 提供着色器代码编辑、分析和预览功能。
*/
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { Core } from '@esengine/ecs-framework';
import { MessageHub, IFileSystemService, IFileSystem, ProjectService } from '@esengine/editor-core';
import { getMaterialManager, Shader } from '@esengine/material-system';
import {
Save, RefreshCw, Play, AlertTriangle, CheckCircle,
Code, Eye, BarChart3, FileCode, ChevronDown, ChevronRight
} from 'lucide-react';
import { ShaderAnalyzer, ShaderAnalysis } from '../analysis/ShaderAnalyzer';
import { useShaderEditorStore } from '../stores/ShaderEditorStore';
import './ShaderEditorPanel.css';
/**
* Shader Editor Panel Props.
* 着色器编辑器面板属性。
*/
interface ShaderEditorPanelProps {
filePath?: string;
}
/**
* Shader Editor Panel Component.
* 着色器编辑器面板组件。
*/
export function ShaderEditorPanel({ filePath: propFilePath }: ShaderEditorPanelProps) {
const {
filePath, shaderData, isDirty,
setFilePath, setShaderData, setDirty, reset
} = useShaderEditorStore();
const [activeTab, setActiveTab] = useState<'vertex' | 'fragment'>('fragment');
const [showAnalysis, setShowAnalysis] = useState(true);
const [_showPreview, _setShowPreview] = useState(true);
const [vertexAnalysis, setVertexAnalysis] = useState<ShaderAnalysis | null>(null);
const [fragmentAnalysis, setFragmentAnalysis] = useState<ShaderAnalysis | null>(null);
const [compileError, setCompileError] = useState<string | null>(null);
const [compileSuccess, setCompileSuccess] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const analyzer = useRef(new ShaderAnalyzer());
// Load shader file.
// 加载着色器文件。
useEffect(() => {
const pathToLoad = propFilePath || filePath;
if (pathToLoad) {
loadShaderFile(pathToLoad);
}
}, [propFilePath]);
// Subscribe to file open messages.
// 订阅文件打开消息。
useEffect(() => {
const messageHub = Core.services.tryResolve(MessageHub);
if (!messageHub) return;
const unsubscribe = messageHub.subscribe('shader:open', (payload: { filePath: string }) => {
loadShaderFile(payload.filePath);
});
return () => unsubscribe();
}, []);
// Analyze shader when source changes.
// 当源代码改变时分析着色器。
useEffect(() => {
if (shaderData) {
setVertexAnalysis(analyzer.current.analyze(shaderData.vertex, true));
setFragmentAnalysis(analyzer.current.analyze(shaderData.fragment, false));
}
}, [shaderData?.vertex, shaderData?.fragment]);
const loadShaderFile = async (path: string) => {
const fileSystem = Core.services.tryResolve<IFileSystem>(IFileSystemService);
if (!fileSystem) return;
try {
const content = await fileSystem.readFile(path);
const data = JSON.parse(content);
setFilePath(path);
setShaderData({
version: data.version || 1,
name: data.shader.name || 'Untitled',
vertex: data.shader.vertexSource || '',
fragment: data.shader.fragmentSource || ''
});
setDirty(false);
setCompileError(null);
setCompileSuccess(false);
} catch (error) {
console.error('[ShaderEditorPanel] Failed to load shader:', error);
}
};
const handleSave = async () => {
if (!filePath || !shaderData) return;
const fileSystem = Core.services.tryResolve<IFileSystem>(IFileSystemService);
if (!fileSystem) return;
try {
// Save in new wrapper format.
// 以新的包装格式保存。
const fileData = {
version: shaderData.version || 1,
shader: {
name: shaderData.name,
vertexSource: shaderData.vertex,
fragmentSource: shaderData.fragment
}
};
const content = JSON.stringify(fileData, null, 2);
await fileSystem.writeFile(filePath, content);
setDirty(false);
// Notify.
const messageHub = Core.services.tryResolve(MessageHub);
if (messageHub) {
messageHub.publish('shader:saved', { filePath });
}
} catch (error) {
console.error('[ShaderEditorPanel] Failed to save shader:', error);
}
};
const handleCompile = async () => {
if (!shaderData) return;
setCompileError(null);
setCompileSuccess(false);
try {
const materialManager = getMaterialManager();
if (!materialManager) {
setCompileError('MaterialManager not available');
return;
}
// Create a temporary shader to test compilation.
// 创建临时着色器测试编译。
const testShader = new Shader(
`test_${Date.now()}`,
shaderData.vertex,
shaderData.fragment
);
// Try to register (which compiles).
// 尝试注册(会进行编译)。
const shaderId = await materialManager.registerShader(testShader);
if (shaderId > 0) {
setCompileSuccess(true);
// Remove test shader.
materialManager.removeShader(shaderId);
} else {
setCompileError('Compilation failed');
}
} catch (error: any) {
setCompileError(error.message || 'Compilation failed');
}
};
const handleSourceChange = (type: 'vertex' | 'fragment', value: string) => {
if (!shaderData) return;
setShaderData({
...shaderData,
[type]: value
});
setDirty(true);
setCompileSuccess(false);
};
const currentAnalysis = activeTab === 'vertex' ? vertexAnalysis : fragmentAnalysis;
if (!shaderData) {
return (
<div className="shader-editor-panel shader-editor-empty">
<FileCode size={48} />
<p>No shader loaded</p>
<p className="shader-editor-hint">Open a .shader file to edit</p>
</div>
);
}
return (
<div className="shader-editor-panel">
{/* Header */}
<div className="shader-editor-header">
<div className="shader-editor-title">
<FileCode size={16} />
<span>{shaderData.name}</span>
{isDirty && <span className="shader-editor-dirty">*</span>}
</div>
<div className="shader-editor-actions">
<button
className="shader-editor-btn"
onClick={handleCompile}
title="Compile shader"
>
<Play size={14} />
Compile
</button>
<button
className="shader-editor-btn"
onClick={handleSave}
disabled={!isDirty}
title="Save shader"
>
<Save size={14} />
Save
</button>
</div>
</div>
{/* Compile status */}
{compileError && (
<div className="shader-editor-status error">
<AlertTriangle size={14} />
<span>{compileError}</span>
</div>
)}
{compileSuccess && (
<div className="shader-editor-status success">
<CheckCircle size={14} />
<span>Compilation successful!</span>
</div>
)}
{/* Main content */}
<div className="shader-editor-content">
{/* Code editor */}
<div className="shader-editor-code-section">
{/* Tabs */}
<div className="shader-editor-tabs">
<button
className={`shader-editor-tab ${activeTab === 'vertex' ? 'active' : ''}`}
onClick={() => setActiveTab('vertex')}
>
<Code size={12} />
Vertex
</button>
<button
className={`shader-editor-tab ${activeTab === 'fragment' ? 'active' : ''}`}
onClick={() => setActiveTab('fragment')}
>
<Code size={12} />
Fragment
</button>
</div>
{/* Editor */}
<div className="shader-editor-textarea-wrapper">
<textarea
ref={textareaRef}
className="shader-editor-textarea"
value={activeTab === 'vertex' ? shaderData.vertex : shaderData.fragment}
onChange={e => handleSourceChange(activeTab, e.target.value)}
spellCheck={false}
placeholder={`Enter ${activeTab} shader code...`}
/>
<div className="shader-editor-line-numbers">
{(activeTab === 'vertex' ? shaderData.vertex : shaderData.fragment)
.split('\n')
.map((_, i) => (
<span key={i}>{i + 1}</span>
))}
</div>
</div>
</div>
{/* Analysis panel */}
<div className="shader-editor-analysis-section">
{/* Analysis header */}
<div
className="shader-editor-section-header"
onClick={() => setShowAnalysis(!showAnalysis)}
>
{showAnalysis ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
<BarChart3 size={14} />
<span>Analysis</span>
</div>
{showAnalysis && currentAnalysis && (
<div className="shader-editor-analysis-content">
{/* Complexity */}
<div className="analysis-section">
<div className="analysis-section-title">Complexity</div>
<div className={`complexity-badge ${currentAnalysis.complexity.level}`}>
{currentAnalysis.complexity.level.toUpperCase()}
</div>
<div className="analysis-metrics">
<div className="metric">
<span className="metric-label">Instructions</span>
<span className="metric-value">~{currentAnalysis.complexity.instructionCount}</span>
</div>
<div className="metric">
<span className="metric-label">Texture Samples</span>
<span className="metric-value">{currentAnalysis.complexity.textureSamples}</span>
</div>
<div className="metric">
<span className="metric-label">Branches</span>
<span className="metric-value">{currentAnalysis.complexity.branches}</span>
</div>
<div className="metric">
<span className="metric-label">Loops</span>
<span className="metric-value">{currentAnalysis.complexity.loops}</span>
</div>
<div className="metric">
<span className="metric-label">Math Ops</span>
<span className="metric-value">{currentAnalysis.complexity.mathOps}</span>
</div>
</div>
</div>
{/* Uniforms */}
{currentAnalysis.uniforms.length > 0 && (
<div className="analysis-section">
<div className="analysis-section-title">
Uniforms ({currentAnalysis.uniforms.length})
</div>
<div className="analysis-list">
{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>
</div>
)}
{/* Attributes (vertex only) */}
{activeTab === 'vertex' && currentAnalysis.attributes.length > 0 && (
<div className="analysis-section">
<div className="analysis-section-title">
Attributes ({currentAnalysis.attributes.length})
</div>
<div className="analysis-list">
{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>
</div>
)}
{/* Varyings */}
{currentAnalysis.varyings.length > 0 && (
<div className="analysis-section">
<div className="analysis-section-title">
Varyings ({currentAnalysis.varyings.length})
</div>
<div className="analysis-list">
{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>
</div>
)}
{/* Tips */}
{currentAnalysis.complexity.tips.length > 0 && (
<div className="analysis-section">
<div className="analysis-section-title">Performance Tips</div>
<div className="analysis-tips">
{currentAnalysis.complexity.tips.map((tip, i) => (
<div key={i} className="analysis-tip">
<AlertTriangle size={12} />
<span>{tip}</span>
</div>
))}
</div>
</div>
)}
{/* Warnings */}
{currentAnalysis.warnings.length > 0 && (
<div className="analysis-section">
<div className="analysis-section-title">Warnings</div>
<div className="analysis-warnings">
{currentAnalysis.warnings.map((warning, i) => (
<div key={i} className="analysis-warning">
<AlertTriangle size={12} />
<span>{warning}</span>
</div>
))}
</div>
</div>
)}
{/* Info */}
<div className="analysis-section">
<div className="analysis-section-title">Info</div>
<div className="analysis-info">
<div className="info-item">
<span className="info-label">GLSL Version</span>
<span className="info-value">{currentAnalysis.version}</span>
</div>
<div className="info-item">
<span className="info-label">Precision</span>
<span className="info-value">{currentAnalysis.precision}</span>
</div>
</div>
</div>
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,110 @@
/**
* @esengine/shader-editor
*
* Shader editor with code editing, analysis, and preview.
* 着色器编辑器,支持代码编辑、分析和预览。
*/
import type { ServiceContainer } from '@esengine/ecs-framework';
import type {
IEditorModuleLoader,
IPlugin,
ModuleManifest,
IFileSystem
} from '@esengine/editor-core';
import {
InspectorRegistry,
IInspectorRegistry,
IFileSystemService
} from '@esengine/editor-core';
// Components
import { useShaderEditorStore } from './stores/ShaderEditorStore';
import { ShaderAssetInspectorProvider } from './providers/ShaderAssetInspectorProvider';
// Import styles
import './styles/ShaderInspector.css';
// Re-exports
export { useShaderEditorStore, createDefaultShaderData } from './stores/ShaderEditorStore';
export type { ShaderData, ShaderEditorState } from './stores/ShaderEditorStore';
export { ShaderAnalyzer, shaderAnalyzer } from './analysis/ShaderAnalyzer';
export type {
ShaderAnalysis,
ShaderComplexity,
UniformInfo,
AttributeInfo,
VaryingInfo
} from './analysis/ShaderAnalyzer';
export { ShaderAssetInspectorProvider } from './providers/ShaderAssetInspectorProvider';
/**
* Shader Editor Module.
* 着色器编辑器模块。
*/
export class ShaderEditorModule implements IEditorModuleLoader {
private unsubscribers: Array<() => void> = [];
private inspectorProvider?: ShaderAssetInspectorProvider;
async install(services: ServiceContainer): Promise<void> {
// Register Shader Asset Inspector Provider.
// 注册着色器资产检视器提供者。
const inspectorRegistry = services.resolve<InspectorRegistry>(IInspectorRegistry);
if (inspectorRegistry) {
this.inspectorProvider = new ShaderAssetInspectorProvider();
// Set up save handler.
// 设置保存处理器。
const fileSystem = services.tryResolve<IFileSystem>(IFileSystemService);
if (fileSystem) {
this.inspectorProvider.setSaveHandler(async (path, content) => {
await fileSystem.writeFile(path, content);
});
}
inspectorRegistry.register(this.inspectorProvider);
}
}
async uninstall(): Promise<void> {
// Clean up subscriptions.
this.unsubscribers.forEach(unsub => unsub());
this.unsubscribers = [];
// Reset store.
useShaderEditorStore.getState().reset();
}
}
export const shaderEditorModule = new ShaderEditorModule();
/**
* Shader Editor Plugin Manifest.
* 着色器编辑器插件清单。
*/
const manifest: ModuleManifest = {
id: '@esengine/shader-editor',
name: '@esengine/shader-editor',
displayName: 'Shader Editor',
version: '1.0.0',
description: 'Shader code editing with analysis and preview',
category: 'Rendering',
isCore: true,
defaultEnabled: true,
isEngineModule: true,
dependencies: ['material-system'],
exports: {
other: ['ShaderAnalyzer']
}
};
/**
* Shader Editor Plugin (editor only, no runtime).
* 着色器编辑器插件(仅编辑器,无运行时)。
*/
export const ShaderEditorPlugin: IPlugin = {
manifest,
editorModule: shaderEditorModule
};
export default shaderEditorModule;

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}
/>
);
}
}

View File

@@ -0,0 +1,105 @@
/**
* Shader Editor Store.
* 着色器编辑器状态存储。
*/
import { create } from 'zustand';
/**
* Shader data structure.
* 着色器数据结构。
*/
export interface ShaderData {
version: string;
name: string;
vertex: string;
fragment: string;
}
/**
* Shader editor state.
* 着色器编辑器状态。
*/
export interface ShaderEditorState {
/** Current file path. | 当前文件路径。 */
filePath: string | null;
/** Shader data. | 着色器数据。 */
shaderData: ShaderData | null;
/** Whether data has been modified. | 数据是否已修改。 */
isDirty: boolean;
/** Set file path. | 设置文件路径。 */
setFilePath: (path: string | null) => void;
/** Set shader data. | 设置着色器数据。 */
setShaderData: (data: ShaderData | null) => void;
/** Set dirty flag. | 设置修改标志。 */
setDirty: (dirty: boolean) => void;
/** Reset state. | 重置状态。 */
reset: () => void;
}
/**
* Create default shader data.
* 创建默认着色器数据。
*/
export function createDefaultShaderData(name: string = 'New Shader'): ShaderData {
return {
version: '1.0',
name,
vertex: `#version 300 es
precision highp float;
layout(location = 0) in vec2 a_position;
layout(location = 1) in vec2 a_texCoord;
layout(location = 2) in vec4 a_color;
uniform mat3 u_projection;
out vec2 v_texCoord;
out vec4 v_color;
void main() {
vec3 pos = u_projection * vec3(a_position, 1.0);
gl_Position = vec4(pos.xy, 0.0, 1.0);
v_texCoord = a_texCoord;
v_color = a_color;
}`,
fragment: `#version 300 es
precision highp float;
in vec2 v_texCoord;
in vec4 v_color;
uniform sampler2D u_texture;
out vec4 fragColor;
void main() {
vec4 texColor = texture(u_texture, v_texCoord);
fragColor = texColor * v_color;
if (fragColor.a < 0.01) {
discard;
}
}`
};
}
/**
* Shader editor store.
* 着色器编辑器存储。
*/
export const useShaderEditorStore = create<ShaderEditorState>((set) => ({
filePath: null,
shaderData: null,
isDirty: false,
setFilePath: (path) => set({ filePath: path }),
setShaderData: (data) => set({ shaderData: data }),
setDirty: (dirty) => set({ isDirty: dirty }),
reset: () => set({
filePath: null,
shaderData: null,
isDirty: false
})
}));

View File

@@ -0,0 +1,286 @@
/**
* Shader Inspector Styles.
* 着色器检视器样式。
*/
.shader-inspector {
font-size: 12px;
}
.shader-dirty-indicator {
color: var(--warning-color, #fbbf24);
margin-left: 4px;
}
.shader-error,
.shader-loading {
padding: 16px;
text-align: center;
color: var(--text-secondary, #888);
}
.shader-error {
color: var(--error-color, #f87171);
}
/* Toolbar */
.shader-toolbar {
display: flex;
gap: 8px;
padding: 8px 12px;
border-bottom: 1px solid var(--border-color, #333);
background: var(--bg-secondary, #252526);
}
.shader-toolbar-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border: 1px solid var(--border-color, #444);
border-radius: 4px;
background: var(--button-bg, #333);
color: var(--text-primary, #e0e0e0);
font-size: 11px;
cursor: pointer;
transition: all 0.15s ease;
}
.shader-toolbar-btn:hover:not(:disabled) {
background: var(--button-hover-bg, #444);
border-color: var(--accent-color, #0078d4);
}
.shader-toolbar-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Status */
.shader-status {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
font-size: 11px;
}
.shader-status.success {
background: var(--success-bg, #1a3a1a);
color: var(--success-color, #4ade80);
}
.shader-status.error {
background: var(--error-bg, #3a2020);
color: var(--error-color, #f87171);
}
/* Input */
.shader-input {
width: 100%;
padding: 4px 8px;
border: 1px solid var(--border-color, #333);
border-radius: 3px;
background: var(--input-bg, #333);
color: var(--text-primary, #e0e0e0);
font-size: 11px;
}
.shader-input:focus {
outline: none;
border-color: var(--accent-color, #0078d4);
}
/* Tabs */
.shader-tabs {
display: flex;
border-bottom: 1px solid var(--border-color, #333);
margin-bottom: 8px;
}
.shader-tab {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
border: none;
border-bottom: 2px solid transparent;
background: transparent;
color: var(--text-secondary, #888);
font-size: 11px;
cursor: pointer;
transition: all 0.15s ease;
}
.shader-tab:hover {
color: var(--text-primary, #e0e0e0);
}
.shader-tab.active {
color: var(--accent-color, #0078d4);
border-bottom-color: var(--accent-color, #0078d4);
}
/* Code Editor */
.shader-code-wrapper {
position: relative;
border: 1px solid var(--border-color, #333);
border-radius: 4px;
overflow: hidden;
}
.shader-code-editor {
width: 100%;
min-height: 200px;
max-height: 400px;
padding: 8px;
border: none;
background: var(--bg-tertiary, #1a1a1a);
color: var(--text-primary, #e0e0e0);
font-family: 'Consolas', 'Monaco', monospace;
font-size: 11px;
line-height: 1.5;
resize: vertical;
outline: none;
tab-size: 4;
white-space: pre;
}
.shader-code-editor::placeholder {
color: var(--text-tertiary, #555);
}
/* Analysis */
.shader-analysis {
padding: 8px 0;
}
.analysis-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 0;
}
.analysis-label {
color: var(--text-secondary, #888);
font-size: 11px;
}
.analysis-value {
color: var(--text-primary, #e0e0e0);
font-size: 11px;
font-weight: 500;
}
/* Complexity Badge */
.complexity-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 10px;
font-size: 10px;
font-weight: 600;
}
.complexity-badge.low {
background: var(--success-bg, #1a3a1a);
color: var(--success-color, #4ade80);
}
.complexity-badge.medium {
background: var(--info-bg, #1a2a3a);
color: var(--info-color, #60a5fa);
}
.complexity-badge.high {
background: var(--warning-bg, #3a3a1a);
color: var(--warning-color, #fbbf24);
}
.complexity-badge.very-high {
background: var(--error-bg, #3a2020);
color: var(--error-color, #f87171);
}
/* Analysis Groups */
.analysis-group {
margin-top: 12px;
padding-top: 8px;
border-top: 1px solid var(--border-color, #333);
}
.analysis-group-title {
font-size: 10px;
font-weight: 500;
color: var(--text-secondary, #888);
text-transform: uppercase;
margin-bottom: 6px;
}
/* Analysis Items */
.analysis-item {
display: flex;
align-items: center;
gap: 6px;
padding: 3px 6px;
margin-bottom: 2px;
border-radius: 3px;
background: var(--bg-tertiary, #1e1e1e);
font-size: 10px;
font-family: 'Consolas', 'Monaco', monospace;
}
.item-type {
color: var(--accent-color, #0078d4);
}
.item-name {
color: var(--text-primary, #e0e0e0);
}
.item-array {
color: var(--text-tertiary, #666);
}
.item-location {
color: var(--text-tertiary, #666);
font-size: 9px;
}
.item-qualifier {
padding: 1px 4px;
border-radius: 2px;
font-size: 9px;
font-weight: 500;
}
.item-qualifier.in {
background: var(--success-bg, #1a3a1a);
color: var(--success-color, #4ade80);
}
.item-qualifier.out {
background: var(--info-bg, #1a2a3a);
color: var(--info-color, #60a5fa);
}
/* Tips and Warnings */
.analysis-tip,
.analysis-warning {
display: flex;
align-items: flex-start;
gap: 6px;
padding: 6px 8px;
margin-bottom: 4px;
border-radius: 4px;
font-size: 10px;
line-height: 1.4;
background: var(--warning-bg, #3a3a1a);
color: var(--warning-color, #fbbf24);
}
.analysis-tip svg,
.analysis-warning svg {
flex-shrink: 0;
margin-top: 1px;
}