feat(asset): 统一资产引用使用 GUID 替代路径 (#287)

* feat(world-streaming): 添加世界流式加载系统

实现基于区块的世界流式加载系统,支持开放世界游戏:

运行时包 (@esengine/world-streaming):
- ChunkComponent: 区块实体组件,包含坐标、边界、状态
- StreamingAnchorComponent: 流式锚点组件(玩家/摄像机)
- ChunkLoaderComponent: 流式加载配置组件
- ChunkStreamingSystem: 区块加载/卸载调度系统
- ChunkCullingSystem: 区块可见性剔除系统
- ChunkManager: 区块生命周期管理服务
- SpatialHashGrid: 空间哈希网格
- ChunkSerializer: 区块序列化

编辑器包 (@esengine/world-streaming-editor):
- ChunkVisualizer: 区块可视化覆盖层
- ChunkLoaderInspectorProvider: 区块加载器检视器
- StreamingAnchorInspectorProvider: 流式锚点检视器
- WorldStreamingPlugin: 完整插件导出

* feat(asset): 统一资产引用使用 GUID 替代路径

将所有组件的资产引用字段从路径改为 GUID:
- SpriteComponent: texture -> textureGuid, material -> materialGuid
- SpriteAnimatorComponent: AnimationFrame.texture -> textureGuid
- UIRenderComponent: texture -> textureGuid
- UIButtonComponent: normalTexture -> normalTextureGuid 等
- AudioSourceComponent: clip -> clipGuid
- ParticleSystemComponent: 已使用 textureGuid

修复 AssetRegistryService 注册问题和路径规范化,
添加渲染系统的 GUID 解析支持。

* fix(sprite-editor): 更新 material 为 materialGuid

* fix(editor-app): 更新 AnimationFrame.texture 为 textureGuid
This commit is contained in:
YHH
2025-12-06 14:08:48 +08:00
committed by GitHub
parent 0c03b13d74
commit 3617f40309
25 changed files with 443 additions and 152 deletions

View File

@@ -1,11 +1,15 @@
import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework'; import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework';
@ECSComponent('AudioSource') @ECSComponent('AudioSource')
@Serializable({ version: 1, typeId: 'AudioSource' }) @Serializable({ version: 2, typeId: 'AudioSource' })
export class AudioSourceComponent extends Component { export class AudioSourceComponent extends Component {
/**
* 音频资产 GUID
* Audio clip asset GUID
*/
@Serialize() @Serialize()
@Property({ type: 'asset', label: 'Audio Clip', assetType: 'audio' }) @Property({ type: 'asset', label: 'Audio Clip', assetType: 'audio' })
clip: string = ''; clipGuid: string = '';
/** 范围 [0, 1] */ /** 范围 [0, 1] */
@Serialize() @Serialize()

View File

@@ -109,9 +109,9 @@ export class SpriteRenderHelper {
// Convert hex color string to packed RGBA // Convert hex color string to packed RGBA
const color = this.hexToPackedColor(sprite.color, sprite.alpha); const color = this.hexToPackedColor(sprite.color, sprite.alpha);
// Get material ID from path (0 = default if not found or no path specified) // Get material ID from GUID (0 = default if not found or no GUID specified)
const materialId = sprite.material const materialId = sprite.materialGuid
? getMaterialManager().getMaterialIdByPath(sprite.material) ? getMaterialManager().getMaterialIdByPath(sprite.materialGuid)
: 0; : 0;
// Collect material overrides if any // Collect material overrides if any

View File

@@ -10,6 +10,6 @@ export type { EngineBridgeConfig } from './core/EngineBridge';
export { RenderBatcher } from './core/RenderBatcher'; export { RenderBatcher } from './core/RenderBatcher';
export { SpriteRenderHelper } from './core/SpriteRenderHelper'; export { SpriteRenderHelper } from './core/SpriteRenderHelper';
export type { ITransformComponent } from './core/SpriteRenderHelper'; export type { ITransformComponent } from './core/SpriteRenderHelper';
export { EngineRenderSystem, type TransformComponentType, type IRenderDataProvider, type IUIRenderDataProvider, type GizmoDataProviderFn, type HasGizmoProviderFn, type ProviderRenderData } from './systems/EngineRenderSystem'; export { EngineRenderSystem, type TransformComponentType, type IRenderDataProvider, type IUIRenderDataProvider, type GizmoDataProviderFn, type HasGizmoProviderFn, type ProviderRenderData, type AssetPathResolverFn } from './systems/EngineRenderSystem';
export { CameraSystem } from './systems/CameraSystem'; export { CameraSystem } from './systems/CameraSystem';
export * from './types'; export * from './types';

View File

@@ -119,6 +119,18 @@ export type HasGizmoProviderFn = (component: Component) => boolean;
*/ */
export type TransformComponentType = ComponentType & (new (...args: any[]) => Component & ITransformComponent); export type TransformComponentType = ComponentType & (new (...args: any[]) => Component & ITransformComponent);
/**
* Asset path resolver function type.
* 资产路径解析器函数类型。
*
* Resolves GUID or path to actual file path for loading.
* 将 GUID 或路径解析为实际文件路径以进行加载。
*
* @param guidOrPath - Asset GUID or path | 资产 GUID 或路径
* @returns Resolved file path, or original value if cannot resolve | 解析后的文件路径,或无法解析时返回原值
*/
export type AssetPathResolverFn = (guidOrPath: string) => string;
/** /**
* ECS System for rendering sprites using the Rust engine. * ECS System for rendering sprites using the Rust engine.
* 使用Rust引擎渲染精灵的ECS系统。 * 使用Rust引擎渲染精灵的ECS系统。
@@ -177,6 +189,10 @@ export class EngineRenderSystem extends EntitySystem {
// UI 渲染数据提供者(支持屏幕空间和世界空间) // UI 渲染数据提供者(支持屏幕空间和世界空间)
private uiRenderDataProvider: IUIRenderDataProvider | null = null; private uiRenderDataProvider: IUIRenderDataProvider | null = null;
// Asset path resolver (injected from editor layer for GUID resolution)
// 资产路径解析器(从编辑器层注入,用于 GUID 解析)
private assetPathResolver: AssetPathResolverFn | null = null;
// Preview mode flag: when true, UI uses screen space overlay projection // Preview mode flag: when true, UI uses screen space overlay projection
// when false (editor mode), UI renders in world space following editor camera // when false (editor mode), UI renders in world space following editor camera
// 预览模式标志:为 true 时UI 使用屏幕空间叠加投影 // 预览模式标志:为 true 时UI 使用屏幕空间叠加投影
@@ -288,14 +304,24 @@ export class EngineRenderSystem extends EntitySystem {
// Use Rust engine's path-based texture loading for automatic caching // Use Rust engine's path-based texture loading for automatic caching
// 使用Rust引擎的基于路径的纹理加载实现自动缓存 // 使用Rust引擎的基于路径的纹理加载实现自动缓存
let textureId = 0; let textureId = 0;
if (sprite.texture) { const textureSource = sprite.getTextureSource();
textureId = this.bridge.getOrLoadTextureByPath(sprite.texture); if (textureSource) {
// Resolve GUID to path if resolver is available
// 如果有解析器,将 GUID 解析为路径
const texturePath = this.assetPathResolver
? this.assetPathResolver(textureSource)
: textureSource;
textureId = this.bridge.getOrLoadTextureByPath(texturePath);
} }
// Get material ID from path (0 = default if not found or no path specified) // Get material ID from GUID (0 = default if not found or no GUID specified)
// 从路径获取材质 ID0 = 默认,如果未找到或未指定路径 // 从 GUID 获取材质 ID0 = 默认,如果未找到或未指定 GUID
const materialId = sprite.material const materialGuidOrPath = sprite.materialGuid;
? getMaterialManager().getMaterialIdByPath(sprite.material) const materialPath = materialGuidOrPath && this.assetPathResolver
? this.assetPathResolver(materialGuidOrPath)
: materialGuidOrPath;
const materialId = materialPath
? getMaterialManager().getMaterialIdByPath(materialPath)
: 0; : 0;
// Collect material overrides if any // Collect material overrides if any
@@ -1159,4 +1185,28 @@ export class EngineRenderSystem extends EntitySystem {
loadTexture(id: number, url: string): void { loadTexture(id: number, url: string): void {
this.bridge.loadTexture(id, url); this.bridge.loadTexture(id, url);
} }
/**
* Set asset path resolver.
* 设置资产路径解析器。
*
* The resolver function is used to convert asset GUIDs to file paths.
* This allows the editor to inject AssetRegistryService functionality
* without creating a direct dependency.
* 解析器函数用于将资产 GUID 转换为文件路径。
* 这允许编辑器注入 AssetRegistryService 功能而不创建直接依赖。
*
* @param resolver - Function to resolve GUID/path to actual path | 将 GUID/路径解析为实际路径的函数
*/
setAssetPathResolver(resolver: AssetPathResolverFn | null): void {
this.assetPathResolver = resolver;
}
/**
* Get asset path resolver.
* 获取资产路径解析器。
*/
getAssetPathResolver(): AssetPathResolverFn | null {
return this.assetPathResolver;
}
} }

View File

@@ -38,7 +38,7 @@ import {
Settings Settings
} from 'lucide-react'; } from 'lucide-react';
import { Core } from '@esengine/ecs-framework'; import { Core } from '@esengine/ecs-framework';
import { MessageHub, FileActionRegistry, type FileCreationTemplate } from '@esengine/editor-core'; import { MessageHub, FileActionRegistry, AssetRegistryService, type FileCreationTemplate } from '@esengine/editor-core';
import { TauriAPI, DirectoryEntry } from '../api/tauri'; import { TauriAPI, DirectoryEntry } from '../api/tauri';
import { SettingsService } from '../services/SettingsService'; import { SettingsService } from '../services/SettingsService';
import { ContextMenu, ContextMenuItem } from './ContextMenu'; import { ContextMenu, ContextMenuItem } from './ContextMenu';
@@ -770,8 +770,19 @@ export class ${className} {
const parentPath = asset.path.substring(0, lastSlash); const parentPath = asset.path.substring(0, lastSlash);
const newPath = `${parentPath}/${newName}`; const newPath = `${parentPath}/${newName}`;
// Update AssetMetaManager to preserve GUID | 更新 AssetMetaManager 以保持 GUID 不变
const assetRegistry = Core.services.tryResolve(AssetRegistryService) as AssetRegistryService | null;
if (assetRegistry && asset.type !== 'folder') {
await assetRegistry.metaManager.handleAssetRename(asset.path, newPath);
}
await TauriAPI.renameFileOrFolder(asset.path, newPath); await TauriAPI.renameFileOrFolder(asset.path, newPath);
// Refresh asset registry | 刷新资产注册表
if (assetRegistry && asset.type !== 'folder') {
await assetRegistry.refreshAsset(newPath);
}
if (currentPath) { if (currentPath) {
await loadAssets(currentPath); await loadAssets(currentPath);
} }
@@ -1371,6 +1382,18 @@ export class ${className} {
if (asset.type === 'file') { if (asset.type === 'file') {
e.dataTransfer.setData('asset-path', asset.path); e.dataTransfer.setData('asset-path', asset.path);
e.dataTransfer.setData('text/plain', asset.path); e.dataTransfer.setData('text/plain', asset.path);
// Add GUID for new asset reference system
const assetRegistry = Core.services.tryResolve(AssetRegistryService) as AssetRegistryService | null;
if (assetRegistry) {
// Convert absolute path to relative path for GUID lookup
const relativePath = assetRegistry.absoluteToRelative(asset.path);
if (relativePath) {
const guid = assetRegistry.getGuidByPath(relativePath);
if (guid) {
e.dataTransfer.setData('asset-guid', guid);
}
}
}
} }
}} }}
> >

View File

@@ -5,7 +5,7 @@ import {
Save, Tag, Link, FileSearch, Globe, Package, Clipboard, RefreshCw, Settings Save, Tag, Link, FileSearch, Globe, Package, Clipboard, RefreshCw, Settings
} from 'lucide-react'; } from 'lucide-react';
import { TauriAPI, DirectoryEntry } from '../api/tauri'; import { TauriAPI, DirectoryEntry } from '../api/tauri';
import { MessageHub, FileActionRegistry } from '@esengine/editor-core'; import { MessageHub, FileActionRegistry, AssetRegistryService } from '@esengine/editor-core';
import { SettingsService } from '../services/SettingsService'; import { SettingsService } from '../services/SettingsService';
import { Core } from '@esengine/ecs-framework'; import { Core } from '@esengine/ecs-framework';
import { ContextMenu, ContextMenuItem } from './ContextMenu'; import { ContextMenu, ContextMenuItem } from './ContextMenu';
@@ -999,6 +999,19 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
e.dataTransfer.setData('asset-extension', ext || ''); e.dataTransfer.setData('asset-extension', ext || '');
e.dataTransfer.setData('text/plain', node.path); e.dataTransfer.setData('text/plain', node.path);
// Add GUID for new asset reference system
const assetRegistry = Core.services.tryResolve(AssetRegistryService) as AssetRegistryService | null;
if (assetRegistry) {
// Convert absolute path to relative path for GUID lookup
const relativePath = assetRegistry.absoluteToRelative(node.path);
if (relativePath) {
const guid = assetRegistry.getGuidByPath(relativePath);
if (guid) {
e.dataTransfer.setData('asset-guid', guid);
}
}
}
// 添加视觉反馈 // 添加视觉反馈
e.currentTarget.style.opacity = '0.5'; e.currentTarget.style.opacity = '0.5';
} }

View File

@@ -1,13 +1,14 @@
import React, { useState, useRef, useCallback, useEffect } from 'react'; import React, { useState, useRef, useCallback, useEffect, useMemo } from 'react';
import { Image, X, Navigation, ChevronDown, Copy } from 'lucide-react'; import { Image, X, Navigation, ChevronDown, Copy } from 'lucide-react';
import { convertFileSrc } from '@tauri-apps/api/core'; import { convertFileSrc } from '@tauri-apps/api/core';
import { Core } from '@esengine/ecs-framework'; import { Core } from '@esengine/ecs-framework';
import { ProjectService } from '@esengine/editor-core'; import { ProjectService, AssetRegistryService } from '@esengine/editor-core';
import { AssetPickerDialog } from '../../../components/dialogs/AssetPickerDialog'; import { AssetPickerDialog } from '../../../components/dialogs/AssetPickerDialog';
import './AssetField.css'; import './AssetField.css';
interface AssetFieldProps { interface AssetFieldProps {
label?: string; label?: string;
/** Value can be GUID or path (for backward compatibility) */
value: string | null; value: string | null;
onChange: (value: string | null) => void; onChange: (value: string | null) => void;
fileExtension?: string; fileExtension?: string;
@@ -17,6 +18,14 @@ interface AssetFieldProps {
onCreate?: () => void; onCreate?: () => void;
} }
/**
* Check if a string is a valid UUID v4 (GUID format)
*/
function isGUID(str: string): boolean {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidRegex.test(str);
}
export function AssetField({ export function AssetField({
label, label,
value, value,
@@ -32,6 +41,24 @@ export function AssetField({
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(null); const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(null);
const inputRef = useRef<HTMLDivElement>(null); const inputRef = useRef<HTMLDivElement>(null);
// Get AssetRegistryService for GUID ↔ Path conversion
const assetRegistry = useMemo(() => {
return Core.services.tryResolve(AssetRegistryService) as AssetRegistryService | null;
}, []);
// Resolve value to path (value can be GUID or path)
const resolvedPath = useMemo(() => {
if (!value) return null;
// If value is a GUID, resolve to path
if (isGUID(value) && assetRegistry) {
return assetRegistry.getPathByGuid(value) || null;
}
// Otherwise treat as path (backward compatibility)
return value;
}, [value, assetRegistry]);
// 检测是否是图片资源 // 检测是否是图片资源
const isImageAsset = useCallback((path: string | null) => { const isImageAsset = useCallback((path: string | null) => {
if (!path) return false; if (!path) return false;
@@ -40,18 +67,18 @@ export function AssetField({
); );
}, []); }, []);
// 加载缩略图 // 加载缩略图(使用 resolvedPath
useEffect(() => { useEffect(() => {
if (value && isImageAsset(value)) { if (resolvedPath && isImageAsset(resolvedPath)) {
// 获取项目路径并构建完整路径 // 获取项目路径并构建完整路径
const projectService = Core.services.tryResolve(ProjectService); const projectService = Core.services.tryResolve(ProjectService);
const projectPath = projectService?.getCurrentProject()?.path; const projectPath = projectService?.getCurrentProject()?.path;
if (projectPath) { if (projectPath) {
// 构建完整的文件路径 // 构建完整的文件路径
const fullPath = value.startsWith('/') || value.includes(':') const fullPath = resolvedPath.startsWith('/') || resolvedPath.includes(':')
? value ? resolvedPath
: `${projectPath}/${value}`; : `${projectPath}/${resolvedPath}`;
try { try {
const url = convertFileSrc(fullPath); const url = convertFileSrc(fullPath);
@@ -60,9 +87,9 @@ export function AssetField({
setThumbnailUrl(null); setThumbnailUrl(null);
} }
} else { } else {
// 没有项目路径时,尝试直接使用 value // 没有项目路径时,尝试直接使用 resolvedPath
try { try {
const url = convertFileSrc(value); const url = convertFileSrc(resolvedPath);
setThumbnailUrl(url); setThumbnailUrl(url);
} catch { } catch {
setThumbnailUrl(null); setThumbnailUrl(null);
@@ -71,7 +98,7 @@ export function AssetField({
} else { } else {
setThumbnailUrl(null); setThumbnailUrl(null);
} }
}, [value, isImageAsset]); }, [resolvedPath, isImageAsset]);
const handleDragEnter = useCallback((e: React.DragEvent) => { const handleDragEnter = useCallback((e: React.DragEvent) => {
e.preventDefault(); e.preventDefault();
@@ -99,27 +126,66 @@ export function AssetField({
if (readonly) return; if (readonly) return;
// Try to get GUID from drag data first
const assetGuid = e.dataTransfer.getData('asset-guid');
if (assetGuid && isGUID(assetGuid)) {
// Validate extension if needed
if (fileExtension && assetRegistry) {
const path = assetRegistry.getPathByGuid(assetGuid);
if (path && !path.endsWith(fileExtension)) {
return; // Extension mismatch
}
}
onChange(assetGuid);
return;
}
// Fallback: handle asset-path and convert to GUID
const assetPath = e.dataTransfer.getData('asset-path');
if (assetPath && (!fileExtension || assetPath.endsWith(fileExtension))) {
// Try to get GUID from path
if (assetRegistry) {
// Path might be absolute, convert to relative first
let relativePath = assetPath;
if (assetPath.includes(':') || assetPath.startsWith('/')) {
relativePath = assetRegistry.absoluteToRelative(assetPath) || assetPath;
}
const guid = assetRegistry.getGuidByPath(relativePath);
if (guid) {
onChange(guid);
return;
}
}
// Fallback to path if GUID not found (backward compatibility)
onChange(assetPath);
return;
}
// Handle file drops
const files = Array.from(e.dataTransfer.files); const files = Array.from(e.dataTransfer.files);
const file = files.find((f) => const file = files.find((f) =>
!fileExtension || f.name.endsWith(fileExtension) !fileExtension || f.name.endsWith(fileExtension)
); );
if (file) { if (file) {
// For file drops, we still use filename (need to register first)
onChange(file.name); onChange(file.name);
return; return;
} }
const assetPath = e.dataTransfer.getData('asset-path');
if (assetPath && (!fileExtension || assetPath.endsWith(fileExtension))) {
onChange(assetPath);
return;
}
const text = e.dataTransfer.getData('text/plain'); const text = e.dataTransfer.getData('text/plain');
if (text && (!fileExtension || text.endsWith(fileExtension))) { if (text && (!fileExtension || text.endsWith(fileExtension))) {
// Try to convert to GUID if it's a path
if (assetRegistry && !isGUID(text)) {
const guid = assetRegistry.getGuidByPath(text);
if (guid) {
onChange(guid);
return;
}
}
onChange(text); onChange(text);
} }
}, [onChange, fileExtension, readonly]); }, [onChange, fileExtension, readonly, assetRegistry]);
const handleBrowse = useCallback(() => { const handleBrowse = useCallback(() => {
if (readonly) return; if (readonly) return;
@@ -127,9 +193,24 @@ export function AssetField({
}, [readonly]); }, [readonly]);
const handlePickerSelect = useCallback((path: string) => { const handlePickerSelect = useCallback((path: string) => {
// Convert path to GUID if possible
if (assetRegistry) {
// Path might be absolute, convert to relative first
let relativePath = path;
if (path.includes(':') || path.startsWith('/')) {
relativePath = assetRegistry.absoluteToRelative(path) || path;
}
const guid = assetRegistry.getGuidByPath(relativePath);
if (guid) {
onChange(guid);
setShowPicker(false);
return;
}
}
// Fallback to path if GUID not found
onChange(path); onChange(path);
setShowPicker(false); setShowPicker(false);
}, [onChange]); }, [onChange, assetRegistry]);
const handleClear = useCallback(() => { const handleClear = useCallback(() => {
if (!readonly) { if (!readonly) {
@@ -137,11 +218,15 @@ export function AssetField({
} }
}, [onChange, readonly]); }, [onChange, readonly]);
const getFileName = (path: string) => { const getFileName = (path: string | null) => {
if (!path) return placeholder;
const parts = path.split(/[\\/]/); const parts = path.split(/[\\/]/);
return parts[parts.length - 1]; return parts[parts.length - 1];
}; };
// Display name uses resolvedPath
const displayName = resolvedPath ? getFileName(resolvedPath) : placeholder;
return ( return (
<div className="asset-field"> <div className="asset-field">
{label && <label className="asset-field__label">{label}</label>} {label && <label className="asset-field__label">{label}</label>}
@@ -166,16 +251,16 @@ export function AssetField({
{/* 下拉选择框 */} {/* 下拉选择框 */}
<div <div
ref={inputRef} ref={inputRef}
className={`asset-field__dropdown ${value ? 'has-value' : ''} ${isDragging ? 'dragging' : ''}`} className={`asset-field__dropdown ${resolvedPath ? 'has-value' : ''} ${isDragging ? 'dragging' : ''}`}
onClick={!readonly ? handleBrowse : undefined} onClick={!readonly ? handleBrowse : undefined}
onDragEnter={handleDragEnter} onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave} onDragLeave={handleDragLeave}
onDragOver={handleDragOver} onDragOver={handleDragOver}
onDrop={handleDrop} onDrop={handleDrop}
title={value || placeholder} title={resolvedPath || placeholder}
> >
<span className="asset-field__value"> <span className="asset-field__value">
{value ? getFileName(value) : placeholder} {displayName}
</span> </span>
<ChevronDown size={12} className="asset-field__dropdown-arrow" /> <ChevronDown size={12} className="asset-field__dropdown-arrow" />
</div> </div>
@@ -183,12 +268,12 @@ export function AssetField({
{/* 操作按钮行 */} {/* 操作按钮行 */}
<div className="asset-field__actions"> <div className="asset-field__actions">
{/* 定位按钮 */} {/* 定位按钮 */}
{value && onNavigate && ( {resolvedPath && onNavigate && (
<button <button
className="asset-field__btn" className="asset-field__btn"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onNavigate(value); onNavigate(resolvedPath);
}} }}
title="Locate in Asset Browser" title="Locate in Asset Browser"
> >
@@ -196,13 +281,13 @@ export function AssetField({
</button> </button>
)} )}
{/* 复制路径按钮 */} {/* 复制路径按钮 - copy path, not GUID */}
{value && ( {resolvedPath && (
<button <button
className="asset-field__btn" className="asset-field__btn"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
navigator.clipboard.writeText(value); navigator.clipboard.writeText(resolvedPath);
}} }}
title="Copy Path" title="Copy Path"
> >

View File

@@ -169,7 +169,7 @@ function AnimationClipsEditor({ label, clips, onChange, readonly, component, onD
const newClips = [...clips]; const newClips = [...clips];
const clip = newClips[clipIndex]; const clip = newClips[clipIndex];
if (!clip) return; if (!clip) return;
clip.frames = [...clip.frames, { texture: '', duration: 0.1 }]; clip.frames = [...clip.frames, { textureGuid: '', duration: 0.1 }];
onChange(newClips); onChange(newClips);
}; };
@@ -196,8 +196,8 @@ function AnimationClipsEditor({ label, clips, onChange, readonly, component, onD
const newClips = [...clips]; const newClips = [...clips];
const clip = newClips[clipIndex]; const clip = newClips[clipIndex];
if (!clip) return; if (!clip) return;
const newFrames = texturePaths.map((texture) => ({ const newFrames = texturePaths.map((textureGuid) => ({
texture, textureGuid,
duration: 0.1 duration: 0.1
})); }));
clip.frames = [...clip.frames, ...newFrames]; clip.frames = [...clip.frames, ...newFrames];
@@ -451,8 +451,8 @@ function AnimationClipsEditor({ label, clips, onChange, readonly, component, onD
<span className="frame-index">{frameIndex + 1}</span> <span className="frame-index">{frameIndex + 1}</span>
<div className="frame-texture-field"> <div className="frame-texture-field">
<AssetField <AssetField
value={frame.texture} value={frame.textureGuid}
onChange={(val) => updateFrame(clipIndex, frameIndex, { texture: val || '' })} onChange={(val) => updateFrame(clipIndex, frameIndex, { textureGuid: val || '' })}
fileExtension=".png" fileExtension=".png"
placeholder="Texture..." placeholder="Texture..."
readonly={readonly} readonly={readonly}

View File

@@ -20,16 +20,20 @@ const logger = createLogger('AssetMetaPlugin');
class AssetMetaEditorModule implements IEditorModuleLoader { class AssetMetaEditorModule implements IEditorModuleLoader {
private _assetRegistry: AssetRegistryService | null = null; private _assetRegistry: AssetRegistryService | null = null;
async install(_services: ServiceContainer): Promise<void> { async install(services: ServiceContainer): Promise<void> {
// 创建 AssetRegistryService 并初始化 // 创建 AssetRegistryService 并初始化
// Create AssetRegistryService and initialize // Create AssetRegistryService and initialize
this._assetRegistry = new AssetRegistryService(); this._assetRegistry = new AssetRegistryService();
// 注册到服务容器,以便其他地方可以访问
// Register to service container so other places can access it
services.registerInstance(AssetRegistryService, this._assetRegistry);
// 初始化服务(订阅 project:opened 事件) // 初始化服务(订阅 project:opened 事件)
// Initialize service (subscribes to project:opened event) // Initialize service (subscribes to project:opened event)
await this._assetRegistry.initialize(); await this._assetRegistry.initialize();
logger.info('AssetRegistryService initialized'); logger.info('AssetRegistryService initialized and registered');
} }
async uninstall(): Promise<void> { async uninstall(): Promise<void> {

View File

@@ -155,9 +155,9 @@ export class EditorEngineSync {
if (bridge) { if (bridge) {
for (const clip of animator.clips) { for (const clip of animator.clips) {
for (const frame of clip.frames) { for (const frame of clip.frames) {
if (frame.texture) { if (frame.textureGuid) {
// Trigger texture loading // Trigger texture loading
bridge.getOrLoadTextureByPath(frame.texture); bridge.getOrLoadTextureByPath(frame.textureGuid);
} }
} }
} }
@@ -168,8 +168,8 @@ export class EditorEngineSync {
const firstClip = animator.clips[0]; const firstClip = animator.clips[0];
if (firstClip && firstClip.frames && firstClip.frames.length > 0) { if (firstClip && firstClip.frames && firstClip.frames.length > 0) {
const firstFrame = firstClip.frames[0]; const firstFrame = firstClip.frames[0];
if (firstFrame && firstFrame.texture && spriteComponent) { if (firstFrame && firstFrame.textureGuid && spriteComponent) {
spriteComponent.texture = firstFrame.texture; spriteComponent.textureGuid = firstFrame.textureGuid;
} }
} }
} }
@@ -228,8 +228,8 @@ export class EditorEngineSync {
// Preload all frame textures // Preload all frame textures
for (const clip of animator.clips) { for (const clip of animator.clips) {
for (const frame of clip.frames) { for (const frame of clip.frames) {
if (frame.texture) { if (frame.textureGuid) {
bridge.getOrLoadTextureByPath(frame.texture); bridge.getOrLoadTextureByPath(frame.textureGuid);
} }
} }
} }
@@ -240,8 +240,8 @@ export class EditorEngineSync {
const firstClip = animator.clips[0]; const firstClip = animator.clips[0];
if (firstClip && firstClip.frames && firstClip.frames.length > 0) { if (firstClip && firstClip.frames && firstClip.frames.length > 0) {
const firstFrame = firstClip.frames[0]; const firstFrame = firstClip.frames[0];
if (firstFrame && firstFrame.texture) { if (firstFrame && firstFrame.textureGuid) {
sprite.texture = firstFrame.texture; sprite.textureGuid = firstFrame.textureGuid;
} }
} }
} }

View File

@@ -6,7 +6,7 @@
* Uses the unified GameRuntime architecture * Uses the unified GameRuntime architecture
*/ */
import { GizmoRegistry, EntityStoreService, MessageHub, SceneManagerService, ProjectService, PluginManager, IPluginManager, type SystemContext } from '@esengine/editor-core'; import { GizmoRegistry, EntityStoreService, MessageHub, SceneManagerService, ProjectService, PluginManager, IPluginManager, AssetRegistryService, type SystemContext } from '@esengine/editor-core';
import { Core, Scene, Entity, SceneSerializer, ProfilerSDK } from '@esengine/ecs-framework'; import { Core, Scene, Entity, SceneSerializer, ProfilerSDK } from '@esengine/ecs-framework';
import { CameraConfig } from '@esengine/ecs-engine-bindgen'; import { CameraConfig } from '@esengine/ecs-engine-bindgen';
import { TransformComponent } from '@esengine/engine-core'; import { TransformComponent } from '@esengine/engine-core';
@@ -148,6 +148,10 @@ export class EngineService {
// 初始化资产系统 // 初始化资产系统
await this._initializeAssetSystem(); await this._initializeAssetSystem();
// 设置资产路径解析器(用于 GUID 到路径的转换)
// Set asset path resolver (for GUID to path conversion)
this._setupAssetPathResolver();
// 同步视口尺寸 // 同步视口尺寸
const canvas = document.getElementById(canvasId) as HTMLCanvasElement; const canvas = document.getElementById(canvasId) as HTMLCanvasElement;
if (canvas && canvas.parentElement) { if (canvas && canvas.parentElement) {
@@ -339,8 +343,8 @@ export class EngineService {
const firstClip = animator.clips[0]; const firstClip = animator.clips[0];
if (firstClip && firstClip.frames && firstClip.frames.length > 0) { if (firstClip && firstClip.frames && firstClip.frames.length > 0) {
const firstFrame = firstClip.frames[0]; const firstFrame = firstClip.frames[0];
if (firstFrame && firstFrame.texture) { if (firstFrame && firstFrame.textureGuid) {
sprite.texture = firstFrame.texture; sprite.textureGuid = firstFrame.textureGuid;
} }
} }
} }
@@ -427,6 +431,59 @@ export class EngineService {
} }
} }
/**
* Setup asset path resolver for EngineRenderSystem.
* 为 EngineRenderSystem 设置资产路径解析器。
*
* This enables GUID-based asset references. When a component stores a GUID,
* the resolver converts it to an actual file path for loading.
* 这启用了基于 GUID 的资产引用。当组件存储 GUID 时,
* 解析器将其转换为实际文件路径以进行加载。
*/
private _setupAssetPathResolver(): void {
const renderSystem = this._runtime?.renderSystem;
if (!renderSystem) return;
// UUID v4 regex for GUID detection
// UUID v4 正则表达式用于 GUID 检测
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
renderSystem.setAssetPathResolver((guidOrPath: string): string => {
// Skip if already a valid URL
// 如果已经是有效的 URL 则跳过
if (!guidOrPath || guidOrPath.startsWith('http') || guidOrPath.startsWith('asset://') || guidOrPath.startsWith('data:')) {
return guidOrPath;
}
// Check if this is a GUID
// 检查是否为 GUID
if (uuidRegex.test(guidOrPath)) {
const assetRegistry = Core.services.tryResolve(AssetRegistryService) as AssetRegistryService | null;
if (assetRegistry) {
const relativePath = assetRegistry.getPathByGuid(guidOrPath);
if (relativePath) {
// Convert relative path to absolute
// 将相对路径转换为绝对路径
const absolutePath = assetRegistry.relativeToAbsolute(relativePath);
if (absolutePath) {
// Convert to Tauri asset URL for WebView loading
// 转换为 Tauri 资产 URL 以便 WebView 加载
return convertFileSrc(absolutePath);
}
return relativePath;
}
}
// GUID not found, return original value
// 未找到 GUID返回原值
return guidOrPath;
}
// Not a GUID, treat as file path and convert
// 不是 GUID当作文件路径处理并转换
return convertFileSrc(guidOrPath);
});
}
/** /**
* Create entity with sprite and transform. * Create entity with sprite and transform.
*/ */

View File

@@ -139,9 +139,13 @@ class SimpleAssetDatabase {
private readonly _typeToGuids = new Map<AssetRegistryType, Set<AssetGUID>>(); private readonly _typeToGuids = new Map<AssetRegistryType, Set<AssetGUID>>();
addAsset(metadata: IAssetRegistryMetadata): void { addAsset(metadata: IAssetRegistryMetadata): void {
const { guid, path, type } = metadata; const { guid, type } = metadata;
this._metadata.set(guid, metadata); // Normalize path separators for consistent storage
this._pathToGuid.set(path, guid); const normalizedPath = metadata.path.replace(/\\/g, '/');
const normalizedMetadata = { ...metadata, path: normalizedPath };
this._metadata.set(guid, normalizedMetadata);
this._pathToGuid.set(normalizedPath, guid);
if (!this._typeToGuids.has(type)) { if (!this._typeToGuids.has(type)) {
this._typeToGuids.set(type, new Set()); this._typeToGuids.set(type, new Set());
@@ -154,6 +158,7 @@ class SimpleAssetDatabase {
if (!metadata) return; if (!metadata) return;
this._metadata.delete(guid); this._metadata.delete(guid);
// Path is already normalized when stored
this._pathToGuid.delete(metadata.path); this._pathToGuid.delete(metadata.path);
const typeSet = this._typeToGuids.get(metadata.type); const typeSet = this._typeToGuids.get(metadata.type);
@@ -167,7 +172,9 @@ class SimpleAssetDatabase {
} }
getMetadataByPath(path: string): IAssetRegistryMetadata | undefined { getMetadataByPath(path: string): IAssetRegistryMetadata | undefined {
const guid = this._pathToGuid.get(path); // Normalize path separators for consistent lookup
const normalizedPath = path.replace(/\\/g, '/');
const guid = this._pathToGuid.get(normalizedPath);
return guid ? this._metadata.get(guid) : undefined; return guid ? this._metadata.get(guid) : undefined;
} }
@@ -638,6 +645,11 @@ export class AssetRegistryService {
*/ */
getGuidByPath(relativePath: string): AssetGUID | undefined { getGuidByPath(relativePath: string): AssetGUID | undefined {
const metadata = this._database.getMetadataByPath(relativePath); const metadata = this._database.getMetadataByPath(relativePath);
if (!metadata) {
// Debug: show registered paths if not found
const stats = this._database.getStatistics();
logger.debug(`[AssetRegistry] GUID not found for path: "${relativePath}", total assets: ${stats.totalAssets}`);
}
return metadata?.guid; return metadata?.guid;
} }
@@ -776,4 +788,13 @@ export class AssetRegistryService {
get projectPath(): string | null { get projectPath(): string | null {
return this._projectPath; return this._projectPath;
} }
/**
* Dispose the service
*/
dispose(): void {
this._unsubscribeFromFileChanges();
this.unloadProject();
this._initialized = false;
}
} }

View File

@@ -54,9 +54,25 @@ export enum SimulationSpace {
* 管理粒子发射、模拟,并为渲染提供数据。 * 管理粒子发射、模拟,并为渲染提供数据。
*/ */
@ECSComponent('ParticleSystem') @ECSComponent('ParticleSystem')
@Serializable({ version: 1, typeId: 'ParticleSystem' }) @Serializable({ version: 2, typeId: 'ParticleSystem' })
export class ParticleSystemComponent extends Component { export class ParticleSystemComponent extends Component {
// ============= 资产引用 | Asset Reference =============
/**
* 粒子效果资产 GUID
* Particle effect asset GUID
*
* When set, loads particle configuration from .particle file.
* Inline properties below are ignored when asset is set.
* 设置后从 .particle 文件加载粒子配置。
* 设置了资产后,下面的内联属性将被忽略。
*/
@Serialize()
@Property({ type: 'asset', label: 'Particle Asset', extensions: ['.particle', '.particle.json'] })
public particleAssetGuid: string = '';
// ============= 基础属性 | Basic Properties ============= // ============= 基础属性 | Basic Properties =============
// These are used when particleAssetGuid is not set
/** 最大粒子数量 | Maximum particle count */ /** 最大粒子数量 | Maximum particle count */
@Serialize() @Serialize()
@@ -200,10 +216,13 @@ export class ParticleSystemComponent extends Component {
// ============= 渲染属性 | Rendering Properties ============= // ============= 渲染属性 | Rendering Properties =============
/** 粒子纹理 | Particle texture */ /**
* 粒子纹理 GUID
* Particle texture GUID
*/
@Serialize() @Serialize()
@Property({ type: 'asset', label: 'Texture', assetType: 'texture' }) @Property({ type: 'asset', label: 'Texture', assetType: 'texture' })
public texture: string = ''; public textureGuid: string = '';
/** 粒子尺寸(像素)| Particle size (pixels) */ /** 粒子尺寸(像素)| Particle size (pixels) */
@Serialize() @Serialize()

View File

@@ -108,8 +108,8 @@ export interface IParticleAsset {
blendMode: ParticleBlendMode; blendMode: ParticleBlendMode;
/** 排序顺序 | Sorting order */ /** 排序顺序 | Sorting order */
sortingOrder: number; sortingOrder: number;
/** 纹理路径 | Texture path */ /** 纹理资产 GUID | Texture asset GUID */
texture?: string; textureGuid?: string;
// 模块配置 | Module configurations // 模块配置 | Module configurations
/** 模块列表 | Module list */ /** 模块列表 | Module list */

View File

@@ -191,7 +191,7 @@ export class ParticleRenderDataProvider implements IRenderDataProvider {
colors: this._colors.subarray(0, particleIndex), colors: this._colors.subarray(0, particleIndex),
tileCount: particleIndex, tileCount: particleIndex,
sortingOrder, sortingOrder,
texturePath: systems[0]?.component.texture || undefined texturePath: systems[0]?.component.textureGuid || undefined
}; };
this._renderDataCache.push(renderData); this._renderDataCache.push(renderData);

View File

@@ -65,7 +65,7 @@ export class Canvas2DRenderSystem extends EntitySystem {
this.ctx.translate(x, y); this.ctx.translate(x, y);
this.ctx.rotate(rotation); this.ctx.rotate(rotation);
const texture = this.textureCache.get(sprite.texture || ''); const texture = this.textureCache.get(sprite.textureGuid || '');
if (texture) { if (texture) {
this.ctx.drawImage(texture, -width / 2, -height / 2, width, height); this.ctx.drawImage(texture, -width / 2, -height / 2, width, height);
} else { } else {

View File

@@ -626,7 +626,7 @@ function MaterialOverrideEditor({ sprite, material, onChange }: MaterialOverride
onChange('materialOverrides', newOverrides); onChange('materialOverrides', newOverrides);
}; };
if (!sprite.material) { if (!sprite.materialGuid) {
return null; return null;
} }
@@ -871,10 +871,10 @@ function SpriteInspectorContent({ context }: { context: ComponentInspectorContex
const [material, setMaterial] = useState<Material | null>(null); const [material, setMaterial] = useState<Material | null>(null);
const [, forceUpdate] = useState({}); const [, forceUpdate] = useState({});
// Load material when sprite.material changes. // Load material when sprite.materialGuid changes.
// 当 sprite.material 变化时加载材质。 // 当 sprite.materialGuid 变化时加载材质。
useEffect(() => { useEffect(() => {
if (!sprite.material) { if (!sprite.materialGuid) {
setMaterial(null); setMaterial(null);
return; return;
} }
@@ -887,7 +887,7 @@ function SpriteInspectorContent({ context }: { context: ComponentInspectorContex
// Try to get cached material by ID. // Try to get cached material by ID.
// 尝试通过 ID 获取缓存的材质。 // 尝试通过 ID 获取缓存的材质。
const materialId = materialManager.getMaterialIdByPath(sprite.material); const materialId = materialManager.getMaterialIdByPath(sprite.materialGuid);
if (materialId > 0) { if (materialId > 0) {
const mat = materialManager.getMaterial(materialId); const mat = materialManager.getMaterial(materialId);
setMaterial(mat || null); setMaterial(mat || null);
@@ -896,7 +896,7 @@ function SpriteInspectorContent({ context }: { context: ComponentInspectorContex
// Load material asynchronously. // Load material asynchronously.
// 异步加载材质。 // 异步加载材质。
materialManager.loadMaterialFromPath(sprite.material) materialManager.loadMaterialFromPath(sprite.materialGuid)
.then(matId => { .then(matId => {
const mat = materialManager.getMaterial(matId); const mat = materialManager.getMaterial(matId);
setMaterial(mat || null); setMaterial(mat || null);
@@ -904,7 +904,7 @@ function SpriteInspectorContent({ context }: { context: ComponentInspectorContex
.catch(() => { .catch(() => {
setMaterial(null); setMaterial(null);
}); });
}, [sprite.material]); }, [sprite.materialGuid]);
const handleChange = useCallback((propertyName: string, value: unknown) => { const handleChange = useCallback((propertyName: string, value: unknown) => {
(sprite as unknown as Record<string, unknown>)[propertyName] = value; (sprite as unknown as Record<string, unknown>)[propertyName] = value;
@@ -930,7 +930,7 @@ function SpriteInspectorContent({ context }: { context: ComponentInspectorContex
}, []); }, []);
// No material selected // No material selected
if (!sprite.material) { if (!sprite.materialGuid) {
return null; return null;
} }
@@ -940,7 +940,7 @@ function SpriteInspectorContent({ context }: { context: ComponentInspectorContex
{material && ( {material && (
<InlineMaterialEditor <InlineMaterialEditor
material={material} material={material}
materialPath={sprite.material} materialPath={sprite.materialGuid}
onMaterialChange={handleMaterialChange} onMaterialChange={handleMaterialChange}
/> />
)} )}

View File

@@ -5,8 +5,11 @@ import { Component, ECSComponent, Serializable, Serialize, Property } from '@ese
* Animation frame data * Animation frame data
*/ */
export interface AnimationFrame { export interface AnimationFrame {
/** 纹理路径 | Texture path */ /**
texture: string; * 纹理资产 GUID
* Texture asset GUID
*/
textureGuid: string;
/** 帧持续时间(秒) | Frame duration in seconds */ /** 帧持续时间(秒) | Frame duration in seconds */
duration: number; duration: number;
/** UV坐标 [u0, v0, u1, v1] | UV coordinates */ /** UV坐标 [u0, v0, u1, v1] | UV coordinates */
@@ -43,7 +46,7 @@ export class SpriteAnimatorComponent extends Component {
@Property({ @Property({
type: 'animationClips', type: 'animationClips',
label: 'Animation Clips', label: 'Animation Clips',
controls: [{ component: 'Sprite', property: 'texture' }] controls: [{ component: 'Sprite', property: 'textureGuid' }]
}) })
public clips: AnimationClip[] = []; public clips: AnimationClip[] = [];
@@ -101,7 +104,7 @@ export class SpriteAnimatorComponent extends Component {
* Create animation clip from sprite atlas * Create animation clip from sprite atlas
* *
* @param name - 动画名称 | Animation name * @param name - 动画名称 | Animation name
* @param texture - 纹理路径 | Texture path * @param textureGuid - 纹理资产 GUID | Texture asset GUID
* @param frameCount - 帧数 | Number of frames * @param frameCount - 帧数 | Number of frames
* @param frameWidth - 每帧宽度 | Frame width * @param frameWidth - 每帧宽度 | Frame width
* @param frameHeight - 每帧高度 | Frame height * @param frameHeight - 每帧高度 | Frame height
@@ -112,7 +115,7 @@ export class SpriteAnimatorComponent extends Component {
*/ */
createClipFromAtlas( createClipFromAtlas(
name: string, name: string,
texture: string, textureGuid: string,
frameCount: number, frameCount: number,
frameWidth: number, frameWidth: number,
frameHeight: number, frameHeight: number,
@@ -132,7 +135,7 @@ export class SpriteAnimatorComponent extends Component {
const y = row * frameHeight; const y = row * frameHeight;
frames.push({ frames.push({
texture, textureGuid,
duration, duration,
uv: [ uv: [
x / atlasWidth, x / atlasWidth,
@@ -159,19 +162,19 @@ export class SpriteAnimatorComponent extends Component {
* Create animation clip from frame sequence * Create animation clip from frame sequence
* *
* @param name - 动画名称 | Animation name * @param name - 动画名称 | Animation name
* @param textures - 纹理路径数组 | Array of texture paths * @param textureGuids - 纹理资产 GUID 数组 | Array of texture asset GUIDs
* @param fps - 帧率 | Frames per second * @param fps - 帧率 | Frames per second
* @param loop - 是否循环 | Whether to loop * @param loop - 是否循环 | Whether to loop
*/ */
createClipFromSequence( createClipFromSequence(
name: string, name: string,
textures: string[], textureGuids: string[],
fps: number = 12, fps: number = 12,
loop: boolean = true loop: boolean = true
): AnimationClip { ): AnimationClip {
const duration = 1 / fps; const duration = 1 / fps;
const frames: AnimationFrame[] = textures.map((texture) => ({ const frames: AnimationFrame[] = textureGuids.map((textureGuid) => ({
texture, textureGuid,
duration duration
})); }));

View File

@@ -27,19 +27,20 @@ export type MaterialOverrides = Record<string, MaterialPropertyOverride>;
* Sprite component - manages 2D image rendering * Sprite component - manages 2D image rendering
*/ */
@ECSComponent('Sprite') @ECSComponent('Sprite')
@Serializable({ version: 3, typeId: 'Sprite' }) @Serializable({ version: 4, typeId: 'Sprite' })
export class SpriteComponent extends Component { export class SpriteComponent extends Component {
/** 纹理路径或资源ID | Texture path or asset ID */
@Serialize()
@Property({ type: 'asset', label: 'Texture', assetType: 'texture' })
public texture: string = '';
/** /**
* 资产GUID(新的资产系统) * 纹理资产 GUID
* Asset GUID for new asset system * Texture asset GUID
*
* Stores the unique identifier of the texture asset.
* The actual file path is resolved at runtime via AssetDatabase.
* 存储纹理资产的唯一标识符。
* 实际文件路径在运行时通过 AssetDatabase 解析。
*/ */
@Serialize() @Serialize()
public assetGuid?: string; @Property({ type: 'asset', label: 'Texture', assetType: 'texture' })
public textureGuid: string = '';
/** /**
* 纹理ID运行时使用 * 纹理ID运行时使用
@@ -151,15 +152,15 @@ export class SpriteComponent extends Component {
public sortingOrder: number = 0; public sortingOrder: number = 0;
/** /**
* 材质资产路径(共享材质) * 材质资产 GUID(共享材质)
* Material asset path (shared material) * Material asset GUID (shared material)
* *
* Multiple sprites can reference the same material file. * Multiple sprites can reference the same material file.
* 多个精灵可以引用同一个材质文件。 * 多个精灵可以引用同一个材质文件。
*/ */
@Serialize() @Serialize()
@Property({ type: 'asset', label: 'Material', extensions: ['.mat'] }) @Property({ type: 'asset', label: 'Material', extensions: ['.mat'] })
public material: string = ''; public materialGuid: string = '';
/** /**
* 材质属性覆盖(实例级别) * 材质属性覆盖(实例级别)
@@ -215,9 +216,13 @@ export class SpriteComponent extends Component {
this.originY = value; this.originY = value;
} }
constructor(texture: string = '') { /**
* @param textureGuidOrPath - Texture GUID or path (for backward compatibility)
*/
constructor(textureGuidOrPath: string = '') {
super(); super();
this.texture = texture; // Support both GUID and path for backward compatibility
this.textureGuid = textureGuidOrPath;
} }
/** /**
@@ -260,7 +265,7 @@ export class SpriteComponent extends Component {
} }
this._assetReference = reference; this._assetReference = reference;
if (reference) { if (reference) {
this.assetGuid = reference.guid; this.textureGuid = reference.guid;
} }
} }
@@ -292,11 +297,11 @@ export class SpriteComponent extends Component {
} }
/** /**
* 获取有效的纹理源 * 获取纹理 GUID
* Get effective texture source * Get texture GUID
*/ */
getTextureSource(): string { getTextureSource(): string {
return this.assetGuid || this.texture; return this.textureGuid;
} }
// ============= Material Override Methods ============= // ============= Material Override Methods =============

View File

@@ -51,7 +51,7 @@ export class SpriteAnimatorSystem extends EntitySystem {
if (sprite) { if (sprite) {
const frame = animator.getCurrentFrame(); const frame = animator.getCurrentFrame();
if (frame) { if (frame) {
sprite.texture = frame.texture; sprite.textureGuid = frame.textureGuid;
// Update UV if specified // Update UV if specified
if (frame.uv) { if (frame.uv) {

View File

@@ -61,7 +61,8 @@ export interface UITextConfig extends UIBaseConfig {
* Image configuration * Image configuration
*/ */
export interface UIImageConfig extends UIBaseConfig { export interface UIImageConfig extends UIBaseConfig {
texture: string | number; /** 纹理资产 GUID 或运行时 ID | Texture asset GUID or runtime ID */
textureGuid: string | number;
tint?: number; tint?: number;
} }
@@ -236,7 +237,7 @@ export class UIBuilder {
const render = entity.addComponent(new UIRenderComponent()); const render = entity.addComponent(new UIRenderComponent());
render.type = UIRenderType.Image; render.type = UIRenderType.Image;
render.texture = config.texture; render.textureGuid = config.textureGuid;
render.textureTint = config.tint ?? 0xFFFFFF; render.textureTint = config.tint ?? 0xFFFFFF;
return entity; return entity;

View File

@@ -96,12 +96,12 @@ export class UIRenderComponent extends Component {
// ===== 纹理 Texture ===== // ===== 纹理 Texture =====
/** /**
* 纹理路径或 ID * 纹理资产 GUID 或运行时 ID
* Texture path or runtime ID * Texture asset GUID or runtime ID
*/ */
@Serialize() @Serialize()
@Property({ type: 'asset', label: 'Texture', assetType: 'texture' }) @Property({ type: 'asset', label: 'Texture', assetType: 'texture' })
public texture: string | number | null = null; public textureGuid: string | number | null = null;
/** /**
* 纹理 UV 坐标 (用于图集) * 纹理 UV 坐标 (用于图集)
@@ -230,20 +230,25 @@ export class UIRenderComponent extends Component {
/** /**
* 设置图片 * 设置图片
* Set image texture * Set image texture
*
* @param textureGuid - 纹理资产 GUID | Texture asset GUID
*/ */
public setImage(texture: string | number): this { public setImage(textureGuid: string | number): this {
this.type = UIRenderType.Image; this.type = UIRenderType.Image;
this.texture = texture; this.textureGuid = textureGuid;
return this; return this;
} }
/** /**
* 设置九宫格 * 设置九宫格
* Set nine-patch image * Set nine-patch image
*
* @param textureGuid - 纹理资产 GUID | Texture asset GUID
* @param margins - 九宫格边距 | Nine-patch margins
*/ */
public setNinePatch(texture: string | number, margins: [number, number, number, number]): this { public setNinePatch(textureGuid: string | number, margins: [number, number, number, number]): this {
this.type = UIRenderType.NinePatch; this.type = UIRenderType.NinePatch;
this.texture = texture; this.textureGuid = textureGuid;
this.ninePatchMargins = margins; this.ninePatchMargins = margins;
return this; return this;
} }

View File

@@ -10,7 +10,8 @@ export interface UIButtonStyle {
textColor: number; textColor: number;
borderColor: number; borderColor: number;
borderWidth: number; borderWidth: number;
texture?: string; /** 纹理资产 GUID | Texture asset GUID */
textureGuid?: string;
} }
/** /**
@@ -51,36 +52,36 @@ export class UIButtonComponent extends Component {
// ===== 状态纹理 State Textures ===== // ===== 状态纹理 State Textures =====
/** /**
* 正常状态纹理 * 正常状态纹理 GUID
* Normal state texture * Normal state texture GUID
*/ */
@Serialize() @Serialize()
@Property({ type: 'asset', label: 'Normal Texture', assetType: 'texture' }) @Property({ type: 'asset', label: 'Normal Texture', assetType: 'texture' })
public normalTexture: string = ''; public normalTextureGuid: string = '';
/** /**
* 悬停状态纹理 * 悬停状态纹理 GUID
* Hover state texture * Hover state texture GUID
*/ */
@Serialize() @Serialize()
@Property({ type: 'asset', label: 'Hover Texture', assetType: 'texture' }) @Property({ type: 'asset', label: 'Hover Texture', assetType: 'texture' })
public hoverTexture: string = ''; public hoverTextureGuid: string = '';
/** /**
* 按下状态纹理 * 按下状态纹理 GUID
* Pressed state texture * Pressed state texture GUID
*/ */
@Serialize() @Serialize()
@Property({ type: 'asset', label: 'Pressed Texture', assetType: 'texture' }) @Property({ type: 'asset', label: 'Pressed Texture', assetType: 'texture' })
public pressedTexture: string = ''; public pressedTextureGuid: string = '';
/** /**
* 禁用状态纹理 * 禁用状态纹理 GUID
* Disabled state texture * Disabled state texture GUID
*/ */
@Serialize() @Serialize()
@Property({ type: 'asset', label: 'Disabled Texture', assetType: 'texture' }) @Property({ type: 'asset', label: 'Disabled Texture', assetType: 'texture' })
public disabledTexture: string = ''; public disabledTextureGuid: string = '';
// ===== 状态样式 State Styles ===== // ===== 状态样式 State Styles =====
@@ -245,16 +246,16 @@ export class UIButtonComponent extends Component {
} }
/** /**
* 获取当前应该显示的纹理 * 获取当前应该显示的纹理 GUID
* Get the texture that should be displayed based on state * Get the texture GUID that should be displayed based on state
*/ */
public getStateTexture(state: 'disabled' | 'pressed' | 'hovered' | 'focused' | 'normal'): string { public getStateTextureGuid(state: 'disabled' | 'pressed' | 'hovered' | 'focused' | 'normal'): string {
if (this.disabled && this.disabledTexture) return this.disabledTexture; if (this.disabled && this.disabledTextureGuid) return this.disabledTextureGuid;
switch (state) { switch (state) {
case 'pressed': return this.pressedTexture || this.normalTexture; case 'pressed': return this.pressedTextureGuid || this.normalTextureGuid;
case 'hovered': return this.hoverTexture || this.normalTexture; case 'hovered': return this.hoverTextureGuid || this.normalTextureGuid;
case 'focused': return this.normalTexture; case 'focused': return this.normalTextureGuid;
default: return this.normalTexture; default: return this.normalTextureGuid;
} }
} }
@@ -263,7 +264,7 @@ export class UIButtonComponent extends Component {
* Whether to use texture for rendering * Whether to use texture for rendering
*/ */
public useTexture(): boolean { public useTexture(): boolean {
return (this.displayMode === 'texture' || this.displayMode === 'both') && !!this.normalTexture; return (this.displayMode === 'texture' || this.displayMode === 'both') && !!this.normalTextureGuid;
} }
/** /**
@@ -297,14 +298,14 @@ export class UIButtonComponent extends Component {
} }
/** /**
* 设置纹理 * 设置纹理 GUID
* Set textures for different states * Set texture GUIDs for different states
*/ */
public setTextures(normal: string, hover?: string, pressed?: string, disabled?: string): this { public setTextureGuids(normalGuid: string, hoverGuid?: string, pressedGuid?: string, disabledGuid?: string): this {
this.normalTexture = normal; this.normalTextureGuid = normalGuid;
if (hover) this.hoverTexture = hover; if (hoverGuid) this.hoverTextureGuid = hoverGuid;
if (pressed) this.pressedTexture = pressed; if (pressedGuid) this.pressedTextureGuid = pressedGuid;
if (disabled) this.disabledTexture = disabled; if (disabledGuid) this.disabledTextureGuid = disabledGuid;
this.displayMode = 'texture'; this.displayMode = 'texture';
return this; return this;
} }

View File

@@ -66,8 +66,8 @@ export class UIButtonRenderSystem extends EntitySystem {
// Render texture if in texture or both mode // Render texture if in texture or both mode
// 如果在纹理或两者模式下,渲染纹理 // 如果在纹理或两者模式下,渲染纹理
if (button.useTexture()) { if (button.useTexture()) {
const texture = button.getStateTexture('normal'); const textureGuid = button.getStateTextureGuid('normal');
if (texture) { if (textureGuid) {
collector.addRect( collector.addRect(
renderX, renderY, renderX, renderY,
width, height, width, height,
@@ -78,7 +78,7 @@ export class UIButtonRenderSystem extends EntitySystem {
rotation, rotation,
pivotX, pivotX,
pivotY, pivotY,
texturePath: texture texturePath: textureGuid
} }
); );
} }

View File

@@ -96,9 +96,9 @@ export class UIRectRenderSystem extends EntitySystem {
// Render texture if present // Render texture if present
// 如果有纹理,渲染纹理 // 如果有纹理,渲染纹理
if (render.texture) { if (render.textureGuid) {
const texturePath = typeof render.texture === 'string' ? render.texture : undefined; const texturePath = typeof render.textureGuid === 'string' ? render.textureGuid : undefined;
const textureId = typeof render.texture === 'number' ? render.texture : undefined; const textureId = typeof render.textureGuid === 'number' ? render.textureGuid : undefined;
collector.addRect( collector.addRect(
renderX, renderY, renderX, renderY,