feat: 纹理路径稳定 ID 与架构改进 (#305)
* feat(asset-system): 实现路径稳定 ID 生成器 使用 FNV-1a hash 算法为纹理生成稳定的运行时 ID: - 新增 _pathIdCache 静态缓存,跨 Play/Stop 循环保持稳定 - 新增 getStableIdForPath() 方法,相同路径永远返回相同 ID - 修改 loadTextureForComponent/loadTextureByGuid 使用稳定 ID - clearTextureMappings() 不再清除 _pathIdCache 这解决了 Play/Stop 后纹理 ID 失效的根本问题。 * fix(runtime-core): 移除 Play/Stop 循环中的 clearTextureMappings 调用 使用路径稳定 ID 后,不再需要在快照保存/恢复时清除纹理缓存: - saveSceneSnapshot() 移除 clearTextureMappings() 调用 - restoreSceneSnapshot() 移除 clearTextureMappings() 调用 - 组件保存的 textureId 在 Play/Stop 后仍然有效 * fix(editor-core): 修复场景切换时的资源泄漏 在 openScene() 加载新场景前先卸载旧场景资源: - 调用 sceneResourceManager.unloadSceneResources() 释放旧资源 - 使用引用计数机制,仅卸载不再被引用的资源 - 路径稳定 ID 缓存不受影响,保持 ID 稳定性 * fix(runtime-core): 修复 PluginManager 组件注册类型错误 将 ComponentRegistry 类改为 GlobalComponentRegistry 实例: - registerComponents() 期望 IComponentRegistry 接口实例 - GlobalComponentRegistry 是 ComponentRegistry 的全局实例 * refactor(core): 提取 IComponentRegistry 接口 将组件注册表抽象为接口,支持场景级组件注册: - 新增 IComponentRegistry 接口定义 - Scene 持有独立的 componentRegistry 实例 - 支持从 GlobalComponentRegistry 克隆 - 各系统支持传入自定义注册表 * refactor(engine-core): 改进插件服务注册机制 - 更新 IComponentRegistry 类型引用 - 优化 PluginServiceRegistry 服务管理 * refactor(modules): 适配新的组件注册接口 更新各模块 RuntimeModule 使用 IComponentRegistry 接口: - audio, behavior-tree, camera - sprite, tilemap, world-streaming * fix(physics-rapier2d): 修复物理插件组件注册 - PhysicsEditorPlugin 添加 runtimeModule 引用 - 适配 IComponentRegistry 接口 - 修复物理组件在场景加载时未注册的问题 * feat(editor-core): 添加 UserCodeService 就绪信号机制 - 新增 waitForReady()/signalReady() API - 支持等待用户脚本编译完成 - 解决场景加载时组件未注册的时序问题 * fix(editor-app): 在编译完成后调用 signalReady() 确保用户脚本编译完成后发出就绪信号: - 编译成功后调用 userCodeService.signalReady() - 编译失败也要发出信号,避免阻塞场景加载 * feat(editor-core): 改进编辑器核心服务 - EntityStoreService 添加调试日志 - AssetRegistryService 优化资产注册 - PluginManager 改进插件管理 - IFileAPI 添加 getFileMtime 接口 * feat(engine): 改进 Rust 纹理管理器 - 支持任意 ID 的纹理加载(非递增) - 添加纹理状态追踪 API - 优化纹理缓存清理机制 - 更新 TypeScript 绑定 * feat(ui): 添加场景切换和文本闪烁组件 新增组件: - SceneLoadTriggerComponent: 场景切换触发器 - TextBlinkComponent: 文本闪烁效果 新增系统: - SceneLoadTriggerSystem: 处理场景切换逻辑 - TextBlinkSystem: 处理文本闪烁动画 其他改进: - UIRuntimeModule 适配新组件注册接口 - UI 渲染系统优化 * feat(editor-app): 添加外部文件修改检测 - 新增 ExternalModificationDialog 组件 - TauriFileAPI 支持 getFileMtime - 场景文件被外部修改时提示用户 * feat(editor-app): 添加渲染调试面板 - 新增 RenderDebugService 和调试面板 UI - App/ContentBrowser 添加调试日志 - TitleBar/Viewport 优化 - DialogManager 改进 * refactor(editor-app): 编辑器服务和组件优化 - EngineService 改进引擎集成 - EditorEngineSync 同步优化 - AssetFileInspector 改进 - VectorFieldEditors 优化 - InstantiatePrefabCommand 改进 * feat(i18n): 更新国际化翻译 - 添加新功能相关翻译 - 更新中文、英文、西班牙文 * feat(tauri): 添加文件修改时间查询命令 - 新增 get_file_mtime 命令 - 支持检测文件外部修改 * refactor(particle): 粒子系统改进 - 适配新的组件注册接口 - ParticleSystem 优化 - 添加单元测试 * refactor(platform): 平台适配层优化 - BrowserRuntime 改进 - 新增 RuntimeSceneManager 服务 - 导出优化 * refactor(asset-system-editor): 资产元数据改进 - AssetMetaFile 优化 - 导出调整 * fix(asset-system): 移除未使用的 TextureLoader 导入 * fix(tests): 更新测试以使用 GlobalComponentRegistry 实例 修复多个测试文件以适配 ComponentRegistry 从静态类变为实例类的变更: - ComponentStorage.test.ts: 使用 GlobalComponentRegistry.reset() - EntitySerializer.test.ts: 使用 GlobalComponentRegistry 实例 - IncrementalSerialization.test.ts: 使用 GlobalComponentRegistry 实例 - SceneSerializer.test.ts: 使用 GlobalComponentRegistry 实例 - ComponentRegistry.extended.test.ts: 使用 GlobalComponentRegistry,同时注册到 scene.componentRegistry - SystemTypes.test.ts: 在 Scene 创建前注册组件 - QuerySystem.test.ts: mockScene 添加 componentRegistry
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Folder, File as FileIcon, Image as ImageIcon, Clock, HardDrive, Settings2 } from 'lucide-react';
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { Folder, File as FileIcon, Image as ImageIcon, Clock, HardDrive, Settings2, Grid3X3 } from 'lucide-react';
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { AssetRegistryService } from '@esengine/editor-core';
|
||||
import type { ISpriteSettings } from '@esengine/asset-system-editor';
|
||||
import { EngineService } from '../../../services/EngineService';
|
||||
import { AssetFileInfo } from '../types';
|
||||
import { ImagePreview, CodePreview, getLanguageFromExtension } from '../common';
|
||||
@@ -50,6 +51,165 @@ function formatDate(timestamp?: number): string {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sprite Settings Editor Component
|
||||
* 精灵设置编辑器组件
|
||||
*
|
||||
* Allows editing nine-patch slice borders for texture assets.
|
||||
* 允许编辑纹理资源的九宫格切片边框。
|
||||
*/
|
||||
interface SpriteSettingsEditorProps {
|
||||
filePath: string;
|
||||
imageSrc: string;
|
||||
initialSettings?: ISpriteSettings;
|
||||
onSettingsChange: (settings: ISpriteSettings) => void;
|
||||
}
|
||||
|
||||
function SpriteSettingsEditor({ filePath, imageSrc, initialSettings, onSettingsChange }: SpriteSettingsEditorProps) {
|
||||
const [sliceBorder, setSliceBorder] = useState<[number, number, number, number]>(
|
||||
initialSettings?.sliceBorder || [0, 0, 0, 0]
|
||||
);
|
||||
const [imageSize, setImageSize] = useState<{ width: number; height: number } | null>(null);
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
|
||||
// Sync sliceBorder state when initialSettings changes (async load)
|
||||
// 当 initialSettings 变化时同步 sliceBorder 状态(异步加载)
|
||||
useEffect(() => {
|
||||
if (initialSettings?.sliceBorder) {
|
||||
setSliceBorder(initialSettings.sliceBorder);
|
||||
}
|
||||
}, [initialSettings?.sliceBorder]);
|
||||
|
||||
// Load image to get dimensions
|
||||
// 加载图像以获取尺寸
|
||||
useEffect(() => {
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
setImageSize({ width: img.width, height: img.height });
|
||||
};
|
||||
img.src = imageSrc;
|
||||
}, [imageSrc]);
|
||||
|
||||
// Draw slice preview
|
||||
// 绘制切片预览
|
||||
useEffect(() => {
|
||||
if (!canvasRef.current || !imageSize) return;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
// Calculate scale to fit canvas
|
||||
// 计算缩放以适应画布
|
||||
const maxSize = 200;
|
||||
const scale = Math.min(maxSize / img.width, maxSize / img.height, 1);
|
||||
const displayWidth = img.width * scale;
|
||||
const displayHeight = img.height * scale;
|
||||
|
||||
canvas.width = displayWidth;
|
||||
canvas.height = displayHeight;
|
||||
|
||||
// Draw image
|
||||
// 绘制图像
|
||||
ctx.drawImage(img, 0, 0, displayWidth, displayHeight);
|
||||
|
||||
// Draw slice lines
|
||||
// 绘制切片线
|
||||
const [top, right, bottom, left] = sliceBorder;
|
||||
|
||||
ctx.strokeStyle = '#00ff00';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.setLineDash([4, 4]);
|
||||
|
||||
// Top line
|
||||
if (top > 0) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, top * scale);
|
||||
ctx.lineTo(displayWidth, top * scale);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Bottom line
|
||||
if (bottom > 0) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, displayHeight - bottom * scale);
|
||||
ctx.lineTo(displayWidth, displayHeight - bottom * scale);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Left line
|
||||
if (left > 0) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(left * scale, 0);
|
||||
ctx.lineTo(left * scale, displayHeight);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Right line
|
||||
if (right > 0) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(displayWidth - right * scale, 0);
|
||||
ctx.lineTo(displayWidth - right * scale, displayHeight);
|
||||
ctx.stroke();
|
||||
}
|
||||
};
|
||||
img.src = imageSrc;
|
||||
}, [imageSrc, imageSize, sliceBorder]);
|
||||
|
||||
const handleSliceChange = (index: number, value: number) => {
|
||||
const newSlice = [...sliceBorder] as [number, number, number, number];
|
||||
newSlice[index] = Math.max(0, value);
|
||||
setSliceBorder(newSlice);
|
||||
onSettingsChange({ ...initialSettings, sliceBorder: newSlice });
|
||||
};
|
||||
|
||||
const labels = ['Top', 'Right', 'Bottom', 'Left'];
|
||||
const labelsCN = ['上', '右', '下', '左'];
|
||||
|
||||
return (
|
||||
<div className="sprite-settings-editor">
|
||||
{/* Slice Preview Canvas */}
|
||||
<div style={{ marginBottom: '12px', textAlign: 'center' }}>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={{
|
||||
border: '1px solid #444',
|
||||
borderRadius: '4px',
|
||||
maxWidth: '100%'
|
||||
}}
|
||||
/>
|
||||
{imageSize && (
|
||||
<div style={{ fontSize: '11px', color: '#888', marginTop: '4px' }}>
|
||||
{imageSize.width} × {imageSize.height} px
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Slice Border Inputs */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px' }}>
|
||||
{sliceBorder.map((value, index) => (
|
||||
<div key={index} className="property-field" style={{ marginBottom: '0' }}>
|
||||
<label className="property-label" style={{ minWidth: '50px' }}>
|
||||
{labelsCN[index]} ({labels[index]})
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={value}
|
||||
onChange={(e) => handleSliceChange(index, parseInt(e.target.value) || 0)}
|
||||
min={0}
|
||||
max={imageSize ? (index % 2 === 0 ? imageSize.height : imageSize.width) : 9999}
|
||||
className="property-input property-input-number"
|
||||
style={{ width: '60px' }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AssetFileInspector({ fileInfo, content, isImage }: AssetFileInspectorProps) {
|
||||
const IconComponent = fileInfo.isDirectory ? Folder : isImage ? ImageIcon : FileIcon;
|
||||
const iconColor = fileInfo.isDirectory ? '#dcb67a' : isImage ? '#a78bfa' : '#90caf9';
|
||||
@@ -60,6 +220,10 @@ export function AssetFileInspector({ fileInfo, content, isImage }: AssetFileInsp
|
||||
const [detectedType, setDetectedType] = useState<string | null>(null);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
|
||||
// State for sprite settings (nine-patch borders)
|
||||
// 精灵设置状态(九宫格边框)
|
||||
const [spriteSettings, setSpriteSettings] = useState<ISpriteSettings | undefined>(undefined);
|
||||
|
||||
// Load meta info and available loader types
|
||||
useEffect(() => {
|
||||
if (fileInfo.isDirectory) return;
|
||||
@@ -76,6 +240,14 @@ export function AssetFileInspector({ fileInfo, content, isImage }: AssetFileInsp
|
||||
setCurrentLoaderType(meta.loaderType || null);
|
||||
setDetectedType(meta.type);
|
||||
|
||||
// Get sprite settings from meta (for texture assets)
|
||||
// 从 meta 获取精灵设置(用于纹理资源)
|
||||
if (meta.importSettings?.spriteSettings) {
|
||||
setSpriteSettings(meta.importSettings.spriteSettings as ISpriteSettings);
|
||||
} else {
|
||||
setSpriteSettings(undefined);
|
||||
}
|
||||
|
||||
// Get available loader types from assetManager
|
||||
const assetManager = EngineService.getInstance().getAssetManager();
|
||||
const loaderFactory = assetManager?.getLoaderFactory();
|
||||
@@ -117,6 +289,39 @@ export function AssetFileInspector({ fileInfo, content, isImage }: AssetFileInsp
|
||||
}
|
||||
}, [fileInfo.path, fileInfo.name, fileInfo.isDirectory, isUpdating]);
|
||||
|
||||
// Handle sprite settings change
|
||||
// 处理精灵设置更改
|
||||
const handleSpriteSettingsChange = useCallback(async (newSettings: ISpriteSettings) => {
|
||||
if (fileInfo.isDirectory || isUpdating) return;
|
||||
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
const assetRegistry = Core.services.tryResolve(AssetRegistryService) as AssetRegistryService | null;
|
||||
if (!assetRegistry?.isReady) return;
|
||||
|
||||
const metaManager = assetRegistry.metaManager;
|
||||
const meta = await metaManager.getOrCreateMeta(fileInfo.path);
|
||||
|
||||
// Update meta with new sprite settings
|
||||
// 使用新的精灵设置更新 meta
|
||||
const updatedImportSettings = {
|
||||
...meta.importSettings,
|
||||
spriteSettings: newSettings
|
||||
};
|
||||
|
||||
await metaManager.updateMeta(fileInfo.path, {
|
||||
importSettings: updatedImportSettings
|
||||
});
|
||||
|
||||
setSpriteSettings(newSettings);
|
||||
console.log(`[AssetFileInspector] Updated sprite settings for ${fileInfo.name}:`, newSettings);
|
||||
} catch (error) {
|
||||
console.error('Failed to update sprite settings:', error);
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
}, [fileInfo.path, fileInfo.name, fileInfo.isDirectory, isUpdating]);
|
||||
|
||||
return (
|
||||
<div className="entity-inspector">
|
||||
<div className="inspector-header">
|
||||
@@ -228,6 +433,23 @@ export function AssetFileInspector({ fileInfo, content, isImage }: AssetFileInsp
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sprite Settings Section - only for image files */}
|
||||
{/* 精灵设置部分 - 仅用于图像文件 */}
|
||||
{isImage && (
|
||||
<div className="inspector-section">
|
||||
<div className="section-title">
|
||||
<Grid3X3 size={14} style={{ verticalAlign: 'middle', marginRight: '4px' }} />
|
||||
九宫格设置 (Nine-Patch)
|
||||
</div>
|
||||
<SpriteSettingsEditor
|
||||
filePath={fileInfo.path}
|
||||
imageSrc={convertFileSrc(fileInfo.path)}
|
||||
initialSettings={spriteSettings}
|
||||
onSettingsChange={handleSpriteSettingsChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{content && (
|
||||
<div className="inspector-section code-preview-section">
|
||||
<div className="section-title">文件预览</div>
|
||||
|
||||
Reference in New Issue
Block a user