feat(editor-app): 添加渲染调试面板

- 新增 RenderDebugService 和调试面板 UI
- App/ContentBrowser 添加调试日志
- TitleBar/Viewport 优化
- DialogManager 改进
This commit is contained in:
yhh
2025-12-16 11:28:34 +08:00
parent 792fd05c85
commit d64e463a71
9 changed files with 2684 additions and 10 deletions

View File

@@ -40,11 +40,15 @@ import { Inspector } from './components/inspectors/Inspector';
import { AssetBrowser } from './components/AssetBrowser';
import { Viewport } from './components/Viewport';
import { AdvancedProfilerWindow } from './components/AdvancedProfilerWindow';
import { RenderDebugPanel } from './components/debug/RenderDebugPanel';
import { emit, emitTo, listen } from '@tauri-apps/api/event';
import { renderDebugService } from './services/RenderDebugService';
import { PortManager } from './components/PortManager';
import { SettingsWindow } from './components/SettingsWindow';
import { AboutDialog } from './components/AboutDialog';
import { ErrorDialog } from './components/ErrorDialog';
import { ConfirmDialog } from './components/ConfirmDialog';
import { ExternalModificationDialog } from './components/ExternalModificationDialog';
import { PluginGeneratorWindow } from './components/PluginGeneratorWindow';
import { BuildSettingsWindow } from './components/BuildSettingsWindow';
import { ForumPanel } from './components/forum';
@@ -63,6 +67,7 @@ import { useLocale } from './hooks/useLocale';
import { useStoreSubscriptions } from './hooks/useStoreSubscriptions';
import { en, zh, es } from './locales';
import type { Locale } from '@esengine/editor-core';
import { UserCodeService } from '@esengine/editor-core';
import { Loader2 } from 'lucide-react';
import './styles/App.css';
@@ -84,12 +89,24 @@ Core.services.registerInstance(ICompilerRegistry, compilerRegistryInstance);
const logger = createLogger('App');
// 检查是否为独立窗口模式 | Check if standalone window mode
const isFrameDebuggerMode = new URLSearchParams(window.location.search).get('mode') === 'frame-debugger';
function App() {
const initRef = useRef(false);
const layoutContainerRef = useRef<FlexLayoutDockContainerHandle>(null);
const [pluginLoader] = useState(() => new PluginLoader());
const { showToast, hideToast } = useToast();
// 如果是独立调试窗口模式,只渲染调试面板 | If standalone debugger mode, only render debug panel
if (isFrameDebuggerMode) {
return (
<div style={{ width: '100vw', height: '100vh', overflow: 'hidden' }}>
<RenderDebugPanel visible={true} onClose={() => window.close()} standalone />
</div>
);
}
// ===== 本地初始化状态(只用于初始化阶段)| Local init state (only for initialization) =====
const [initialized, setInitialized] = useState(false);
@@ -170,10 +187,40 @@ function App() {
showAbout, setShowAbout,
showPluginGenerator, setShowPluginGenerator,
showBuildSettings, setShowBuildSettings,
showRenderDebug, setShowRenderDebug,
errorDialog, setErrorDialog,
confirmDialog, setConfirmDialog
confirmDialog, setConfirmDialog,
externalModificationDialog, setExternalModificationDialog
} = useDialogStore();
// 全局监听独立调试窗口的数据请求 | Global listener for standalone debug window requests
useEffect(() => {
let broadcastInterval: ReturnType<typeof setInterval> | null = null;
const unlistenPromise = listen('render-debug-request-data', () => {
// 开始定时广播数据 | Start broadcasting data periodically
if (!broadcastInterval) {
const broadcast = () => {
renderDebugService.setEnabled(true);
const snap = renderDebugService.collectSnapshot();
if (snap) {
// 使用 emitTo 发送到独立窗口 | Use emitTo to send to standalone window
emitTo('frame-debugger', 'render-debug-snapshot', snap).catch(() => {});
}
};
broadcast(); // 立即广播一次 | Broadcast immediately
broadcastInterval = setInterval(broadcast, 500);
}
});
return () => {
unlistenPromise.then(unlisten => unlisten());
if (broadcastInterval) {
clearInterval(broadcastInterval);
}
};
}, []);
useEffect(() => {
// 禁用默认右键菜单
const handleContextMenu = (e: MouseEvent) => {
@@ -483,6 +530,113 @@ function App() {
};
}, [initialized]);
// Handle external scene file changes
// 处理外部场景文件变更
useEffect(() => {
if (!initialized || !messageHubRef.current || !sceneManagerRef.current) return;
const hub = messageHubRef.current;
const sm = sceneManagerRef.current;
const unsubscribe = hub.subscribe('scene:external-change', (data: {
path: string;
sceneName: string;
}) => {
logger.info('Scene externally modified:', data.path);
// Show confirmation dialog to reload the scene
// 显示确认对话框以重新加载场景
setConfirmDialog({
title: t('scene.externalChange.title'),
message: t('scene.externalChange.message', { name: data.sceneName }),
confirmText: t('scene.externalChange.reload'),
cancelText: t('scene.externalChange.ignore'),
onConfirm: async () => {
setConfirmDialog(null);
try {
await sm.openScene(data.path);
showToast(t('scene.reloadedSuccess', { name: data.sceneName }), 'success');
} catch (error) {
console.error('Failed to reload scene:', error);
showToast(t('scene.reloadFailed'), 'error');
}
},
onCancel: () => {
// User chose to ignore, do nothing
// 用户选择忽略,不做任何操作
}
});
});
return () => unsubscribe?.();
}, [initialized, t, showToast]);
// Handle external modification when saving scene
// 处理保存场景时的外部修改检测
useEffect(() => {
if (!initialized || !messageHubRef.current || !sceneManagerRef.current) return;
const hub = messageHubRef.current;
const sm = sceneManagerRef.current;
const unsubscribe = hub.subscribe('scene:externalModification', (data: {
path: string;
sceneName: string;
}) => {
logger.info('Scene file externally modified during save:', data.path);
// Show external modification dialog with three options
// 显示外部修改对话框,提供三个选项
setExternalModificationDialog({
sceneName: data.sceneName,
onReload: async () => {
setExternalModificationDialog(null);
try {
await sm.reloadScene();
showToast(t('scene.reloadedSuccess', { name: data.sceneName }), 'success');
} catch (error) {
console.error('Failed to reload scene:', error);
showToast(t('scene.reloadFailed'), 'error');
}
},
onOverwrite: async () => {
setExternalModificationDialog(null);
try {
await sm.saveScene(true); // Force save, overwriting external changes
showToast(t('scene.savedSuccess', { name: data.sceneName }), 'success');
} catch (error) {
console.error('Failed to save scene:', error);
showToast(t('scene.saveFailed'), 'error');
}
}
});
});
return () => unsubscribe?.();
}, [initialized, t, showToast, setExternalModificationDialog]);
// Handle user code compilation results
// 处理用户代码编译结果
useEffect(() => {
if (!initialized || !messageHubRef.current) return;
const hub = messageHubRef.current;
const unsubscribe = hub.subscribe('usercode:compilation-result', (data: {
success: boolean;
exports: string[];
errors: string[];
}) => {
if (data.success) {
if (data.exports.length > 0) {
showToast(t('usercode.compileSuccess', { count: data.exports.length }), 'success');
}
} else {
const errorMsg = data.errors[0] ?? t('usercode.compileError');
showToast(errorMsg, 'error');
}
});
return () => unsubscribe?.();
}, [initialized, t, showToast]);
const handleOpenRecentProject = async (projectPath: string) => {
try {
setIsLoading(true, t('loading.step1'));
@@ -523,7 +677,6 @@ function App() {
const sceneFiles = await TauriAPI.scanDirectory(`${projectPath}/scenes`, '*.ecs');
const sceneNames = sceneFiles.map(f => `scenes/${f.split(/[\\/]/).pop()}`);
setAvailableScenes(sceneNames);
console.log('[App] Found scenes:', sceneNames);
} catch (e) {
console.warn('[App] Failed to scan scenes:', e);
}
@@ -545,12 +698,8 @@ function App() {
// Load project plugin config and activate plugins (after engine init, before module system init)
if (pluginManagerRef.current) {
const pluginSettings = projectService.getPluginSettings();
console.log('[App] Plugin settings from project:', pluginSettings);
if (pluginSettings && pluginSettings.enabledPlugins.length > 0) {
console.log('[App] Loading plugin config:', pluginSettings.enabledPlugins);
await pluginManagerRef.current.loadConfig({ enabledPlugins: pluginSettings.enabledPlugins });
} else {
console.log('[App] No plugin settings found in project config');
}
}
@@ -566,6 +715,13 @@ function App() {
setIsLoading(true, t('loading.step3'));
// Wait for user code to be compiled and registered before loading scenes
// 等待用户代码编译和注册完成后再加载场景
const userCodeService = Core.services.tryResolve(UserCodeService);
if (userCodeService) {
await userCodeService.waitForReady();
}
const sceneManagerService = Core.services.resolve(SceneManagerService);
if (sceneManagerService) {
await sceneManagerService.newScene();
@@ -696,6 +852,13 @@ function App() {
}
try {
// Wait for user code to be ready before loading scene
// 在加载场景前等待用户代码就绪
const userCodeService = Core.services.tryResolve(UserCodeService);
if (userCodeService) {
await userCodeService.waitForReady();
}
await sceneManager.openScene();
const sceneState = sceneManager.getSceneState();
setStatus(t('scene.openedSuccess', { name: sceneState.sceneName }));
@@ -706,13 +869,25 @@ function App() {
};
const handleOpenSceneByPath = useCallback(async (scenePath: string) => {
console.log('[App] handleOpenSceneByPath called:', scenePath);
if (!sceneManager) {
console.error('SceneManagerService not available');
return;
}
try {
// Wait for user code to be ready before loading scene
// 在加载场景前等待用户代码就绪
const userCodeService = Core.services.tryResolve(UserCodeService);
if (userCodeService) {
console.log('[App] Waiting for user code service...');
await userCodeService.waitForReady();
console.log('[App] User code service ready');
}
console.log('[App] Calling sceneManager.openScene...');
await sceneManager.openScene(scenePath);
console.log('[App] Scene opened successfully');
const sceneState = sceneManager.getSceneState();
setStatus(t('scene.openedSuccess', { name: sceneState.sceneName }));
} catch (error) {
@@ -1087,6 +1262,14 @@ function App() {
}}
/>
)}
{externalModificationDialog && (
<ExternalModificationDialog
sceneName={externalModificationDialog.sceneName}
onReload={externalModificationDialog.onReload}
onOverwrite={externalModificationDialog.onOverwrite}
onCancel={() => setExternalModificationDialog(null)}
/>
)}
</>
);
}
@@ -1121,6 +1304,7 @@ function App() {
onCreatePlugin={handleCreatePlugin}
onReloadPlugins={handleReloadPlugins}
onOpenBuildSettings={() => setShowBuildSettings(true)}
onOpenRenderDebug={() => setShowRenderDebug(true)}
/>
<MainToolbar
messageHub={messageHub || undefined}
@@ -1226,6 +1410,12 @@ function App() {
/>
)}
{/* 渲染调试面板 | Render Debug Panel */}
<RenderDebugPanel
visible={showRenderDebug}
onClose={() => setShowRenderDebug(false)}
/>
{errorDialog && (
<ErrorDialog
title={errorDialog.title}
@@ -1252,6 +1442,15 @@ function App() {
}}
/>
)}
{externalModificationDialog && (
<ExternalModificationDialog
sceneName={externalModificationDialog.sceneName}
onReload={externalModificationDialog.onReload}
onOverwrite={externalModificationDialog.onOverwrite}
onCancel={() => setExternalModificationDialog(null)}
/>
)}
</div>
);
}

View File

@@ -6,6 +6,16 @@ interface ErrorDialogData {
message: string;
}
/**
* 外部修改对话框数据
* External modification dialog data
*/
export interface ExternalModificationDialogData {
sceneName: string;
onReload: () => void;
onOverwrite: () => void;
}
interface DialogState {
showProfiler: boolean;
showAdvancedProfiler: boolean;
@@ -14,8 +24,10 @@ interface DialogState {
showAbout: boolean;
showPluginGenerator: boolean;
showBuildSettings: boolean;
showRenderDebug: boolean;
errorDialog: ErrorDialogData | null;
confirmDialog: ConfirmDialogData | null;
externalModificationDialog: ExternalModificationDialogData | null;
setShowProfiler: (show: boolean) => void;
setShowAdvancedProfiler: (show: boolean) => void;
@@ -24,8 +36,10 @@ interface DialogState {
setShowAbout: (show: boolean) => void;
setShowPluginGenerator: (show: boolean) => void;
setShowBuildSettings: (show: boolean) => void;
setShowRenderDebug: (show: boolean) => void;
setErrorDialog: (data: ErrorDialogData | null) => void;
setConfirmDialog: (data: ConfirmDialogData | null) => void;
setExternalModificationDialog: (data: ExternalModificationDialogData | null) => void;
closeAllDialogs: () => void;
}
@@ -37,8 +51,10 @@ export const useDialogStore = create<DialogState>((set) => ({
showAbout: false,
showPluginGenerator: false,
showBuildSettings: false,
showRenderDebug: false,
errorDialog: null,
confirmDialog: null,
externalModificationDialog: null,
setShowProfiler: (show) => set({ showProfiler: show }),
setShowAdvancedProfiler: (show) => set({ showAdvancedProfiler: show }),
@@ -47,8 +63,10 @@ export const useDialogStore = create<DialogState>((set) => ({
setShowAbout: (show) => set({ showAbout: show }),
setShowPluginGenerator: (show) => set({ showPluginGenerator: show }),
setShowBuildSettings: (show) => set({ showBuildSettings: show }),
setShowRenderDebug: (show) => set({ showRenderDebug: show }),
setErrorDialog: (data) => set({ errorDialog: data }),
setConfirmDialog: (data) => set({ confirmDialog: data }),
setExternalModificationDialog: (data) => set({ externalModificationDialog: data }),
closeAllDialogs: () => set({
showProfiler: false,
@@ -58,7 +76,9 @@ export const useDialogStore = create<DialogState>((set) => ({
showAbout: false,
showPluginGenerator: false,
showBuildSettings: false,
showRenderDebug: false,
errorDialog: null,
confirmDialog: null
confirmDialog: null,
externalModificationDialog: null
})
}));

View File

@@ -1026,13 +1026,16 @@ export class ${className} {
// Handle asset double click
const handleAssetDoubleClick = useCallback(async (asset: AssetItem) => {
console.log('[ContentBrowser] Double click:', asset.name, 'type:', asset.type, 'ext:', asset.extension);
if (asset.type === 'folder') {
setCurrentPath(asset.path);
loadAssets(asset.path);
setExpandedFolders(prev => new Set([...prev, asset.path]));
} else {
const ext = asset.extension?.toLowerCase();
console.log('[ContentBrowser] File ext:', ext, 'onOpenScene:', !!onOpenScene);
if (ext === 'ecs' && onOpenScene) {
console.log('[ContentBrowser] Opening scene:', asset.path);
onOpenScene(asset.path);
return;
}

View File

@@ -38,6 +38,7 @@ interface TitleBarProps {
onCreatePlugin?: () => void;
onReloadPlugins?: () => void;
onOpenBuildSettings?: () => void;
onOpenRenderDebug?: () => void;
}
export function TitleBar({
@@ -61,7 +62,8 @@ export function TitleBar({
onOpenAbout,
onCreatePlugin,
onReloadPlugins,
onOpenBuildSettings
onOpenBuildSettings,
onOpenRenderDebug
}: TitleBarProps) {
const { t } = useLocale();
const [openMenu, setOpenMenu] = useState<string | null>(null);
@@ -197,6 +199,7 @@ export function TitleBar({
{ label: t('menu.tools.reloadPlugins'), shortcut: 'Ctrl+R', onClick: onReloadPlugins },
{ separator: true },
{ label: t('menu.tools.portManager'), onClick: onOpenPortManager },
{ label: t('menu.tools.renderDebug'), onClick: onOpenRenderDebug },
{ separator: true },
{ label: t('menu.tools.settings'), onClick: onOpenSettings }
],

View File

@@ -8,9 +8,9 @@ import '../styles/Viewport.css';
import { useEngine } from '../hooks/useEngine';
import { useLocale } from '../hooks/useLocale';
import { EngineService } from '../services/EngineService';
import { Core, Entity, SceneSerializer, PrefabSerializer, ComponentRegistry } from '@esengine/ecs-framework';
import { Core, Entity, SceneSerializer, PrefabSerializer, GlobalComponentRegistry } from '@esengine/ecs-framework';
import type { PrefabData, ComponentType } from '@esengine/ecs-framework';
import { MessageHub, ProjectService, AssetRegistryService, EntityStoreService, CommandManager, SceneManagerService } from '@esengine/editor-core';
import { MessageHub, ProjectService, AssetRegistryService, EntityStoreService, CommandManager, SceneManagerService, UserCodeService, UserCodeTarget } from '@esengine/editor-core';
import { InstantiatePrefabCommand } from '../application/commands/prefab/InstantiatePrefabCommand';
import { TransformCommand, type TransformState, type TransformOperationType } from '../application/commands';
import { TransformComponent } from '@esengine/engine-core';
@@ -21,6 +21,8 @@ import { open } from '@tauri-apps/plugin-shell';
import { RuntimeResolver } from '../services/RuntimeResolver';
import { QRCodeDialog } from './QRCodeDialog';
import { collectAssetReferences } from '@esengine/asset-system';
import { RuntimeSceneManager, type IRuntimeSceneManager } from '@esengine/runtime-core';
import { ParticleSystemComponent } from '@esengine/particle';
import type { ModuleManifest } from '../services/RuntimeResolver';
@@ -264,6 +266,9 @@ export function Viewport({ locale = 'en', messageHub, commandManager }: Viewport
const editorCameraRef = useRef({ x: 0, y: 0, zoom: 1 });
const playStateRef = useRef<PlayState>('stopped');
// Runtime scene manager for play mode scene switching | Play 模式场景切换管理器
const runtimeSceneManagerRef = useRef<IRuntimeSceneManager | null>(null);
// Live transform display state | 实时变换显示状态
const [liveTransform, setLiveTransform] = useState<{
type: 'move' | 'rotate' | 'scale';
@@ -811,7 +816,22 @@ export function Viewport({ locale = 'en', messageHub, commandManager }: Viewport
return;
}
// Save scene snapshot before playing
// saveSceneSnapshot clears all textures, so we need to reset particle textureIds after
// saveSceneSnapshot 会清除所有纹理,所以之后需要重置粒子的 textureId
EngineService.getInstance().saveSceneSnapshot();
// Reset particle component textureIds after snapshot (textures were cleared)
// 快照后重置粒子组件的 textureId纹理已被清除
const scene = Core.scene;
if (scene) {
for (const entity of scene.entities.buffer) {
const particleComponent = entity.getComponent(ParticleSystemComponent);
if (particleComponent) {
particleComponent.textureId = 0;
}
}
}
// Save editor camera state
editorCameraRef.current = { x: camera2DOffset.x, y: camera2DOffset.y, zoom: camera2DZoom };
setPlayState('playing');
@@ -820,6 +840,132 @@ export function Viewport({ locale = 'en', messageHub, commandManager }: Viewport
EngineService.getInstance().setEditorMode(false);
// Switch to player camera
syncPlayerCamera();
// Register RuntimeSceneManager for scene switching in play mode
// 注册 RuntimeSceneManager 以支持 Play 模式下的场景切换
const projectService = Core.services.tryResolve(ProjectService);
const projectPath = projectService?.getCurrentProject()?.path;
if (projectPath) {
// Create scene loader function that reads scene files using Tauri API
// 创建使用 Tauri API 读取场景文件的场景加载器函数
const editorSceneLoader = async (scenePath: string): Promise<void> => {
try {
// Normalize path: handle both relative and absolute paths
// 标准化路径:处理相对路径和绝对路径
let fullPath = scenePath;
if (!scenePath.includes(':') && !scenePath.startsWith('/')) {
// Relative path - construct full path
// 相对路径 - 构建完整路径
const normalizedPath = scenePath.replace(/^\.\//, '').replace(/\//g, '\\');
fullPath = `${projectPath}\\${normalizedPath}`;
} else {
// Absolute path - normalize separators for Windows
// 绝对路径 - 为 Windows 规范化分隔符
fullPath = scenePath.replace(/\//g, '\\');
}
// Read scene file content
// 读取场景文件内容
const sceneJson = await TauriAPI.readFileContent(fullPath);
// Validate scene data
// 验证场景数据
const validation = SceneSerializer.validate(sceneJson);
if (!validation.valid) {
throw new Error(`Invalid scene: ${validation.errors?.join(', ')}`);
}
// Save current scene snapshot (so we can go back)
// 保存当前场景快照(以便返回)
EngineService.getInstance().saveSceneSnapshot();
// Load new scene by deserializing into current scene
// 通过反序列化加载新场景到当前场景
const scene = Core.scene;
if (scene) {
scene.deserialize(sceneJson, { strategy: 'replace' });
// Reset particle component textureIds after scene switch
// 场景切换后重置粒子组件的 textureId
// This ensures ParticleUpdateSystem will reload textures
// 这确保 ParticleUpdateSystem 会重新加载纹理
for (const entity of scene.entities.buffer) {
const particleComponent = entity.getComponent(ParticleSystemComponent);
if (particleComponent) {
particleComponent.textureId = 0;
}
}
// Re-register user code components and systems after scene switch
// 场景切换后重新注册用户代码组件和系统
const userCodeService = Core.services.tryResolve(UserCodeService);
if (userCodeService) {
const runtimeModule = userCodeService.getModule(UserCodeTarget.Runtime);
if (runtimeModule) {
// Re-register components (ensures GlobalComponentRegistry has correct references)
// 重新注册组件(确保 GlobalComponentRegistry 有正确的引用)
userCodeService.registerComponents(runtimeModule, GlobalComponentRegistry);
// Re-register systems (recreates systems with correct component references)
// 重新注册系统(使用正确的组件引用重建系统)
userCodeService.registerSystems(runtimeModule, scene);
}
}
// Load scene resources (textures, etc.)
// 加载场景资源(纹理等)
await EngineService.getInstance().loadSceneResources();
// Sync entity store
// 同步实体存储
const entityStore = Core.services.tryResolve(EntityStoreService);
entityStore?.syncFromScene();
}
console.log(`[Viewport] Scene loaded in play mode: ${scenePath}`);
} catch (error) {
console.error(`[Viewport] Failed to load scene: ${scenePath}`, error);
throw error;
}
};
// Create and register RuntimeSceneManager
// 创建并注册 RuntimeSceneManager
const sceneManager = new RuntimeSceneManager(
editorSceneLoader,
`${projectPath}\\scenes`
);
runtimeSceneManagerRef.current = sceneManager;
// Register to Core.services with the global key
// 使用全局 key 注册到 Core.services
const GlobalSceneManagerKey = Symbol.for('@esengine/service:runtimeSceneManager');
if (!Core.services.isRegistered(GlobalSceneManagerKey)) {
Core.services.registerInstance(GlobalSceneManagerKey, sceneManager);
}
console.log('[Viewport] RuntimeSceneManager registered for play mode');
}
// Register user code components and systems before starting engine
// 在启动引擎前注册用户代码组件和系统
const userCodeService = Core.services.tryResolve(UserCodeService);
if (userCodeService) {
const runtimeModule = userCodeService.getModule(UserCodeTarget.Runtime);
if (runtimeModule) {
// Register components first (ensures GlobalComponentRegistry has correct references)
// 先注册组件(确保 GlobalComponentRegistry 有正确的引用)
userCodeService.registerComponents(runtimeModule, GlobalComponentRegistry);
// Then register systems (uses registered component references)
// 然后注册系统(使用已注册的组件引用)
const scene = Core.scene;
if (scene) {
userCodeService.registerSystems(runtimeModule, scene);
}
}
}
engine.start();
} else if (playState === 'paused') {
setPlayState('playing');
@@ -837,6 +983,19 @@ export function Viewport({ locale = 'en', messageHub, commandManager }: Viewport
const handleStop = async () => {
setPlayState('stopped');
engine.stop();
// Unregister RuntimeSceneManager
// 注销 RuntimeSceneManager
if (runtimeSceneManagerRef.current) {
const GlobalSceneManagerKey = Symbol.for('@esengine/service:runtimeSceneManager');
if (Core.services.isRegistered(GlobalSceneManagerKey)) {
Core.services.unregister(GlobalSceneManagerKey);
}
runtimeSceneManagerRef.current.dispose();
runtimeSceneManagerRef.current = null;
console.log('[Viewport] RuntimeSceneManager unregistered');
}
// Restore scene snapshot
await EngineService.getInstance().restoreSceneSnapshot();
// Restore editor camera state

View File

@@ -0,0 +1,633 @@
/**
* 渲染调试面板样式 (浮动窗口)
* Render Debug Panel Styles (Floating Window)
*/
/* ==================== Floating Window ==================== */
.render-debug-window {
position: fixed;
display: flex;
flex-direction: column;
background: #1e1e1e;
border: 1px solid #3c3c3c;
border-radius: 6px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
z-index: 1000;
overflow: hidden;
font-family: 'Segoe UI', system-ui, sans-serif;
font-size: 11px;
color: #ccc;
}
.render-debug-window.dragging {
cursor: move;
user-select: none;
}
/* 独立窗口模式 | Standalone mode */
.render-debug-window.standalone {
position: relative;
border: none;
border-radius: 0;
box-shadow: none;
}
.render-debug-window.standalone .window-header {
cursor: default;
}
/* ==================== Window Header ==================== */
.render-debug-window .window-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: #2d2d2d;
border-bottom: 1px solid #1a1a1a;
cursor: move;
flex-shrink: 0;
}
.render-debug-window .window-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: #e0e0e0;
}
.render-debug-window .window-title svg {
color: #4a9eff;
}
.render-debug-window .paused-badge {
padding: 2px 6px;
background: #f59e0b;
color: #000;
font-size: 9px;
font-weight: 700;
border-radius: 3px;
letter-spacing: 0.5px;
}
.render-debug-window .window-controls {
display: flex;
gap: 4px;
}
.render-debug-window .window-btn {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: transparent;
border: none;
border-radius: 4px;
color: #888;
cursor: pointer;
transition: all 0.15s;
}
.render-debug-window .window-btn:hover {
background: #3a3a3a;
color: #fff;
}
/* ==================== Toolbar ==================== */
.render-debug-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 12px;
background: #262626;
border-bottom: 1px solid #1a1a1a;
flex-shrink: 0;
}
.render-debug-toolbar .toolbar-left,
.render-debug-toolbar .toolbar-right {
display: flex;
align-items: center;
gap: 6px;
}
.render-debug-toolbar .toolbar-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background: #3a3a3a;
border: 1px solid #4a4a4a;
border-radius: 3px;
color: #ccc;
font-size: 10px;
cursor: pointer;
transition: all 0.15s;
}
.render-debug-toolbar .toolbar-btn:hover {
background: #4a4a4a;
color: #fff;
}
.render-debug-toolbar .toolbar-btn.active {
background: #4a9eff;
border-color: #4a9eff;
color: #fff;
}
.render-debug-toolbar .toolbar-btn.icon-only {
padding: 4px 6px;
}
.render-debug-toolbar .toolbar-btn.recording {
background: rgba(239, 68, 68, 0.2);
border-color: #ef4444;
}
.render-debug-toolbar .toolbar-btn .record-dot {
display: inline-block;
width: 10px;
height: 10px;
background: #ef4444;
border-radius: 50%;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.render-debug-toolbar .history-badge {
padding: 2px 6px;
background: #8b5cf6;
color: #fff;
font-size: 9px;
font-weight: 700;
border-radius: 3px;
letter-spacing: 0.5px;
margin-left: 4px;
}
.render-debug-toolbar .toolbar-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.render-debug-toolbar .toolbar-btn:disabled:hover {
background: #3a3a3a;
color: #ccc;
}
/* ==================== Timeline ==================== */
.render-debug-timeline {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px 12px;
background: #222;
border-bottom: 1px solid #1a1a1a;
flex-shrink: 0;
}
.render-debug-timeline .timeline-slider {
width: 100%;
height: 6px;
-webkit-appearance: none;
appearance: none;
background: #333;
border-radius: 3px;
cursor: pointer;
}
.render-debug-timeline .timeline-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 14px;
height: 14px;
background: #4a9eff;
border-radius: 50%;
cursor: grab;
transition: transform 0.1s;
}
.render-debug-timeline .timeline-slider::-webkit-slider-thumb:hover {
transform: scale(1.2);
}
.render-debug-timeline .timeline-slider::-webkit-slider-thumb:active {
cursor: grabbing;
transform: scale(1.1);
}
.render-debug-timeline .timeline-slider::-moz-range-thumb {
width: 14px;
height: 14px;
background: #4a9eff;
border: none;
border-radius: 50%;
cursor: grab;
}
.render-debug-timeline .timeline-info {
display: flex;
justify-content: space-between;
font-size: 9px;
color: #666;
}
.render-debug-toolbar .toolbar-separator {
width: 1px;
height: 16px;
background: #3a3a3a;
}
.render-debug-toolbar .frame-counter {
font-family: 'Consolas', monospace;
font-size: 10px;
color: #888;
padding: 0 6px;
}
/* ==================== Main Layout ==================== */
.render-debug-main {
display: flex;
flex: 1;
min-height: 0;
overflow: hidden;
}
/* ==================== Left Panel (Event List) ==================== */
.render-debug-left {
width: 260px;
min-width: 180px;
display: flex;
flex-direction: column;
background: #222;
border-right: 1px solid #1a1a1a;
flex-shrink: 0;
}
.event-list-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
background: #262626;
border-bottom: 1px solid #1a1a1a;
font-size: 10px;
font-weight: 600;
color: #888;
}
.event-list-header .event-count {
font-weight: 400;
color: #666;
}
.event-list {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
}
.event-list::-webkit-scrollbar {
width: 5px;
}
.event-list::-webkit-scrollbar-track {
background: #1a1a1a;
}
.event-list::-webkit-scrollbar-thumb {
background: #3a3a3a;
border-radius: 2px;
}
.event-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #555;
font-size: 10px;
text-align: center;
padding: 16px;
line-height: 1.5;
}
/* Event Items */
.event-item {
display: flex;
align-items: center;
padding: 3px 6px;
cursor: pointer;
user-select: none;
font-size: 10px;
color: #bbb;
border-bottom: 1px solid #1a1a1a;
gap: 3px;
}
.event-item:hover {
background: rgba(255, 255, 255, 0.05);
}
.event-item.selected {
background: rgba(74, 158, 255, 0.2);
border-left: 2px solid #4a9eff;
padding-left: 4px;
}
.event-item .expand-icon {
display: flex;
align-items: center;
justify-content: center;
width: 12px;
height: 12px;
color: #666;
flex-shrink: 0;
}
.event-item .expand-icon:not(.placeholder):hover {
color: #ccc;
}
.event-item .expand-icon.placeholder {
visibility: hidden;
}
.event-item .event-icon {
color: #666;
flex-shrink: 0;
margin-right: 3px;
}
.event-item .event-icon.sprite {
color: #4fc3f7;
}
.event-item .event-icon.particle {
color: #ffb74d;
}
.event-item .event-icon.ui {
color: #81c784;
}
.event-item .event-icon.batch {
color: #81c784;
}
.event-item .event-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.event-item .event-draws {
font-family: 'Consolas', monospace;
font-size: 9px;
color: #666;
padding: 1px 3px;
background: #1a1a1a;
border-radius: 2px;
flex-shrink: 0;
}
/* ==================== Right Panel ==================== */
.render-debug-right {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
}
/* Preview Section */
.render-debug-preview {
height: 40%;
min-height: 120px;
display: flex;
flex-direction: column;
border-bottom: 1px solid #1a1a1a;
}
.preview-header {
padding: 6px 10px;
background: #262626;
border-bottom: 1px solid #1a1a1a;
font-size: 10px;
font-weight: 600;
color: #888;
}
.preview-canvas-container {
flex: 1;
background: #1a1a1a;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.preview-canvas-container canvas {
width: 100%;
height: 100%;
}
/* Details Section */
.render-debug-details {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.details-header {
padding: 6px 10px;
background: #262626;
border-bottom: 1px solid #1a1a1a;
font-size: 10px;
font-weight: 600;
color: #888;
}
.details-content {
flex: 1;
overflow-y: auto;
padding: 10px;
background: #1e1e1e;
}
.details-content::-webkit-scrollbar {
width: 5px;
}
.details-content::-webkit-scrollbar-track {
background: #1a1a1a;
}
.details-content::-webkit-scrollbar-thumb {
background: #3a3a3a;
border-radius: 2px;
}
.details-empty {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #555;
font-size: 10px;
}
/* Details Grid */
.details-grid {
display: flex;
flex-direction: column;
gap: 1px;
}
.details-section {
font-size: 9px;
font-weight: 600;
color: #666;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 8px 0 3px 0;
margin-top: 6px;
border-top: 1px solid #333;
}
.details-section:first-child {
margin-top: 0;
border-top: none;
padding-top: 0;
}
.detail-row {
display: flex;
align-items: flex-start;
padding: 3px 0;
font-size: 10px;
}
.detail-row .detail-label {
width: 100px;
color: #888;
flex-shrink: 0;
}
.detail-row .detail-value {
flex: 1;
color: #ccc;
font-family: 'Consolas', monospace;
word-break: break-all;
}
.detail-row.highlight .detail-value {
color: #4fc3f7;
font-weight: 600;
}
/* ==================== Stats Bar ==================== */
.render-debug-stats {
display: flex;
align-items: center;
gap: 16px;
padding: 6px 12px;
background: #262626;
border-top: 1px solid #1a1a1a;
flex-shrink: 0;
}
.render-debug-stats .stat-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 10px;
color: #888;
}
.render-debug-stats .stat-item svg {
color: #4a9eff;
}
/* ==================== Resize Handle ==================== */
.resize-handle {
position: absolute;
right: 0;
bottom: 0;
width: 16px;
height: 16px;
cursor: se-resize;
background: linear-gradient(135deg, transparent 50%, #3a3a3a 50%);
border-radius: 0 0 6px 0;
}
.resize-handle:hover {
background: linear-gradient(135deg, transparent 50%, #4a9eff 50%);
}
/* ==================== TextureSheet Preview ==================== */
.texture-sheet-preview {
margin-top: 8px;
border-radius: 4px;
overflow: hidden;
background: #1a1a1a;
border: 1px solid #333;
}
.texture-sheet-preview canvas {
display: block;
width: 100%;
height: auto;
}
/* ==================== Texture Preview ==================== */
.texture-preview-row {
display: flex;
align-items: flex-start;
padding: 3px 0;
font-size: 10px;
}
.texture-preview-row .detail-label {
width: 100px;
color: #888;
flex-shrink: 0;
}
.texture-preview-content {
flex: 1;
min-width: 0;
}
.texture-thumbnail-container {
display: flex;
flex-direction: column;
gap: 4px;
}
.texture-thumbnail {
max-width: 100%;
max-height: 80px;
object-fit: contain;
border-radius: 3px;
border: 1px solid #333;
background: repeating-conic-gradient(#2a2a2a 0% 25%, #1a1a1a 0% 50%) 50% / 8px 8px;
}
.texture-path {
font-family: 'Consolas', monospace;
font-size: 9px;
color: #666;
word-break: break-all;
line-height: 1.3;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
/**
* 调试组件导出
* Debug components export
*/
export { RenderDebugPanel } from './RenderDebugPanel';
export type { default as RenderDebugPanelProps } from './RenderDebugPanel';

View File

@@ -0,0 +1,591 @@
/**
* 渲染调试服务
* Render Debug Service
*
* 从引擎收集渲染调试数据
* Collects render debug data from the engine
*/
import { Core, Entity } from '@esengine/ecs-framework';
import { TransformComponent } from '@esengine/engine-core';
import { SpriteComponent } from '@esengine/sprite';
import { ParticleSystemComponent } from '@esengine/particle';
import { UITransformComponent, UIRenderComponent, UITextComponent } from '@esengine/ui';
import { AssetRegistryService, ProjectService } from '@esengine/editor-core';
import { invoke } from '@tauri-apps/api/core';
/**
* 纹理调试信息
* Texture debug info
*/
export interface TextureDebugInfo {
id: number;
path: string;
width: number;
height: number;
state: 'loading' | 'ready' | 'failed';
}
/**
* Sprite 调试信息
* Sprite debug info
*/
export interface SpriteDebugInfo {
entityId: number;
entityName: string;
x: number;
y: number;
width: number;
height: number;
rotation: number;
textureId: number;
texturePath: string;
/** 预解析的纹理 URL可直接用于 img src| Pre-resolved texture URL (can be used directly in img src) */
textureUrl?: string;
uv: [number, number, number, number];
color: string;
alpha: number;
sortingLayer: string;
orderInLayer: number;
}
/**
* 粒子调试信息
* Particle debug info
*/
export interface ParticleDebugInfo {
entityId: number;
entityName: string;
systemName: string;
isPlaying: boolean;
activeCount: number;
maxParticles: number;
textureId: number;
texturePath: string;
/** 预解析的纹理 URL可直接用于 img src| Pre-resolved texture URL (can be used directly in img src) */
textureUrl?: string;
textureSheetAnimation: {
enabled: boolean;
tilesX: number;
tilesY: number;
totalFrames: number;
} | null;
sampleParticles: Array<{
index: number;
x: number;
y: number;
frame: number;
uv: [number, number, number, number];
age: number;
lifetime: number;
size: number;
color: string;
alpha: number;
}>;
}
/**
* UI 元素调试信息
* UI element debug info
*/
export interface UIDebugInfo {
entityId: number;
entityName: string;
type: 'rect' | 'image' | 'text' | 'ninepatch' | 'circle' | 'rounded-rect' | 'unknown';
x: number;
y: number;
width: number;
height: number;
worldX: number;
worldY: number;
rotation: number;
visible: boolean;
alpha: number;
sortingLayer: string;
orderInLayer: number;
textureGuid?: string;
textureUrl?: string;
backgroundColor?: string;
text?: string;
fontSize?: number;
}
/**
* 渲染调试快照
* Render debug snapshot
*/
export interface RenderDebugSnapshot {
timestamp: number;
frameNumber: number;
textures: TextureDebugInfo[];
sprites: SpriteDebugInfo[];
particles: ParticleDebugInfo[];
uiElements: UIDebugInfo[];
stats: {
totalSprites: number;
totalParticles: number;
totalUIElements: number;
totalTextures: number;
drawCalls: number;
};
}
/**
* 渲染调试服务
* Render Debug Service
*/
export class RenderDebugService {
private static _instance: RenderDebugService | null = null;
private _frameNumber: number = 0;
private _enabled: boolean = false;
private _snapshots: RenderDebugSnapshot[] = [];
private _maxSnapshots: number = 60;
// 引擎引用 | Engine reference
private _engineBridge: any = null;
static getInstance(): RenderDebugService {
if (!RenderDebugService._instance) {
RenderDebugService._instance = new RenderDebugService();
}
return RenderDebugService._instance;
}
/**
* 设置引擎桥接
* Set engine bridge
*/
setEngineBridge(bridge: any): void {
this._engineBridge = bridge;
}
/**
* 启用/禁用调试
* Enable/disable debugging
*/
setEnabled(enabled: boolean): void {
this._enabled = enabled;
if (!enabled) {
this._snapshots = [];
}
}
get enabled(): boolean {
return this._enabled;
}
// 纹理 base64 缓存 | Texture base64 cache
private _textureCache = new Map<string, string>();
private _texturePending = new Set<string>();
/**
* 解析纹理 GUID 为 base64 data URL从缓存获取
* Resolve texture GUID to base64 data URL (from cache)
*/
private _resolveTextureUrl(textureGuid: string | null | undefined): string | undefined {
if (!textureGuid) return undefined;
// 从缓存获取 | Get from cache
if (this._textureCache.has(textureGuid)) {
console.log('[RenderDebugService] Texture from cache:', textureGuid);
return this._textureCache.get(textureGuid);
}
// 如果正在加载中,返回 undefined | If loading, return undefined
if (this._texturePending.has(textureGuid)) {
console.log('[RenderDebugService] Texture loading:', textureGuid);
return undefined;
}
// 异步加载纹理 | Load texture asynchronously
console.log('[RenderDebugService] Starting texture load:', textureGuid);
this._loadTextureToCache(textureGuid);
return undefined;
}
/**
* 异步加载纹理到缓存
* Load texture to cache asynchronously
*/
private async _loadTextureToCache(textureGuid: string): Promise<void> {
if (this._textureCache.has(textureGuid) || this._texturePending.has(textureGuid)) {
return;
}
this._texturePending.add(textureGuid);
try {
const assetRegistry = Core.services.tryResolve(AssetRegistryService) as AssetRegistryService | null;
const projectService = Core.services.tryResolve(ProjectService) as { getCurrentProject: () => { path: string } | null } | null;
let resolvedPath: string | null = null;
// 检查是否是 GUID 格式 | Check if GUID format
const isGuid = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(textureGuid);
if (isGuid && assetRegistry) {
resolvedPath = assetRegistry.getPathByGuid(textureGuid) || null;
} else {
resolvedPath = textureGuid;
}
if (!resolvedPath) {
this._texturePending.delete(textureGuid);
return;
}
// 检查是否是图片 | Check if image
const ext = resolvedPath.toLowerCase().split('.').pop() || '';
const imageExts: Record<string, string> = {
'png': 'image/png',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'gif': 'image/gif',
'webp': 'image/webp',
'bmp': 'image/bmp'
};
const mimeType = imageExts[ext];
if (!mimeType) {
this._texturePending.delete(textureGuid);
return;
}
// 构建完整路径 | Build full path
const projectPath = projectService?.getCurrentProject()?.path;
const fullPath = resolvedPath.startsWith('/') || resolvedPath.includes(':')
? resolvedPath
: projectPath
? `${projectPath}/${resolvedPath}`
: resolvedPath;
// 通过 Tauri command 读取文件并转为 base64 | Read file via Tauri command and convert to base64
console.log('[RenderDebugService] Loading texture:', fullPath);
const base64 = await invoke<string>('read_file_as_base64', { filePath: fullPath });
const dataUrl = `data:${mimeType};base64,${base64}`;
console.log('[RenderDebugService] Texture loaded, base64 length:', base64.length);
this._textureCache.set(textureGuid, dataUrl);
} catch (err) {
console.error('[RenderDebugService] Failed to load texture:', textureGuid, err);
} finally {
this._texturePending.delete(textureGuid);
}
}
/**
* 收集当前帧的调试数据
* Collect debug data for current frame
*/
collectSnapshot(): RenderDebugSnapshot | null {
if (!this._enabled) return null;
const scene = Core.scene;
if (!scene) return null;
this._frameNumber++;
const snapshot: RenderDebugSnapshot = {
timestamp: Date.now(),
frameNumber: this._frameNumber,
textures: this._collectTextures(),
sprites: this._collectSprites(scene.entities.buffer),
particles: this._collectParticles(scene.entities.buffer),
uiElements: this._collectUI(scene.entities.buffer),
stats: {
totalSprites: 0,
totalParticles: 0,
totalUIElements: 0,
totalTextures: 0,
drawCalls: 0,
},
};
// 计算统计 | Calculate stats
snapshot.stats.totalSprites = snapshot.sprites.length;
snapshot.stats.totalParticles = snapshot.particles.reduce((sum, p) => sum + p.activeCount, 0);
snapshot.stats.totalUIElements = snapshot.uiElements.length;
snapshot.stats.totalTextures = snapshot.textures.length;
// 保存快照 | Save snapshot
this._snapshots.push(snapshot);
if (this._snapshots.length > this._maxSnapshots) {
this._snapshots.shift();
}
return snapshot;
}
/**
* 获取最新快照
* Get latest snapshot
*/
getLatestSnapshot(): RenderDebugSnapshot | null {
return this._snapshots.length > 0 ? this._snapshots[this._snapshots.length - 1] ?? null : null;
}
/**
* 获取所有快照
* Get all snapshots
*/
getSnapshots(): RenderDebugSnapshot[] {
return [...this._snapshots];
}
/**
* 清除快照
* Clear snapshots
*/
clearSnapshots(): void {
this._snapshots = [];
}
/**
* 收集纹理信息
* Collect texture info
*/
private _collectTextures(): TextureDebugInfo[] {
const textures: TextureDebugInfo[] = [];
// TODO: 从 EngineBridge 获取纹理管理器数据
// TODO: Get texture manager data from EngineBridge
if (this._engineBridge) {
// const textureManager = this._engineBridge.getTextureManager();
// for (const [id, tex] of textureManager.entries()) {
// textures.push({ ... });
// }
}
return textures;
}
/**
* 收集 Sprite 信息
* Collect sprite info
*/
private _collectSprites(entities: readonly Entity[]): SpriteDebugInfo[] {
const sprites: SpriteDebugInfo[] = [];
for (const entity of entities) {
const sprite = entity.getComponent(SpriteComponent);
const transform = entity.getComponent(TransformComponent);
if (!sprite || !transform) continue;
const pos = transform.worldPosition ?? transform.position;
const rot = typeof transform.rotation === 'number'
? transform.rotation
: transform.rotation.z;
const textureGuid = sprite.textureGuid ?? '';
sprites.push({
entityId: entity.id,
entityName: entity.name,
x: pos.x,
y: pos.y,
width: sprite.width,
height: sprite.height,
rotation: rot,
textureId: (sprite as any).textureId ?? 0,
texturePath: textureGuid,
textureUrl: this._resolveTextureUrl(textureGuid),
uv: [...sprite.uv] as [number, number, number, number],
color: sprite.color,
alpha: sprite.alpha,
sortingLayer: sprite.sortingLayer,
orderInLayer: sprite.orderInLayer,
});
}
return sprites;
}
/**
* 收集粒子系统信息
* Collect particle system info
*/
private _collectParticles(entities: readonly Entity[]): ParticleDebugInfo[] {
const particleSystems: ParticleDebugInfo[] = [];
for (const entity of entities) {
const ps = entity.getComponent(ParticleSystemComponent);
const transform = entity.getComponent(TransformComponent);
if (!ps) continue;
const pool = ps.pool;
// 通过 getModule 获取 TextureSheetAnimation 模块 | Get TextureSheetAnimation module via getModule
const textureSheetAnim = ps.getModule?.('TextureSheetAnimation') as any;
// 收集所有活跃粒子 | Collect all active particles
const sampleParticles: ParticleDebugInfo['sampleParticles'] = [];
if (pool) {
let count = 0;
pool.forEachActive((p: any) => {
const tilesX = p._animTilesX ?? 1;
const tilesY = p._animTilesY ?? 1;
const frame = p._animFrame ?? 0;
const col = frame % tilesX;
const row = Math.floor(frame / tilesX);
const uWidth = 1 / tilesX;
const vHeight = 1 / tilesY;
sampleParticles.push({
index: count,
x: p.x,
y: p.y,
frame,
uv: [
col * uWidth,
row * vHeight,
(col + 1) * uWidth,
(row + 1) * vHeight,
],
age: p.age,
lifetime: p.lifetime,
size: p.size ?? p.startSize ?? 1,
color: p.color ?? '#ffffff',
alpha: p.alpha ?? 1,
});
count++;
});
}
// 获取模块的 tilesX/tilesY | Get tilesX/tilesY from module
const tilesX = textureSheetAnim?.tilesX ?? 1;
const tilesY = textureSheetAnim?.tilesY ?? 1;
const totalFrames = textureSheetAnim?.actualTotalFrames ?? (tilesX * tilesY);
const textureGuid = ps.textureGuid ?? '';
particleSystems.push({
entityId: entity.id,
entityName: entity.name,
systemName: `ParticleSystem_${entity.id}`,
isPlaying: ps.isPlaying,
activeCount: pool?.activeCount ?? 0,
maxParticles: ps.maxParticles,
textureId: ps.textureId ?? 0,
texturePath: textureGuid,
textureUrl: this._resolveTextureUrl(textureGuid),
textureSheetAnimation: textureSheetAnim?.enabled ? {
enabled: true,
tilesX,
tilesY,
totalFrames,
} : null,
sampleParticles,
});
}
return particleSystems;
}
/**
* 收集 UI 元素信息
* Collect UI element info
*/
private _collectUI(entities: readonly Entity[]): UIDebugInfo[] {
const uiElements: UIDebugInfo[] = [];
for (const entity of entities) {
const uiTransform = entity.getComponent(UITransformComponent);
if (!uiTransform) continue;
const uiRender = entity.getComponent(UIRenderComponent);
const uiText = entity.getComponent(UITextComponent);
// 确定类型 | Determine type
let type: UIDebugInfo['type'] = 'unknown';
if (uiText) {
type = 'text';
} else if (uiRender) {
switch (uiRender.type) {
case 'rect': type = 'rect'; break;
case 'image': type = 'image'; break;
case 'ninepatch': type = 'ninepatch'; break;
case 'circle': type = 'circle'; break;
case 'rounded-rect': type = 'rounded-rect'; break;
default: type = 'rect';
}
}
// 获取纹理 GUID | Get texture GUID
const textureGuid = uiRender?.textureGuid?.toString() ?? '';
// 转换颜色为十六进制字符串 | Convert color to hex string
const backgroundColor = uiRender?.backgroundColor !== undefined
? `#${uiRender.backgroundColor.toString(16).padStart(6, '0')}`
: undefined;
uiElements.push({
entityId: entity.id,
entityName: entity.name,
type,
x: uiTransform.x,
y: uiTransform.y,
width: uiTransform.width,
height: uiTransform.height,
worldX: uiTransform.worldX,
worldY: uiTransform.worldY,
rotation: uiTransform.rotation,
visible: uiTransform.visible && uiTransform.worldVisible,
alpha: uiTransform.worldAlpha,
sortingLayer: uiTransform.sortingLayer,
orderInLayer: uiTransform.orderInLayer,
textureGuid: textureGuid || undefined,
textureUrl: textureGuid ? this._resolveTextureUrl(textureGuid) : undefined,
backgroundColor,
text: uiText?.text,
fontSize: uiText?.fontSize,
});
}
return uiElements;
}
/**
* 导出调试数据为 JSON
* Export debug data as JSON
*/
exportAsJSON(): string {
return JSON.stringify({
exportTime: new Date().toISOString(),
snapshots: this._snapshots,
}, null, 2);
}
/**
* 打印当前粒子 UV 到控制台
* Print current particle UVs to console
*/
logParticleUVs(): void {
const snapshot = this.collectSnapshot();
if (!snapshot) {
console.log('[RenderDebugService] No scene available');
return;
}
console.group('[RenderDebugService] Particle UV Debug');
for (const ps of snapshot.particles) {
console.group(`${ps.entityName} (${ps.activeCount} active)`);
if (ps.textureSheetAnimation) {
console.log(`TextureSheetAnimation: ${ps.textureSheetAnimation.tilesX}x${ps.textureSheetAnimation.tilesY}`);
}
for (const p of ps.sampleParticles) {
console.log(` Particle ${p.index}: frame=${p.frame}, UV=[${p.uv.map(v => v.toFixed(3)).join(', ')}]`);
}
console.groupEnd();
}
console.groupEnd();
}
}
// 全局实例 | Global instance
export const renderDebugService = RenderDebugService.getInstance();
// 导出到全局以便控制台使用 | Export to global for console usage
if (typeof window !== 'undefined') {
(window as any).renderDebugService = renderDebugService;
}