Skip to content

Commit

Permalink
add: GLTF screenshots
Browse files Browse the repository at this point in the history
  • Loading branch information
nicoecheza committed Oct 18, 2023
1 parent 6487fd5 commit cb6286d
Show file tree
Hide file tree
Showing 27 changed files with 306 additions and 56 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.AssetPreview {
display: flex;
align-items: center;
justify-content: center;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import * as React from 'react'
import { PreviewCamera, PreviewProjection } from '@dcl/schemas'
import { WearablePreview } from 'decentraland-ui'
import { IoIosImage } from 'react-icons/io'

import { isAsset as isGltf } from '../EntityInspector/GltfInspector/utils'
import { toWearableWithBlobs } from './utils'
import { Props } from './types'

import './AssetPreview.css'

export function AssetPreview({ value, onScreenshot }: Props) {
const onLoad = React.useCallback(() => {
const wp = WearablePreview.createController(value.name)
void wp.scene.getScreenshot(1024, 1024).then(($) => onScreenshot($))
}, [])

return (
<div className="AssetPreview">
{isGltf(value.name) ? (
<WearablePreview
id={value.name}
blob={toWearableWithBlobs(value)}
disableAutoRotate
disableBackground
projection={PreviewProjection.ORTHOGRAPHIC}
camera={PreviewCamera.STATIC}
onLoad={onLoad}
/>
) : (
<IoIosImage />
)}
</div>
)
}
2 changes: 2 additions & 0 deletions packages/@dcl/inspector/src/components/AssetPreview/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { AssetPreview } from './AssetPreview'
export { AssetPreview }
4 changes: 4 additions & 0 deletions packages/@dcl/inspector/src/components/AssetPreview/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type Props = {
value: File
onScreenshot: (value: string) => void
}
32 changes: 32 additions & 0 deletions packages/@dcl/inspector/src/components/AssetPreview/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { BodyShape, WearableCategory, WearableWithBlobs } from '@dcl/schemas'

export function toWearableWithBlobs(file: File): WearableWithBlobs {
return {
id: file.name,
name: '',
description: '',
image: '',
thumbnail: '',
i18n: [],
data: {
category: WearableCategory.HAT,
hides: [],
replaces: [],
tags: [],
representations: [
{
bodyShapes: [BodyShape.MALE, BodyShape.FEMALE],
mainFile: file.name,
contents: [
{
key: file.name,
blob: file
}
],
overrideHides: [],
overrideReplaces: []
}
]
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,8 @@
.ImportAsset .file-container .error {
color: var(--primary);
}

.ImportAsset .file-container .AssetPreview {
width: 100px;
height: 100px;
}
44 changes: 32 additions & 12 deletions packages/@dcl/inspector/src/components/ImportAsset/ImportAsset.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,23 @@
import { GLTFValidation } from '@babylonjs/loaders'
import React, { useCallback, useState } from 'react'
import { HiOutlineUpload } from 'react-icons/hi'
import { RxCross2, RxReload } from 'react-icons/rx'
import { IoIosImage } from 'react-icons/io'
import classNames from 'classnames'

import FileInput from '../FileInput'
import { Container } from '../Container'
import { TextField } from '../EntityInspector/TextField'
import { Block } from '../Block'
import { Button } from '../Button'
import { removeBasePath } from '../../lib/logic/remove-base-path'

import { GLTFValidation } from '@babylonjs/loaders'

import './ImportAsset.css'
import classNames from 'classnames'
import { DIRECTORY, withAssetDir } from '../../lib/data-layer/host/fs-utils'
import { importAsset } from '../../redux/data-layer'
import { DIRECTORY, transformBase64ResourceToBinary, withAssetDir } from '../../lib/data-layer/host/fs-utils'
import { importAsset, saveThumbnail } from '../../redux/data-layer'
import { useAppDispatch, useAppSelector } from '../../redux/hooks'
import { selectAssetCatalog } from '../../redux/app'
import { getRandomMnemonic } from './utils'
import { AssetPreview } from '../AssetPreview'

import './ImportAsset.css'

const ONE_MB_IN_BYTES = 1_048_576
const ONE_GB_IN_BYTES = ONE_MB_IN_BYTES * 1024
Expand Down Expand Up @@ -92,17 +91,19 @@ const ImportAsset: React.FC<PropTypes> = ({ onSave }) => {
const files = useAppSelector(selectAssetCatalog)

const [file, setFile] = useState<File>()
const [thumbnail, setThumbnail] = useState<string | null>(null)
const [validationError, setValidationError] = useState<ValidationError>(null)
const [assetName, setAssetName] = useState<string>('')
const [assetExtension, setAssetExtension] = useState<string>('')
const { basePath, assets } = files ?? { basePath: '', assets: [] }

const handleDrop = async (acceptedFiles: File[]) => {
const handleDrop = (acceptedFiles: File[]) => {
// TODO: handle zip file. GLB with multiple external image references
const file = acceptedFiles[0]
if (!file) return
setFile(file)
setValidationError(null)
setThumbnail(null)
const normalizedName = file.name.trim().replaceAll(' ', '_').toLowerCase()
const splitName = normalizedName.split('.')
const extensionName = splitName.pop()
Expand All @@ -127,16 +128,27 @@ const ImportAsset: React.FC<PropTypes> = ({ onSave }) => {
return
}

const basePath = withAssetDir(DIRECTORY.SCENE)
const content: Map<string, Uint8Array> = new Map()
content.set(assetName + '.' + assetExtension, new Uint8Array(binary))
const fullName = assetName + '.' + assetExtension
content.set(fullName, new Uint8Array(binary))

dispatch(
importAsset({
content,
basePath: withAssetDir(DIRECTORY.SCENE),
basePath,
assetPackageName: ''
})
)

if (thumbnail) {
dispatch(
saveThumbnail({
content: transformBase64ResourceToBinary(thumbnail),
path: `${DIRECTORY.THUMBNAILS}/${assetName}.png`
})
)
}
onSave()
}
reader.readAsArrayBuffer(file)
Expand All @@ -146,6 +158,7 @@ const ImportAsset: React.FC<PropTypes> = ({ onSave }) => {
e.stopPropagation()
setFile(undefined)
setValidationError(null)
setThumbnail(null)
}

const handleNameChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
Expand All @@ -170,6 +183,13 @@ const ImportAsset: React.FC<PropTypes> = ({ onSave }) => {
setAssetName(name)
}, [assetName])

const handleScreenshot = useCallback(
(value: string) => {
setThumbnail(value)
},
[file]
)

return (
<div className="ImportAsset">
<FileInput disabled={!!file} onDrop={handleDrop} accept={ACCEPTED_FILE_TYPES}>
Expand All @@ -190,7 +210,7 @@ const ImportAsset: React.FC<PropTypes> = ({ onSave }) => {
<div className="remove-icon" onClick={removeFile}>
<RxCross2 />
</div>
<IoIosImage />
<AssetPreview value={file} onScreenshot={handleScreenshot} />
<div className="file-title">{file.name}</div>
</Container>
<div className={classNames({ error: invalidName })}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ import { AssetNodeFolder } from './types'

import './ProjectAssetExplorer.css'
import { useAppSelector } from '../../redux/hooks'
import { selectAssetCatalog } from '../../redux/app'
import { selectAssetCatalog, selectThumbnails } from '../../redux/app'

function ProjectAssetExplorer() {
const files = useAppSelector(selectAssetCatalog)
const { tree } = useAssetTree(files ?? { basePath: '', assets: [] })
const files = useAppSelector(selectAssetCatalog) ?? { basePath: '', assets: [] }
const thumbnails = useAppSelector(selectThumbnails)
const { tree } = useAssetTree(files)
const folders = tree.children.filter((item) => item.type === 'folder') as AssetNodeFolder[]

return <ProjectView folders={folders} />
return <ProjectView folders={folders} thumbnails={thumbnails} />
}

export default React.memo(ProjectAssetExplorer)
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ function noop() {}

type Props = {
folders: AssetNodeFolder[]
thumbnails: { path: string; content: Uint8Array }[]
}

interface ModalState {
Expand All @@ -35,7 +36,7 @@ export type TreeNode = Omit<AssetNode, 'children'> & { children?: string[]; matc

const FilesTree = Tree<string>()

function ProjectView({ folders }: Props) {
function ProjectView({ folders, thumbnails }: Props) {
const sdk = useSdk()
const dispatch = useAppDispatch()
const [open, setOpen] = useState(new Set<string>())
Expand Down Expand Up @@ -178,6 +179,15 @@ function ProjectView({ folders }: Props) {
[tree, search]
)

const getThumbnail = useCallback(
(value: string) => {
const [name] = value.split('.')
const thumbnail = thumbnails.find(($) => $.path.endsWith(name + '.png'))
return thumbnail?.content
},
[thumbnails]
)

return (
<>
<Modal isOpen={!!modal?.isOpen} onRequestClose={handleModalClose} className="RemoveAsset">
Expand Down Expand Up @@ -236,6 +246,7 @@ function ProjectView({ folders }: Props) {
getDragContext={handleDragContext}
onSelect={handleClickFolder($)}
onRemove={handleRemove}
getThumbnail={getThumbnail}
dndType={DRAG_N_DROP_ASSET_KEY}
/>
))
Expand All @@ -247,6 +258,7 @@ function ProjectView({ folders }: Props) {
getDragContext={handleDragContext}
onSelect={handleClickFolder(selectedTreeNode.name)}
onRemove={handleRemove}
getThumbnail={getThumbnail}
dndType={DRAG_N_DROP_ASSET_KEY}
/>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
font-size: 10px;
}

.Tile svg {
.Tile svg, .Tile img {
width: 42px;
height: 42px;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { IoIosImage } from 'react-icons/io'
import { Item as MenuItem } from 'react-contexify'
import { useDrag } from 'react-dnd'

import { transformBinaryToBase64Resource } from '../../../lib/data-layer/host/fs-utils'
import { ContextMenu as Menu } from '../../ContexMenu'
import FolderIcon from '../../Icons/Folder'
import { withContextMenu } from '../../../hoc/withContextMenu'
Expand All @@ -13,7 +14,7 @@ import { Props } from './types'
import './Tile.css'

export const Tile = withContextMenu<Props>(
({ valueId, value, getDragContext, onSelect, onRemove, contextMenuId, dndType }) => {
({ valueId, value, getDragContext, onSelect, onRemove, contextMenuId, dndType, getThumbnail }) => {
const { handleAction } = useContextMenu()

const [, drag] = useDrag(() => ({ type: dndType, item: { value: valueId, context: getDragContext() } }), [valueId])
Expand All @@ -24,6 +25,13 @@ export const Tile = withContextMenu<Props>(

if (!value) return null

const renderThumbnail = () => {
if (value.type === 'folder') return <FolderIcon />
const thumbnail = getThumbnail(value.name)
if (thumbnail) return <img src={transformBinaryToBase64Resource(thumbnail)} alt={value.name} />
return <IoIosImage />
}

return (
<>
{/* TODO: support removing folders */}
Expand All @@ -43,7 +51,7 @@ export const Tile = withContextMenu<Props>(
data-test-id={valueId}
data-test-label={value.name}
>
{value.type === 'folder' ? <FolderIcon /> : <IoIosImage />}
{renderThumbnail()}
<span>{value.name}</span>
</div>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export interface Props {
onSelect: () => void
onRemove: (value: string) => void
dndType: string
getThumbnail: (value: string) => Uint8Array | undefined
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { CoreComponents } from '../../lib/sdk/components'
export interface IAsset {
src: string
type: 'unknown' | 'gltf' | 'composite' | 'audio'
thumbnail?: string
id?: string
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ const mapError = {
[ErrorType.Undo]: 'Undo failed.',
[ErrorType.Redo]: 'Redo failed.',
[ErrorType.ImportAsset]: 'Failed to import new asset.',
[ErrorType.RemoveAsset]: 'Failed to remove asset.'
[ErrorType.RemoveAsset]: 'Failed to remove asset.',
[ErrorType.SaveThumbnail]: 'Failed to save thumbnail.',
[ErrorType.GetThumbnails]: 'Failed to get thumbnails.'
}

const SocketConnection: React.FC = () => {
Expand Down
Loading

0 comments on commit cb6286d

Please sign in to comment.