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';
|
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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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)
|
||||||
// 从路径获取材质 ID(0 = 默认,如果未找到或未指定路径)
|
// 从 GUID 获取材质 ID(0 = 默认,如果未找到或未指定 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -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 =============
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user