refactor: reorganize package structure and decouple framework packages (#338)
* refactor: reorganize package structure and decouple framework packages ## Package Structure Reorganization - Reorganized 55 packages into categorized subdirectories: - packages/framework/ - Generic framework (Laya/Cocos compatible) - packages/engine/ - ESEngine core modules - packages/rendering/ - Rendering modules (WASM dependent) - packages/physics/ - Physics modules - packages/streaming/ - World streaming - packages/network-ext/ - Network extensions - packages/editor/ - Editor framework and plugins - packages/rust/ - Rust WASM engine - packages/tools/ - Build tools and SDK ## Framework Package Decoupling - Decoupled behavior-tree and blueprint packages from ESEngine dependencies - Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent) - ESEngine-specific code moved to esengine/ subpath exports - Framework packages now usable with Cocos/Laya without ESEngine ## CI Configuration - Updated CI to only type-check and lint framework packages - Added type-check:framework and lint:framework scripts ## Breaking Changes - Package import paths changed due to directory reorganization - ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine') * fix: update es-engine file path after directory reorganization * docs: update README to focus on framework over engine * ci: only build framework packages, remove Rust/WASM dependencies * fix: remove esengine subpath from behavior-tree and blueprint builds ESEngine integration code will only be available in full engine builds. Framework packages are now purely engine-agnostic. * fix: move network-protocols to framework, build both in CI * fix: update workflow paths from packages/core to packages/framework/core * fix: exclude esengine folder from type-check in behavior-tree and blueprint * fix: update network tsconfig references to new paths * fix: add test:ci:framework to only test framework packages in CI * fix: only build core and math npm packages in CI * fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user