2025-12-06 14:08:48 +08:00
import React , { useState , useRef , useCallback , useEffect , useMemo } from 'react' ;
2025-11-29 23:00:48 +08:00
import { Image , X , Navigation , ChevronDown , Copy } from 'lucide-react' ;
import { convertFileSrc } from '@tauri-apps/api/core' ;
2025-12-08 21:26:35 +08:00
import { Core } from '@esengine/ecs-framework' ;
2025-12-06 14:08:48 +08:00
import { ProjectService , AssetRegistryService } from '@esengine/editor-core' ;
2025-11-23 14:49:37 +08:00
import { AssetPickerDialog } from '../../../components/dialogs/AssetPickerDialog' ;
2025-11-19 14:54:03 +08:00
import './AssetField.css' ;
interface AssetFieldProps {
2025-11-23 14:49:37 +08:00
label? : string ;
2025-12-06 14:08:48 +08:00
/** Value can be GUID or path (for backward compatibility) */
2025-11-19 14:54:03 +08:00
value : string | null ;
onChange : ( value : string | null ) = > void ;
2025-11-29 23:00:48 +08:00
fileExtension? : string ;
2025-11-19 14:54:03 +08:00
placeholder? : string ;
readonly ? : boolean ;
2025-11-29 23:00:48 +08:00
onNavigate ? : ( path : string ) = > void ;
onCreate ? : ( ) = > void ;
2025-11-19 14:54:03 +08:00
}
2025-12-06 14:08:48 +08:00
/ * *
* 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 ) ;
}
2025-11-19 14:54:03 +08:00
export function AssetField ( {
label ,
value ,
onChange ,
fileExtension = '' ,
placeholder = 'None' ,
readonly = false ,
2025-11-25 22:23:19 +08:00
onNavigate ,
onCreate
2025-11-19 14:54:03 +08:00
} : AssetFieldProps ) {
const [ isDragging , setIsDragging ] = useState ( false ) ;
2025-11-23 14:49:37 +08:00
const [ showPicker , setShowPicker ] = useState ( false ) ;
2025-11-29 23:00:48 +08:00
const [ thumbnailUrl , setThumbnailUrl ] = useState < string | null > ( null ) ;
2025-11-19 14:54:03 +08:00
const inputRef = useRef < HTMLDivElement > ( null ) ;
2025-12-06 14:08:48 +08:00
// 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 ] ) ;
2025-11-29 23:00:48 +08:00
// 检测是否是图片资源
const isImageAsset = useCallback ( ( path : string | null ) = > {
if ( ! path ) return false ;
return [ '.png' , '.jpg' , '.jpeg' , '.gif' , '.webp' , '.bmp' ] . some ( ext = >
path . toLowerCase ( ) . endsWith ( ext )
) ;
} , [ ] ) ;
2025-12-06 14:08:48 +08:00
// 加载缩略图(使用 resolvedPath)
2025-11-29 23:00:48 +08:00
useEffect ( ( ) = > {
2025-12-06 14:08:48 +08:00
if ( resolvedPath && isImageAsset ( resolvedPath ) ) {
2025-11-29 23:00:48 +08:00
// 获取项目路径并构建完整路径
const projectService = Core . services . tryResolve ( ProjectService ) ;
const projectPath = projectService ? . getCurrentProject ( ) ? . path ;
if ( projectPath ) {
// 构建完整的文件路径
2025-12-06 14:08:48 +08:00
const fullPath = resolvedPath . startsWith ( '/' ) || resolvedPath . includes ( ':' )
? resolvedPath
: ` ${ projectPath } / ${ resolvedPath } ` ;
2025-11-29 23:00:48 +08:00
try {
const url = convertFileSrc ( fullPath ) ;
setThumbnailUrl ( url ) ;
} catch {
setThumbnailUrl ( null ) ;
}
} else {
2025-12-06 14:08:48 +08:00
// 没有项目路径时,尝试直接使用 resolvedPath
2025-11-29 23:00:48 +08:00
try {
2025-12-06 14:08:48 +08:00
const url = convertFileSrc ( resolvedPath ) ;
2025-11-29 23:00:48 +08:00
setThumbnailUrl ( url ) ;
} catch {
setThumbnailUrl ( null ) ;
}
}
} else {
setThumbnailUrl ( null ) ;
}
2025-12-06 14:08:48 +08:00
} , [ resolvedPath , isImageAsset ] ) ;
2025-11-29 23:00:48 +08:00
2025-11-19 14:54:03 +08:00
const handleDragEnter = useCallback ( ( e : React.DragEvent ) = > {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
if ( ! readonly ) {
setIsDragging ( true ) ;
}
} , [ readonly ] ) ;
const handleDragLeave = useCallback ( ( e : React.DragEvent ) = > {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
setIsDragging ( false ) ;
} , [ ] ) ;
const handleDragOver = useCallback ( ( e : React.DragEvent ) = > {
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
} , [ ] ) ;
2025-12-13 19:44:08 +08:00
const handleDrop = useCallback ( async ( e : React.DragEvent ) = > {
2025-11-19 14:54:03 +08:00
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
setIsDragging ( false ) ;
2025-12-13 19:44:08 +08:00
if ( readonly || ! assetRegistry ) return ;
2025-11-19 14:54:03 +08:00
2025-12-06 14:08:48 +08:00
// Try to get GUID from drag data first
const assetGuid = e . dataTransfer . getData ( 'asset-guid' ) ;
if ( assetGuid && isGUID ( assetGuid ) ) {
// Validate extension if needed
2025-12-13 19:44:08 +08:00
if ( fileExtension ) {
2025-12-06 14:08:48 +08:00
const path = assetRegistry . getPathByGuid ( assetGuid ) ;
if ( path && ! path . endsWith ( fileExtension ) ) {
return ; // Extension mismatch
}
}
onChange ( assetGuid ) ;
return ;
}
2025-12-13 19:44:08 +08:00
// Handle asset-path: convert to GUID or register
2025-12-06 14:08:48 +08:00
const assetPath = e . dataTransfer . getData ( 'asset-path' ) ;
if ( assetPath && ( ! fileExtension || assetPath . endsWith ( fileExtension ) ) ) {
2025-12-13 19:44:08 +08:00
// Path might be absolute, convert to relative first
let relativePath = assetPath ;
if ( assetPath . includes ( ':' ) || assetPath . startsWith ( '/' ) ) {
relativePath = assetRegistry . absoluteToRelative ( assetPath ) || assetPath ;
}
// 尝试多种路径格式 | Try multiple path formats
const pathVariants = [ relativePath , relativePath . replace ( /\\/g , '/' ) ] ;
for ( const variant of pathVariants ) {
const guid = assetRegistry . getGuidByPath ( variant ) ;
2025-12-06 14:08:48 +08:00
if ( guid ) {
onChange ( guid ) ;
return ;
}
}
2025-12-13 19:44:08 +08:00
// GUID 不存在,尝试注册 | GUID not found, try to register
const absolutePath = assetPath . includes ( ':' ) ? assetPath : null ;
if ( absolutePath ) {
try {
const newGuid = await assetRegistry . registerAsset ( absolutePath ) ;
if ( newGuid ) {
console . log ( ` [AssetField] Registered dropped asset with GUID: ${ newGuid } ` ) ;
onChange ( newGuid ) ;
return ;
}
} catch ( error ) {
console . error ( ` [AssetField] Failed to register dropped asset: ` , error ) ;
}
}
2025-11-19 14:54:03 +08:00
2025-12-13 19:44:08 +08:00
console . error ( ` [AssetField] Cannot use dropped asset without GUID: " ${ assetPath } " ` ) ;
2025-11-19 14:54:03 +08:00
return ;
}
2025-12-13 19:44:08 +08:00
// Handle text/plain drops (might be GUID or path)
2025-11-19 14:54:03 +08:00
const text = e . dataTransfer . getData ( 'text/plain' ) ;
if ( text && ( ! fileExtension || text . endsWith ( fileExtension ) ) ) {
2025-12-13 19:44:08 +08:00
if ( isGUID ( text ) ) {
onChange ( text ) ;
return ;
}
// Try to get GUID from path
const pathVariants = [ text , text . replace ( /\\/g , '/' ) ] ;
for ( const variant of pathVariants ) {
const guid = assetRegistry . getGuidByPath ( variant ) ;
2025-12-06 14:08:48 +08:00
if ( guid ) {
onChange ( guid ) ;
return ;
}
}
2025-12-13 19:44:08 +08:00
console . error ( ` [AssetField] Cannot use dropped text without GUID: " ${ text } " ` ) ;
2025-11-19 14:54:03 +08:00
}
2025-12-06 14:08:48 +08:00
} , [ onChange , fileExtension , readonly , assetRegistry ] ) ;
2025-11-19 14:54:03 +08:00
2025-11-23 14:49:37 +08:00
const handleBrowse = useCallback ( ( ) = > {
2025-11-19 14:54:03 +08:00
if ( readonly ) return ;
2025-11-23 14:49:37 +08:00
setShowPicker ( true ) ;
} , [ readonly ] ) ;
2025-11-19 14:54:03 +08:00
2025-12-13 19:44:08 +08:00
const handlePickerSelect = useCallback ( async ( path : string ) = > {
// Convert path to GUID - 必须使用 GUID, 不能使用路径!
// Must use GUID, cannot use path!
if ( ! assetRegistry ) {
console . error ( ` [AssetField] AssetRegistry not available, cannot select asset ` ) ;
setShowPicker ( false ) ;
return ;
}
// Path might be absolute, convert to relative first
let relativePath = path ;
if ( path . includes ( ':' ) || path . startsWith ( '/' ) ) {
relativePath = assetRegistry . absoluteToRelative ( path ) || path ;
}
// 尝试多种路径格式 | Try multiple path formats
const pathVariants = [
relativePath ,
relativePath . replace ( /\\/g , '/' ) , // 统一为正斜杠
] ;
for ( const variant of pathVariants ) {
const guid = assetRegistry . getGuidByPath ( variant ) ;
2025-12-06 14:08:48 +08:00
if ( guid ) {
2025-12-13 19:44:08 +08:00
console . log ( ` [AssetField] Found GUID for path " ${ path } ": ${ guid } ` ) ;
2025-12-06 14:08:48 +08:00
onChange ( guid ) ;
setShowPicker ( false ) ;
return ;
}
}
2025-12-13 19:44:08 +08:00
// GUID 不存在,尝试注册资产(创建 .meta 文件)
// GUID not found, try to register asset (create .meta file)
console . warn ( ` [AssetField] GUID not found for path " ${ path } ", registering asset... ` ) ;
try {
// 使用绝对路径注册 | Register using absolute path
const absolutePath = path . includes ( ':' ) ? path : null ;
if ( absolutePath ) {
const newGuid = await assetRegistry . registerAsset ( absolutePath ) ;
if ( newGuid ) {
console . log ( ` [AssetField] Registered new asset with GUID: ${ newGuid } ` ) ;
onChange ( newGuid ) ;
setShowPicker ( false ) ;
return ;
}
}
} catch ( error ) {
console . error ( ` [AssetField] Failed to register asset: ` , error ) ;
}
// 注册失败,不能使用路径(会导致打包后找不到)
// Registration failed, cannot use path (will fail after build)
console . error ( ` [AssetField] Cannot use asset without GUID: " ${ path } ". Please ensure the asset is in a managed directory (assets/, scripts/, scenes/). ` ) ;
2025-11-23 14:49:37 +08:00
setShowPicker ( false ) ;
2025-12-06 14:08:48 +08:00
} , [ onChange , assetRegistry ] ) ;
2025-11-19 14:54:03 +08:00
const handleClear = useCallback ( ( ) = > {
if ( ! readonly ) {
onChange ( null ) ;
}
} , [ onChange , readonly ] ) ;
2025-12-06 14:08:48 +08:00
const getFileName = ( path : string | null ) = > {
if ( ! path ) return placeholder ;
2025-11-19 14:54:03 +08:00
const parts = path . split ( /[\\/]/ ) ;
return parts [ parts . length - 1 ] ;
} ;
2025-12-06 14:08:48 +08:00
// Display name uses resolvedPath
const displayName = resolvedPath ? getFileName ( resolvedPath ) : placeholder ;
2025-11-19 14:54:03 +08:00
return (
< div className = "asset-field" >
2025-11-23 14:49:37 +08:00
{ label && < label className = "asset-field__label" > { label } < / label > }
2025-11-29 23:00:48 +08:00
< div className = "asset-field__content" >
{ /* 缩略图预览 */ }
2025-11-19 14:54:03 +08:00
< div
2025-11-29 23:00:48 +08:00
className = { ` asset-field__thumbnail ${ isDragging ? 'dragging' : '' } ` }
2025-11-19 14:54:03 +08:00
onDragEnter = { handleDragEnter }
onDragLeave = { handleDragLeave }
onDragOver = { handleDragOver }
onDrop = { handleDrop }
>
2025-11-29 23:00:48 +08:00
{ thumbnailUrl ? (
< img src = { thumbnailUrl } alt = "" / >
) : (
< Image size = { 18 } className = "asset-field__thumbnail-icon" / >
2025-11-19 14:54:03 +08:00
) }
2025-11-29 23:00:48 +08:00
< / div >
2025-11-19 14:54:03 +08:00
2025-11-29 23:00:48 +08:00
{ /* 右侧区域 */ }
< div className = "asset-field__right" >
{ /* 下拉选择框 */ }
< div
ref = { inputRef }
2025-12-06 14:08:48 +08:00
className = { ` asset-field__dropdown ${ resolvedPath ? 'has-value' : '' } ${ isDragging ? 'dragging' : '' } ` }
2025-11-29 23:00:48 +08:00
onClick = { ! readonly ? handleBrowse : undefined }
onDragEnter = { handleDragEnter }
onDragLeave = { handleDragLeave }
onDragOver = { handleDragOver }
onDrop = { handleDrop }
2025-12-06 14:08:48 +08:00
title = { resolvedPath || placeholder }
2025-11-29 23:00:48 +08:00
>
< span className = "asset-field__value" >
2025-12-06 14:08:48 +08:00
{ displayName }
2025-11-29 23:00:48 +08:00
< / span >
< ChevronDown size = { 12 } className = "asset-field__dropdown-arrow" / >
< / div >
{ /* 操作按钮行 */ }
< div className = "asset-field__actions" >
{ /* 定位按钮 */ }
2025-12-06 14:08:48 +08:00
{ resolvedPath && onNavigate && (
2025-11-29 23:00:48 +08:00
< button
className = "asset-field__btn"
onClick = { ( e ) = > {
e . stopPropagation ( ) ;
2025-12-06 14:08:48 +08:00
onNavigate ( resolvedPath ) ;
2025-11-29 23:00:48 +08:00
} }
title = "Locate in Asset Browser"
>
< Navigation size = { 12 } / >
< / button >
) }
2025-12-06 14:08:48 +08:00
{ /* 复制路径按钮 - copy path, not GUID */ }
{ resolvedPath && (
2025-11-29 23:00:48 +08:00
< button
className = "asset-field__btn"
onClick = { ( e ) = > {
e . stopPropagation ( ) ;
2025-12-06 14:08:48 +08:00
navigator . clipboard . writeText ( resolvedPath ) ;
2025-11-29 23:00:48 +08:00
} }
title = "Copy Path"
>
< Copy size = { 12 } / >
< / button >
) }
{ /* 清除按钮 */ }
{ value && ! readonly && (
< button
className = "asset-field__btn asset-field__btn--clear"
onClick = { ( e ) = > {
e . stopPropagation ( ) ;
handleClear ( ) ;
} }
title = "Clear"
>
< X size = { 12 } / >
< / button >
) }
< / div >
2025-11-19 14:54:03 +08:00
< / div >
< / div >
2025-11-23 14:49:37 +08:00
< AssetPickerDialog
isOpen = { showPicker }
onClose = { ( ) = > setShowPicker ( false ) }
onSelect = { handlePickerSelect }
title = "Select Asset"
fileExtensions = { fileExtension ? [ fileExtension ] : [ ] }
/ >
2025-11-19 14:54:03 +08:00
< / div >
) ;
2025-11-23 14:49:37 +08:00
}