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:
@@ -1,11 +1,15 @@
|
||||
import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSComponent('AudioSource')
|
||||
@Serializable({ version: 1, typeId: 'AudioSource' })
|
||||
@Serializable({ version: 2, typeId: 'AudioSource' })
|
||||
export class AudioSourceComponent extends Component {
|
||||
/**
|
||||
* 音频资产 GUID
|
||||
* Audio clip asset GUID
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'asset', label: 'Audio Clip', assetType: 'audio' })
|
||||
clip: string = '';
|
||||
clipGuid: string = '';
|
||||
|
||||
/** 范围 [0, 1] */
|
||||
@Serialize()
|
||||
|
||||
@@ -109,9 +109,9 @@ export class SpriteRenderHelper {
|
||||
// Convert hex color string to packed RGBA
|
||||
const color = this.hexToPackedColor(sprite.color, sprite.alpha);
|
||||
|
||||
// Get material ID from path (0 = default if not found or no path specified)
|
||||
const materialId = sprite.material
|
||||
? getMaterialManager().getMaterialIdByPath(sprite.material)
|
||||
// Get material ID from GUID (0 = default if not found or no GUID specified)
|
||||
const materialId = sprite.materialGuid
|
||||
? getMaterialManager().getMaterialIdByPath(sprite.materialGuid)
|
||||
: 0;
|
||||
|
||||
// Collect material overrides if any
|
||||
|
||||
@@ -10,6 +10,6 @@ export type { EngineBridgeConfig } from './core/EngineBridge';
|
||||
export { RenderBatcher } from './core/RenderBatcher';
|
||||
export { SpriteRenderHelper } 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 * from './types';
|
||||
|
||||
@@ -119,6 +119,18 @@ export type HasGizmoProviderFn = (component: Component) => boolean;
|
||||
*/
|
||||
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.
|
||||
* 使用Rust引擎渲染精灵的ECS系统。
|
||||
@@ -177,6 +189,10 @@ export class EngineRenderSystem extends EntitySystem {
|
||||
// UI 渲染数据提供者(支持屏幕空间和世界空间)
|
||||
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
|
||||
// when false (editor mode), UI renders in world space following editor camera
|
||||
// 预览模式标志:为 true 时,UI 使用屏幕空间叠加投影
|
||||
@@ -288,14 +304,24 @@ export class EngineRenderSystem extends EntitySystem {
|
||||
// Use Rust engine's path-based texture loading for automatic caching
|
||||
// 使用Rust引擎的基于路径的纹理加载实现自动缓存
|
||||
let textureId = 0;
|
||||
if (sprite.texture) {
|
||||
textureId = this.bridge.getOrLoadTextureByPath(sprite.texture);
|
||||
const textureSource = sprite.getTextureSource();
|
||||
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)
|
||||
// 从路径获取材质 ID(0 = 默认,如果未找到或未指定路径)
|
||||
const materialId = sprite.material
|
||||
? getMaterialManager().getMaterialIdByPath(sprite.material)
|
||||
// Get material ID from GUID (0 = default if not found or no GUID specified)
|
||||
// 从 GUID 获取材质 ID(0 = 默认,如果未找到或未指定 GUID)
|
||||
const materialGuidOrPath = sprite.materialGuid;
|
||||
const materialPath = materialGuidOrPath && this.assetPathResolver
|
||||
? this.assetPathResolver(materialGuidOrPath)
|
||||
: materialGuidOrPath;
|
||||
const materialId = materialPath
|
||||
? getMaterialManager().getMaterialIdByPath(materialPath)
|
||||
: 0;
|
||||
|
||||
// Collect material overrides if any
|
||||
@@ -1159,4 +1185,28 @@ export class EngineRenderSystem extends EntitySystem {
|
||||
loadTexture(id: number, url: string): void {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -139,9 +139,13 @@ class SimpleAssetDatabase {
|
||||
private readonly _typeToGuids = new Map<AssetRegistryType, Set<AssetGUID>>();
|
||||
|
||||
addAsset(metadata: IAssetRegistryMetadata): void {
|
||||
const { guid, path, type } = metadata;
|
||||
this._metadata.set(guid, metadata);
|
||||
this._pathToGuid.set(path, guid);
|
||||
const { guid, type } = metadata;
|
||||
// Normalize path separators for consistent storage
|
||||
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)) {
|
||||
this._typeToGuids.set(type, new Set());
|
||||
@@ -154,6 +158,7 @@ class SimpleAssetDatabase {
|
||||
if (!metadata) return;
|
||||
|
||||
this._metadata.delete(guid);
|
||||
// Path is already normalized when stored
|
||||
this._pathToGuid.delete(metadata.path);
|
||||
|
||||
const typeSet = this._typeToGuids.get(metadata.type);
|
||||
@@ -167,7 +172,9 @@ class SimpleAssetDatabase {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -638,6 +645,11 @@ export class AssetRegistryService {
|
||||
*/
|
||||
getGuidByPath(relativePath: string): AssetGUID | undefined {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -776,4 +788,13 @@ export class AssetRegistryService {
|
||||
get projectPath(): string | null {
|
||||
return this._projectPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose the service
|
||||
*/
|
||||
dispose(): void {
|
||||
this._unsubscribeFromFileChanges();
|
||||
this.unloadProject();
|
||||
this._initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,9 +54,25 @@ export enum SimulationSpace {
|
||||
* 管理粒子发射、模拟,并为渲染提供数据。
|
||||
*/
|
||||
@ECSComponent('ParticleSystem')
|
||||
@Serializable({ version: 1, typeId: 'ParticleSystem' })
|
||||
@Serializable({ version: 2, typeId: 'ParticleSystem' })
|
||||
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 =============
|
||||
// These are used when particleAssetGuid is not set
|
||||
|
||||
/** 最大粒子数量 | Maximum particle count */
|
||||
@Serialize()
|
||||
@@ -200,10 +216,13 @@ export class ParticleSystemComponent extends Component {
|
||||
|
||||
// ============= 渲染属性 | Rendering Properties =============
|
||||
|
||||
/** 粒子纹理 | Particle texture */
|
||||
/**
|
||||
* 粒子纹理 GUID
|
||||
* Particle texture GUID
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'asset', label: 'Texture', assetType: 'texture' })
|
||||
public texture: string = '';
|
||||
public textureGuid: string = '';
|
||||
|
||||
/** 粒子尺寸(像素)| Particle size (pixels) */
|
||||
@Serialize()
|
||||
|
||||
@@ -108,8 +108,8 @@ export interface IParticleAsset {
|
||||
blendMode: ParticleBlendMode;
|
||||
/** 排序顺序 | Sorting order */
|
||||
sortingOrder: number;
|
||||
/** 纹理路径 | Texture path */
|
||||
texture?: string;
|
||||
/** 纹理资产 GUID | Texture asset GUID */
|
||||
textureGuid?: string;
|
||||
|
||||
// 模块配置 | Module configurations
|
||||
/** 模块列表 | Module list */
|
||||
|
||||
@@ -191,7 +191,7 @@ export class ParticleRenderDataProvider implements IRenderDataProvider {
|
||||
colors: this._colors.subarray(0, particleIndex),
|
||||
tileCount: particleIndex,
|
||||
sortingOrder,
|
||||
texturePath: systems[0]?.component.texture || undefined
|
||||
texturePath: systems[0]?.component.textureGuid || undefined
|
||||
};
|
||||
|
||||
this._renderDataCache.push(renderData);
|
||||
|
||||
@@ -65,7 +65,7 @@ export class Canvas2DRenderSystem extends EntitySystem {
|
||||
this.ctx.translate(x, y);
|
||||
this.ctx.rotate(rotation);
|
||||
|
||||
const texture = this.textureCache.get(sprite.texture || '');
|
||||
const texture = this.textureCache.get(sprite.textureGuid || '');
|
||||
if (texture) {
|
||||
this.ctx.drawImage(texture, -width / 2, -height / 2, width, height);
|
||||
} else {
|
||||
|
||||
@@ -626,7 +626,7 @@ function MaterialOverrideEditor({ sprite, material, onChange }: MaterialOverride
|
||||
onChange('materialOverrides', newOverrides);
|
||||
};
|
||||
|
||||
if (!sprite.material) {
|
||||
if (!sprite.materialGuid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -871,10 +871,10 @@ function SpriteInspectorContent({ context }: { context: ComponentInspectorContex
|
||||
const [material, setMaterial] = useState<Material | null>(null);
|
||||
const [, forceUpdate] = useState({});
|
||||
|
||||
// Load material when sprite.material changes.
|
||||
// 当 sprite.material 变化时加载材质。
|
||||
// Load material when sprite.materialGuid changes.
|
||||
// 当 sprite.materialGuid 变化时加载材质。
|
||||
useEffect(() => {
|
||||
if (!sprite.material) {
|
||||
if (!sprite.materialGuid) {
|
||||
setMaterial(null);
|
||||
return;
|
||||
}
|
||||
@@ -887,7 +887,7 @@ function SpriteInspectorContent({ context }: { context: ComponentInspectorContex
|
||||
|
||||
// Try to get cached material by ID.
|
||||
// 尝试通过 ID 获取缓存的材质。
|
||||
const materialId = materialManager.getMaterialIdByPath(sprite.material);
|
||||
const materialId = materialManager.getMaterialIdByPath(sprite.materialGuid);
|
||||
if (materialId > 0) {
|
||||
const mat = materialManager.getMaterial(materialId);
|
||||
setMaterial(mat || null);
|
||||
@@ -896,7 +896,7 @@ function SpriteInspectorContent({ context }: { context: ComponentInspectorContex
|
||||
|
||||
// Load material asynchronously.
|
||||
// 异步加载材质。
|
||||
materialManager.loadMaterialFromPath(sprite.material)
|
||||
materialManager.loadMaterialFromPath(sprite.materialGuid)
|
||||
.then(matId => {
|
||||
const mat = materialManager.getMaterial(matId);
|
||||
setMaterial(mat || null);
|
||||
@@ -904,7 +904,7 @@ function SpriteInspectorContent({ context }: { context: ComponentInspectorContex
|
||||
.catch(() => {
|
||||
setMaterial(null);
|
||||
});
|
||||
}, [sprite.material]);
|
||||
}, [sprite.materialGuid]);
|
||||
|
||||
const handleChange = useCallback((propertyName: string, value: unknown) => {
|
||||
(sprite as unknown as Record<string, unknown>)[propertyName] = value;
|
||||
@@ -930,7 +930,7 @@ function SpriteInspectorContent({ context }: { context: ComponentInspectorContex
|
||||
}, []);
|
||||
|
||||
// No material selected
|
||||
if (!sprite.material) {
|
||||
if (!sprite.materialGuid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -940,7 +940,7 @@ function SpriteInspectorContent({ context }: { context: ComponentInspectorContex
|
||||
{material && (
|
||||
<InlineMaterialEditor
|
||||
material={material}
|
||||
materialPath={sprite.material}
|
||||
materialPath={sprite.materialGuid}
|
||||
onMaterialChange={handleMaterialChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -5,8 +5,11 @@ import { Component, ECSComponent, Serializable, Serialize, Property } from '@ese
|
||||
* Animation frame data
|
||||
*/
|
||||
export interface AnimationFrame {
|
||||
/** 纹理路径 | Texture path */
|
||||
texture: string;
|
||||
/**
|
||||
* 纹理资产 GUID
|
||||
* Texture asset GUID
|
||||
*/
|
||||
textureGuid: string;
|
||||
/** 帧持续时间(秒) | Frame duration in seconds */
|
||||
duration: number;
|
||||
/** UV坐标 [u0, v0, u1, v1] | UV coordinates */
|
||||
@@ -43,7 +46,7 @@ export class SpriteAnimatorComponent extends Component {
|
||||
@Property({
|
||||
type: 'animationClips',
|
||||
label: 'Animation Clips',
|
||||
controls: [{ component: 'Sprite', property: 'texture' }]
|
||||
controls: [{ component: 'Sprite', property: 'textureGuid' }]
|
||||
})
|
||||
public clips: AnimationClip[] = [];
|
||||
|
||||
@@ -101,7 +104,7 @@ export class SpriteAnimatorComponent extends Component {
|
||||
* Create animation clip from sprite atlas
|
||||
*
|
||||
* @param name - 动画名称 | Animation name
|
||||
* @param texture - 纹理路径 | Texture path
|
||||
* @param textureGuid - 纹理资产 GUID | Texture asset GUID
|
||||
* @param frameCount - 帧数 | Number of frames
|
||||
* @param frameWidth - 每帧宽度 | Frame width
|
||||
* @param frameHeight - 每帧高度 | Frame height
|
||||
@@ -112,7 +115,7 @@ export class SpriteAnimatorComponent extends Component {
|
||||
*/
|
||||
createClipFromAtlas(
|
||||
name: string,
|
||||
texture: string,
|
||||
textureGuid: string,
|
||||
frameCount: number,
|
||||
frameWidth: number,
|
||||
frameHeight: number,
|
||||
@@ -132,7 +135,7 @@ export class SpriteAnimatorComponent extends Component {
|
||||
const y = row * frameHeight;
|
||||
|
||||
frames.push({
|
||||
texture,
|
||||
textureGuid,
|
||||
duration,
|
||||
uv: [
|
||||
x / atlasWidth,
|
||||
@@ -159,19 +162,19 @@ export class SpriteAnimatorComponent extends Component {
|
||||
* Create animation clip from frame sequence
|
||||
*
|
||||
* @param name - 动画名称 | Animation name
|
||||
* @param textures - 纹理路径数组 | Array of texture paths
|
||||
* @param textureGuids - 纹理资产 GUID 数组 | Array of texture asset GUIDs
|
||||
* @param fps - 帧率 | Frames per second
|
||||
* @param loop - 是否循环 | Whether to loop
|
||||
*/
|
||||
createClipFromSequence(
|
||||
name: string,
|
||||
textures: string[],
|
||||
textureGuids: string[],
|
||||
fps: number = 12,
|
||||
loop: boolean = true
|
||||
): AnimationClip {
|
||||
const duration = 1 / fps;
|
||||
const frames: AnimationFrame[] = textures.map((texture) => ({
|
||||
texture,
|
||||
const frames: AnimationFrame[] = textureGuids.map((textureGuid) => ({
|
||||
textureGuid,
|
||||
duration
|
||||
}));
|
||||
|
||||
|
||||
@@ -27,19 +27,20 @@ export type MaterialOverrides = Record<string, MaterialPropertyOverride>;
|
||||
* Sprite component - manages 2D image rendering
|
||||
*/
|
||||
@ECSComponent('Sprite')
|
||||
@Serializable({ version: 3, typeId: 'Sprite' })
|
||||
@Serializable({ version: 4, typeId: 'Sprite' })
|
||||
export class SpriteComponent extends Component {
|
||||
/** 纹理路径或资源ID | Texture path or asset ID */
|
||||
@Serialize()
|
||||
@Property({ type: 'asset', label: 'Texture', assetType: 'texture' })
|
||||
public texture: string = '';
|
||||
|
||||
/**
|
||||
* 资产GUID(新的资产系统)
|
||||
* Asset GUID for new asset system
|
||||
* 纹理资产 GUID
|
||||
* Texture asset GUID
|
||||
*
|
||||
* Stores the unique identifier of the texture asset.
|
||||
* The actual file path is resolved at runtime via AssetDatabase.
|
||||
* 存储纹理资产的唯一标识符。
|
||||
* 实际文件路径在运行时通过 AssetDatabase 解析。
|
||||
*/
|
||||
@Serialize()
|
||||
public assetGuid?: string;
|
||||
@Property({ type: 'asset', label: 'Texture', assetType: 'texture' })
|
||||
public textureGuid: string = '';
|
||||
|
||||
/**
|
||||
* 纹理ID(运行时使用)
|
||||
@@ -151,15 +152,15 @@ export class SpriteComponent extends Component {
|
||||
public sortingOrder: number = 0;
|
||||
|
||||
/**
|
||||
* 材质资产路径(共享材质)
|
||||
* Material asset path (shared material)
|
||||
* 材质资产 GUID(共享材质)
|
||||
* Material asset GUID (shared material)
|
||||
*
|
||||
* Multiple sprites can reference the same material file.
|
||||
* 多个精灵可以引用同一个材质文件。
|
||||
*/
|
||||
@Serialize()
|
||||
@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;
|
||||
}
|
||||
|
||||
constructor(texture: string = '') {
|
||||
/**
|
||||
* @param textureGuidOrPath - Texture GUID or path (for backward compatibility)
|
||||
*/
|
||||
constructor(textureGuidOrPath: string = '') {
|
||||
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;
|
||||
if (reference) {
|
||||
this.assetGuid = reference.guid;
|
||||
this.textureGuid = reference.guid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,11 +297,11 @@ export class SpriteComponent extends Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取有效的纹理源
|
||||
* Get effective texture source
|
||||
* 获取纹理 GUID
|
||||
* Get texture GUID
|
||||
*/
|
||||
getTextureSource(): string {
|
||||
return this.assetGuid || this.texture;
|
||||
return this.textureGuid;
|
||||
}
|
||||
|
||||
// ============= Material Override Methods =============
|
||||
|
||||
@@ -51,7 +51,7 @@ export class SpriteAnimatorSystem extends EntitySystem {
|
||||
if (sprite) {
|
||||
const frame = animator.getCurrentFrame();
|
||||
if (frame) {
|
||||
sprite.texture = frame.texture;
|
||||
sprite.textureGuid = frame.textureGuid;
|
||||
|
||||
// Update UV if specified
|
||||
if (frame.uv) {
|
||||
|
||||
@@ -61,7 +61,8 @@ export interface UITextConfig extends UIBaseConfig {
|
||||
* Image configuration
|
||||
*/
|
||||
export interface UIImageConfig extends UIBaseConfig {
|
||||
texture: string | number;
|
||||
/** 纹理资产 GUID 或运行时 ID | Texture asset GUID or runtime ID */
|
||||
textureGuid: string | number;
|
||||
tint?: number;
|
||||
}
|
||||
|
||||
@@ -236,7 +237,7 @@ export class UIBuilder {
|
||||
|
||||
const render = entity.addComponent(new UIRenderComponent());
|
||||
render.type = UIRenderType.Image;
|
||||
render.texture = config.texture;
|
||||
render.textureGuid = config.textureGuid;
|
||||
render.textureTint = config.tint ?? 0xFFFFFF;
|
||||
|
||||
return entity;
|
||||
|
||||
@@ -96,12 +96,12 @@ export class UIRenderComponent extends Component {
|
||||
// ===== 纹理 Texture =====
|
||||
|
||||
/**
|
||||
* 纹理路径或 ID
|
||||
* Texture path or runtime ID
|
||||
* 纹理资产 GUID 或运行时 ID
|
||||
* Texture asset GUID or runtime ID
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'asset', label: 'Texture', assetType: 'texture' })
|
||||
public texture: string | number | null = null;
|
||||
public textureGuid: string | number | null = null;
|
||||
|
||||
/**
|
||||
* 纹理 UV 坐标 (用于图集)
|
||||
@@ -230,20 +230,25 @@ export class UIRenderComponent extends Component {
|
||||
/**
|
||||
* 设置图片
|
||||
* 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.texture = texture;
|
||||
this.textureGuid = textureGuid;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置九宫格
|
||||
* 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.texture = texture;
|
||||
this.textureGuid = textureGuid;
|
||||
this.ninePatchMargins = margins;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,8 @@ export interface UIButtonStyle {
|
||||
textColor: number;
|
||||
borderColor: number;
|
||||
borderWidth: number;
|
||||
texture?: string;
|
||||
/** 纹理资产 GUID | Texture asset GUID */
|
||||
textureGuid?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,36 +52,36 @@ export class UIButtonComponent extends Component {
|
||||
// ===== 状态纹理 State Textures =====
|
||||
|
||||
/**
|
||||
* 正常状态纹理
|
||||
* Normal state texture
|
||||
* 正常状态纹理 GUID
|
||||
* Normal state texture GUID
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'asset', label: 'Normal Texture', assetType: 'texture' })
|
||||
public normalTexture: string = '';
|
||||
public normalTextureGuid: string = '';
|
||||
|
||||
/**
|
||||
* 悬停状态纹理
|
||||
* Hover state texture
|
||||
* 悬停状态纹理 GUID
|
||||
* Hover state texture GUID
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'asset', label: 'Hover Texture', assetType: 'texture' })
|
||||
public hoverTexture: string = '';
|
||||
public hoverTextureGuid: string = '';
|
||||
|
||||
/**
|
||||
* 按下状态纹理
|
||||
* Pressed state texture
|
||||
* 按下状态纹理 GUID
|
||||
* Pressed state texture GUID
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'asset', label: 'Pressed Texture', assetType: 'texture' })
|
||||
public pressedTexture: string = '';
|
||||
public pressedTextureGuid: string = '';
|
||||
|
||||
/**
|
||||
* 禁用状态纹理
|
||||
* Disabled state texture
|
||||
* 禁用状态纹理 GUID
|
||||
* Disabled state texture GUID
|
||||
*/
|
||||
@Serialize()
|
||||
@Property({ type: 'asset', label: 'Disabled Texture', assetType: 'texture' })
|
||||
public disabledTexture: string = '';
|
||||
public disabledTextureGuid: string = '';
|
||||
|
||||
// ===== 状态样式 State Styles =====
|
||||
|
||||
@@ -245,16 +246,16 @@ export class UIButtonComponent extends Component {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前应该显示的纹理
|
||||
* Get the texture that should be displayed based on state
|
||||
* 获取当前应该显示的纹理 GUID
|
||||
* Get the texture GUID that should be displayed based on state
|
||||
*/
|
||||
public getStateTexture(state: 'disabled' | 'pressed' | 'hovered' | 'focused' | 'normal'): string {
|
||||
if (this.disabled && this.disabledTexture) return this.disabledTexture;
|
||||
public getStateTextureGuid(state: 'disabled' | 'pressed' | 'hovered' | 'focused' | 'normal'): string {
|
||||
if (this.disabled && this.disabledTextureGuid) return this.disabledTextureGuid;
|
||||
switch (state) {
|
||||
case 'pressed': return this.pressedTexture || this.normalTexture;
|
||||
case 'hovered': return this.hoverTexture || this.normalTexture;
|
||||
case 'focused': return this.normalTexture;
|
||||
default: return this.normalTexture;
|
||||
case 'pressed': return this.pressedTextureGuid || this.normalTextureGuid;
|
||||
case 'hovered': return this.hoverTextureGuid || this.normalTextureGuid;
|
||||
case 'focused': return this.normalTextureGuid;
|
||||
default: return this.normalTextureGuid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,7 +264,7 @@ export class UIButtonComponent extends Component {
|
||||
* Whether to use texture for rendering
|
||||
*/
|
||||
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 {
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置纹理
|
||||
* Set textures for different states
|
||||
* 设置纹理 GUID
|
||||
* Set texture GUIDs for different states
|
||||
*/
|
||||
public setTextures(normal: string, hover?: string, pressed?: string, disabled?: string): this {
|
||||
this.normalTexture = normal;
|
||||
if (hover) this.hoverTexture = hover;
|
||||
if (pressed) this.pressedTexture = pressed;
|
||||
if (disabled) this.disabledTexture = disabled;
|
||||
public setTextureGuids(normalGuid: string, hoverGuid?: string, pressedGuid?: string, disabledGuid?: string): this {
|
||||
this.normalTextureGuid = normalGuid;
|
||||
if (hoverGuid) this.hoverTextureGuid = hoverGuid;
|
||||
if (pressedGuid) this.pressedTextureGuid = pressedGuid;
|
||||
if (disabledGuid) this.disabledTextureGuid = disabledGuid;
|
||||
this.displayMode = 'texture';
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -66,8 +66,8 @@ export class UIButtonRenderSystem extends EntitySystem {
|
||||
// Render texture if in texture or both mode
|
||||
// 如果在纹理或两者模式下,渲染纹理
|
||||
if (button.useTexture()) {
|
||||
const texture = button.getStateTexture('normal');
|
||||
if (texture) {
|
||||
const textureGuid = button.getStateTextureGuid('normal');
|
||||
if (textureGuid) {
|
||||
collector.addRect(
|
||||
renderX, renderY,
|
||||
width, height,
|
||||
@@ -78,7 +78,7 @@ export class UIButtonRenderSystem extends EntitySystem {
|
||||
rotation,
|
||||
pivotX,
|
||||
pivotY,
|
||||
texturePath: texture
|
||||
texturePath: textureGuid
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -96,9 +96,9 @@ export class UIRectRenderSystem extends EntitySystem {
|
||||
|
||||
// Render texture if present
|
||||
// 如果有纹理,渲染纹理
|
||||
if (render.texture) {
|
||||
const texturePath = typeof render.texture === 'string' ? render.texture : undefined;
|
||||
const textureId = typeof render.texture === 'number' ? render.texture : undefined;
|
||||
if (render.textureGuid) {
|
||||
const texturePath = typeof render.textureGuid === 'string' ? render.textureGuid : undefined;
|
||||
const textureId = typeof render.textureGuid === 'number' ? render.textureGuid : undefined;
|
||||
|
||||
collector.addRect(
|
||||
renderX, renderY,
|
||||
|
||||
Reference in New Issue
Block a user