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:
@@ -38,7 +38,7 @@ import {
|
||||
Settings
|
||||
} from 'lucide-react';
|
||||
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 { SettingsService } from '../services/SettingsService';
|
||||
import { ContextMenu, ContextMenuItem } from './ContextMenu';
|
||||
@@ -770,8 +770,19 @@ export class ${className} {
|
||||
const parentPath = asset.path.substring(0, lastSlash);
|
||||
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);
|
||||
|
||||
// Refresh asset registry | 刷新资产注册表
|
||||
if (assetRegistry && asset.type !== 'folder') {
|
||||
await assetRegistry.refreshAsset(newPath);
|
||||
}
|
||||
|
||||
if (currentPath) {
|
||||
await loadAssets(currentPath);
|
||||
}
|
||||
@@ -1371,6 +1382,18 @@ export class ${className} {
|
||||
if (asset.type === 'file') {
|
||||
e.dataTransfer.setData('asset-path', 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
Save, Tag, Link, FileSearch, Globe, Package, Clipboard, RefreshCw, Settings
|
||||
} from 'lucide-react';
|
||||
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 { Core } from '@esengine/ecs-framework';
|
||||
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('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';
|
||||
}
|
||||
|
||||
@@ -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 { convertFileSrc } from '@tauri-apps/api/core';
|
||||
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 './AssetField.css';
|
||||
|
||||
interface AssetFieldProps {
|
||||
label?: string;
|
||||
/** Value can be GUID or path (for backward compatibility) */
|
||||
value: string | null;
|
||||
onChange: (value: string | null) => void;
|
||||
fileExtension?: string;
|
||||
@@ -17,6 +18,14 @@ interface AssetFieldProps {
|
||||
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({
|
||||
label,
|
||||
value,
|
||||
@@ -32,6 +41,24 @@ export function AssetField({
|
||||
const [thumbnailUrl, setThumbnailUrl] = useState<string | null>(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) => {
|
||||
if (!path) return false;
|
||||
@@ -40,18 +67,18 @@ export function AssetField({
|
||||
);
|
||||
}, []);
|
||||
|
||||
// 加载缩略图
|
||||
// 加载缩略图(使用 resolvedPath)
|
||||
useEffect(() => {
|
||||
if (value && isImageAsset(value)) {
|
||||
if (resolvedPath && isImageAsset(resolvedPath)) {
|
||||
// 获取项目路径并构建完整路径
|
||||
const projectService = Core.services.tryResolve(ProjectService);
|
||||
const projectPath = projectService?.getCurrentProject()?.path;
|
||||
|
||||
if (projectPath) {
|
||||
// 构建完整的文件路径
|
||||
const fullPath = value.startsWith('/') || value.includes(':')
|
||||
? value
|
||||
: `${projectPath}/${value}`;
|
||||
const fullPath = resolvedPath.startsWith('/') || resolvedPath.includes(':')
|
||||
? resolvedPath
|
||||
: `${projectPath}/${resolvedPath}`;
|
||||
|
||||
try {
|
||||
const url = convertFileSrc(fullPath);
|
||||
@@ -60,9 +87,9 @@ export function AssetField({
|
||||
setThumbnailUrl(null);
|
||||
}
|
||||
} else {
|
||||
// 没有项目路径时,尝试直接使用 value
|
||||
// 没有项目路径时,尝试直接使用 resolvedPath
|
||||
try {
|
||||
const url = convertFileSrc(value);
|
||||
const url = convertFileSrc(resolvedPath);
|
||||
setThumbnailUrl(url);
|
||||
} catch {
|
||||
setThumbnailUrl(null);
|
||||
@@ -71,7 +98,7 @@ export function AssetField({
|
||||
} else {
|
||||
setThumbnailUrl(null);
|
||||
}
|
||||
}, [value, isImageAsset]);
|
||||
}, [resolvedPath, isImageAsset]);
|
||||
|
||||
const handleDragEnter = useCallback((e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -99,27 +126,66 @@ export function AssetField({
|
||||
|
||||
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 file = files.find((f) =>
|
||||
!fileExtension || f.name.endsWith(fileExtension)
|
||||
);
|
||||
|
||||
if (file) {
|
||||
// For file drops, we still use filename (need to register first)
|
||||
onChange(file.name);
|
||||
return;
|
||||
}
|
||||
|
||||
const assetPath = e.dataTransfer.getData('asset-path');
|
||||
if (assetPath && (!fileExtension || assetPath.endsWith(fileExtension))) {
|
||||
onChange(assetPath);
|
||||
return;
|
||||
}
|
||||
|
||||
const text = e.dataTransfer.getData('text/plain');
|
||||
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, fileExtension, readonly]);
|
||||
}, [onChange, fileExtension, readonly, assetRegistry]);
|
||||
|
||||
const handleBrowse = useCallback(() => {
|
||||
if (readonly) return;
|
||||
@@ -127,9 +193,24 @@ export function AssetField({
|
||||
}, [readonly]);
|
||||
|
||||
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);
|
||||
setShowPicker(false);
|
||||
}, [onChange]);
|
||||
}, [onChange, assetRegistry]);
|
||||
|
||||
const handleClear = useCallback(() => {
|
||||
if (!readonly) {
|
||||
@@ -137,11 +218,15 @@ export function AssetField({
|
||||
}
|
||||
}, [onChange, readonly]);
|
||||
|
||||
const getFileName = (path: string) => {
|
||||
const getFileName = (path: string | null) => {
|
||||
if (!path) return placeholder;
|
||||
const parts = path.split(/[\\/]/);
|
||||
return parts[parts.length - 1];
|
||||
};
|
||||
|
||||
// Display name uses resolvedPath
|
||||
const displayName = resolvedPath ? getFileName(resolvedPath) : placeholder;
|
||||
|
||||
return (
|
||||
<div className="asset-field">
|
||||
{label && <label className="asset-field__label">{label}</label>}
|
||||
@@ -166,16 +251,16 @@ export function AssetField({
|
||||
{/* 下拉选择框 */}
|
||||
<div
|
||||
ref={inputRef}
|
||||
className={`asset-field__dropdown ${value ? 'has-value' : ''} ${isDragging ? 'dragging' : ''}`}
|
||||
className={`asset-field__dropdown ${resolvedPath ? 'has-value' : ''} ${isDragging ? 'dragging' : ''}`}
|
||||
onClick={!readonly ? handleBrowse : undefined}
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
title={value || placeholder}
|
||||
title={resolvedPath || placeholder}
|
||||
>
|
||||
<span className="asset-field__value">
|
||||
{value ? getFileName(value) : placeholder}
|
||||
{displayName}
|
||||
</span>
|
||||
<ChevronDown size={12} className="asset-field__dropdown-arrow" />
|
||||
</div>
|
||||
@@ -183,12 +268,12 @@ export function AssetField({
|
||||
{/* 操作按钮行 */}
|
||||
<div className="asset-field__actions">
|
||||
{/* 定位按钮 */}
|
||||
{value && onNavigate && (
|
||||
{resolvedPath && onNavigate && (
|
||||
<button
|
||||
className="asset-field__btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onNavigate(value);
|
||||
onNavigate(resolvedPath);
|
||||
}}
|
||||
title="Locate in Asset Browser"
|
||||
>
|
||||
@@ -196,13 +281,13 @@ export function AssetField({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 复制路径按钮 */}
|
||||
{value && (
|
||||
{/* 复制路径按钮 - copy path, not GUID */}
|
||||
{resolvedPath && (
|
||||
<button
|
||||
className="asset-field__btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigator.clipboard.writeText(value);
|
||||
navigator.clipboard.writeText(resolvedPath);
|
||||
}}
|
||||
title="Copy Path"
|
||||
>
|
||||
|
||||
@@ -169,7 +169,7 @@ function AnimationClipsEditor({ label, clips, onChange, readonly, component, onD
|
||||
const newClips = [...clips];
|
||||
const clip = newClips[clipIndex];
|
||||
if (!clip) return;
|
||||
clip.frames = [...clip.frames, { texture: '', duration: 0.1 }];
|
||||
clip.frames = [...clip.frames, { textureGuid: '', duration: 0.1 }];
|
||||
onChange(newClips);
|
||||
};
|
||||
|
||||
@@ -196,8 +196,8 @@ function AnimationClipsEditor({ label, clips, onChange, readonly, component, onD
|
||||
const newClips = [...clips];
|
||||
const clip = newClips[clipIndex];
|
||||
if (!clip) return;
|
||||
const newFrames = texturePaths.map((texture) => ({
|
||||
texture,
|
||||
const newFrames = texturePaths.map((textureGuid) => ({
|
||||
textureGuid,
|
||||
duration: 0.1
|
||||
}));
|
||||
clip.frames = [...clip.frames, ...newFrames];
|
||||
@@ -451,8 +451,8 @@ function AnimationClipsEditor({ label, clips, onChange, readonly, component, onD
|
||||
<span className="frame-index">{frameIndex + 1}</span>
|
||||
<div className="frame-texture-field">
|
||||
<AssetField
|
||||
value={frame.texture}
|
||||
onChange={(val) => updateFrame(clipIndex, frameIndex, { texture: val || '' })}
|
||||
value={frame.textureGuid}
|
||||
onChange={(val) => updateFrame(clipIndex, frameIndex, { textureGuid: val || '' })}
|
||||
fileExtension=".png"
|
||||
placeholder="Texture..."
|
||||
readonly={readonly}
|
||||
|
||||
@@ -20,16 +20,20 @@ const logger = createLogger('AssetMetaPlugin');
|
||||
class AssetMetaEditorModule implements IEditorModuleLoader {
|
||||
private _assetRegistry: AssetRegistryService | null = null;
|
||||
|
||||
async install(_services: ServiceContainer): Promise<void> {
|
||||
async install(services: ServiceContainer): Promise<void> {
|
||||
// 创建 AssetRegistryService 并初始化
|
||||
// Create AssetRegistryService and initialize
|
||||
this._assetRegistry = new AssetRegistryService();
|
||||
|
||||
// 注册到服务容器,以便其他地方可以访问
|
||||
// Register to service container so other places can access it
|
||||
services.registerInstance(AssetRegistryService, this._assetRegistry);
|
||||
|
||||
// 初始化服务(订阅 project:opened 事件)
|
||||
// Initialize service (subscribes to project:opened event)
|
||||
await this._assetRegistry.initialize();
|
||||
|
||||
logger.info('AssetRegistryService initialized');
|
||||
logger.info('AssetRegistryService initialized and registered');
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
|
||||
@@ -155,9 +155,9 @@ export class EditorEngineSync {
|
||||
if (bridge) {
|
||||
for (const clip of animator.clips) {
|
||||
for (const frame of clip.frames) {
|
||||
if (frame.texture) {
|
||||
if (frame.textureGuid) {
|
||||
// Trigger texture loading
|
||||
bridge.getOrLoadTextureByPath(frame.texture);
|
||||
bridge.getOrLoadTextureByPath(frame.textureGuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,8 +168,8 @@ export class EditorEngineSync {
|
||||
const firstClip = animator.clips[0];
|
||||
if (firstClip && firstClip.frames && firstClip.frames.length > 0) {
|
||||
const firstFrame = firstClip.frames[0];
|
||||
if (firstFrame && firstFrame.texture && spriteComponent) {
|
||||
spriteComponent.texture = firstFrame.texture;
|
||||
if (firstFrame && firstFrame.textureGuid && spriteComponent) {
|
||||
spriteComponent.textureGuid = firstFrame.textureGuid;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,8 +228,8 @@ export class EditorEngineSync {
|
||||
// Preload all frame textures
|
||||
for (const clip of animator.clips) {
|
||||
for (const frame of clip.frames) {
|
||||
if (frame.texture) {
|
||||
bridge.getOrLoadTextureByPath(frame.texture);
|
||||
if (frame.textureGuid) {
|
||||
bridge.getOrLoadTextureByPath(frame.textureGuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,8 +240,8 @@ export class EditorEngineSync {
|
||||
const firstClip = animator.clips[0];
|
||||
if (firstClip && firstClip.frames && firstClip.frames.length > 0) {
|
||||
const firstFrame = firstClip.frames[0];
|
||||
if (firstFrame && firstFrame.texture) {
|
||||
sprite.texture = firstFrame.texture;
|
||||
if (firstFrame && firstFrame.textureGuid) {
|
||||
sprite.textureGuid = firstFrame.textureGuid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* 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 { CameraConfig } from '@esengine/ecs-engine-bindgen';
|
||||
import { TransformComponent } from '@esengine/engine-core';
|
||||
@@ -148,6 +148,10 @@ export class EngineService {
|
||||
// 初始化资产系统
|
||||
await this._initializeAssetSystem();
|
||||
|
||||
// 设置资产路径解析器(用于 GUID 到路径的转换)
|
||||
// Set asset path resolver (for GUID to path conversion)
|
||||
this._setupAssetPathResolver();
|
||||
|
||||
// 同步视口尺寸
|
||||
const canvas = document.getElementById(canvasId) as HTMLCanvasElement;
|
||||
if (canvas && canvas.parentElement) {
|
||||
@@ -339,8 +343,8 @@ export class EngineService {
|
||||
const firstClip = animator.clips[0];
|
||||
if (firstClip && firstClip.frames && firstClip.frames.length > 0) {
|
||||
const firstFrame = firstClip.frames[0];
|
||||
if (firstFrame && firstFrame.texture) {
|
||||
sprite.texture = firstFrame.texture;
|
||||
if (firstFrame && firstFrame.textureGuid) {
|
||||
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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user