From aee2af24f7f2f91bcfb447090409be23c81f8049 Mon Sep 17 00:00:00 2001 From: Nicolas Echezarreta Date: Tue, 10 Oct 2023 16:55:48 -0300 Subject: [PATCH] add: GLTF screenshots --- .../components/AssetPreview/AssetPreview.css | 5 +++ .../components/AssetPreview/AssetPreview.tsx | 35 +++++++++++++++ .../src/components/AssetPreview/index.ts | 2 + .../src/components/AssetPreview/types.ts | 4 ++ .../src/components/AssetPreview/utils.ts | 32 ++++++++++++++ .../components/ImportAsset/ImportAsset.css | 5 +++ .../components/ImportAsset/ImportAsset.tsx | 44 ++++++++++++++----- .../ProjectAssetExplorer.tsx | 9 ++-- .../ProjectAssetExplorer/ProjectView.tsx | 14 +++++- .../ProjectAssetExplorer/Tile/Tile.css | 2 +- .../ProjectAssetExplorer/Tile/Tile.tsx | 12 ++++- .../ProjectAssetExplorer/Tile/types.ts | 1 + .../components/ProjectAssetExplorer/types.ts | 1 - .../SocketConnection/SocketConnection.tsx | 4 +- .../sdkComponents/gltf-container.ts | 27 ++++++++---- .../inspector/src/lib/babylon/setup/input.ts | 1 + .../lib/data-layer/client/feeded-local-fs.ts | 4 ++ .../src/lib/data-layer/host/fs-utils.ts | 42 ++++++++++++------ .../src/lib/data-layer/host/rpc-methods.ts | 22 +++++++++- .../src/lib/data-layer/proto/data-layer.proto | 22 +++++++++- .../src/lib/logic/storage/in-memory.ts | 1 + .../@dcl/inspector/src/redux/app/index.ts | 12 +++-- .../inspector/src/redux/data-layer/index.ts | 14 ++++-- .../src/redux/data-layer/sagas/connected.ts | 3 +- .../redux/data-layer/sagas/get-thumbnails.ts | 19 ++++++++ .../src/redux/data-layer/sagas/index.ts | 8 +++- .../redux/data-layer/sagas/save-thumbnail.ts | 17 +++++++ 27 files changed, 306 insertions(+), 56 deletions(-) create mode 100644 packages/@dcl/inspector/src/components/AssetPreview/AssetPreview.css create mode 100644 packages/@dcl/inspector/src/components/AssetPreview/AssetPreview.tsx create mode 100644 packages/@dcl/inspector/src/components/AssetPreview/index.ts create mode 100644 packages/@dcl/inspector/src/components/AssetPreview/types.ts create mode 100644 packages/@dcl/inspector/src/components/AssetPreview/utils.ts create mode 100644 packages/@dcl/inspector/src/redux/data-layer/sagas/get-thumbnails.ts create mode 100644 packages/@dcl/inspector/src/redux/data-layer/sagas/save-thumbnail.ts diff --git a/packages/@dcl/inspector/src/components/AssetPreview/AssetPreview.css b/packages/@dcl/inspector/src/components/AssetPreview/AssetPreview.css new file mode 100644 index 000000000..5d5d285fe --- /dev/null +++ b/packages/@dcl/inspector/src/components/AssetPreview/AssetPreview.css @@ -0,0 +1,5 @@ +.AssetPreview { + display: flex; + align-items: center; + justify-content: center; +} diff --git a/packages/@dcl/inspector/src/components/AssetPreview/AssetPreview.tsx b/packages/@dcl/inspector/src/components/AssetPreview/AssetPreview.tsx new file mode 100644 index 000000000..d301c1d4e --- /dev/null +++ b/packages/@dcl/inspector/src/components/AssetPreview/AssetPreview.tsx @@ -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 ( +
+ {isGltf(value.name) ? ( + + ) : ( + + )} +
+ ) +} diff --git a/packages/@dcl/inspector/src/components/AssetPreview/index.ts b/packages/@dcl/inspector/src/components/AssetPreview/index.ts new file mode 100644 index 000000000..ead3266ee --- /dev/null +++ b/packages/@dcl/inspector/src/components/AssetPreview/index.ts @@ -0,0 +1,2 @@ +import { AssetPreview } from './AssetPreview' +export { AssetPreview } diff --git a/packages/@dcl/inspector/src/components/AssetPreview/types.ts b/packages/@dcl/inspector/src/components/AssetPreview/types.ts new file mode 100644 index 000000000..04bc31c61 --- /dev/null +++ b/packages/@dcl/inspector/src/components/AssetPreview/types.ts @@ -0,0 +1,4 @@ +export type Props = { + value: File + onScreenshot: (value: string) => void +} diff --git a/packages/@dcl/inspector/src/components/AssetPreview/utils.ts b/packages/@dcl/inspector/src/components/AssetPreview/utils.ts new file mode 100644 index 000000000..225ae23e2 --- /dev/null +++ b/packages/@dcl/inspector/src/components/AssetPreview/utils.ts @@ -0,0 +1,32 @@ +import { BodyShape, WearableCategory, WearableWithBlobs } from '@dcl/schemas' + +export function toWearableWithBlobs(file: File): WearableWithBlobs { + return { + id: 'some-id', + name: '', + description: '', + image: '', + thumbnail: '', + i18n: [], + data: { + category: WearableCategory.HAT, + hides: [], + replaces: [], + tags: [], + representations: [ + { + bodyShapes: [BodyShape.MALE, BodyShape.FEMALE], + mainFile: 'model.glb', + contents: [ + { + key: 'model.glb', + blob: file + } + ], + overrideHides: [], + overrideReplaces: [] + } + ] + } + } +} diff --git a/packages/@dcl/inspector/src/components/ImportAsset/ImportAsset.css b/packages/@dcl/inspector/src/components/ImportAsset/ImportAsset.css index bfae1aa3d..59c4b93a9 100644 --- a/packages/@dcl/inspector/src/components/ImportAsset/ImportAsset.css +++ b/packages/@dcl/inspector/src/components/ImportAsset/ImportAsset.css @@ -113,3 +113,8 @@ .ImportAsset .file-container .error { color: var(--primary); } + +.ImportAsset .file-container .AssetPreview { + width: 100px; + height: 100px; +} diff --git a/packages/@dcl/inspector/src/components/ImportAsset/ImportAsset.tsx b/packages/@dcl/inspector/src/components/ImportAsset/ImportAsset.tsx index 1b5151916..57aac68e6 100644 --- a/packages/@dcl/inspector/src/components/ImportAsset/ImportAsset.tsx +++ b/packages/@dcl/inspector/src/components/ImportAsset/ImportAsset.tsx @@ -1,7 +1,8 @@ +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' @@ -9,16 +10,14 @@ 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 @@ -92,17 +91,19 @@ const ImportAsset: React.FC = ({ onSave }) => { const files = useAppSelector(selectAssetCatalog) const [file, setFile] = useState() + const [thumbnail, setThumbnail] = useState(null) const [validationError, setValidationError] = useState(null) const [assetName, setAssetName] = useState('') const [assetExtension, setAssetExtension] = useState('') 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() @@ -127,16 +128,27 @@ const ImportAsset: React.FC = ({ onSave }) => { return } + const basePath = withAssetDir(DIRECTORY.SCENE) const content: Map = 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) @@ -146,6 +158,7 @@ const ImportAsset: React.FC = ({ onSave }) => { e.stopPropagation() setFile(undefined) setValidationError(null) + setThumbnail(null) } const handleNameChange = useCallback((event: React.ChangeEvent) => { @@ -170,6 +183,13 @@ const ImportAsset: React.FC = ({ onSave }) => { setAssetName(name) }, [assetName]) + const handleScreenshot = useCallback( + (value: string) => { + setThumbnail(value) + }, + [file] + ) + return (
@@ -190,7 +210,7 @@ const ImportAsset: React.FC = ({ onSave }) => {
- +
{file.name}
diff --git a/packages/@dcl/inspector/src/components/ProjectAssetExplorer/ProjectAssetExplorer.tsx b/packages/@dcl/inspector/src/components/ProjectAssetExplorer/ProjectAssetExplorer.tsx index 6afca67c5..85c75f269 100644 --- a/packages/@dcl/inspector/src/components/ProjectAssetExplorer/ProjectAssetExplorer.tsx +++ b/packages/@dcl/inspector/src/components/ProjectAssetExplorer/ProjectAssetExplorer.tsx @@ -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 + return } export default React.memo(ProjectAssetExplorer) diff --git a/packages/@dcl/inspector/src/components/ProjectAssetExplorer/ProjectView.tsx b/packages/@dcl/inspector/src/components/ProjectAssetExplorer/ProjectView.tsx index 1399e7d39..4138edd49 100644 --- a/packages/@dcl/inspector/src/components/ProjectAssetExplorer/ProjectView.tsx +++ b/packages/@dcl/inspector/src/components/ProjectAssetExplorer/ProjectView.tsx @@ -19,6 +19,7 @@ function noop() {} type Props = { folders: AssetNodeFolder[] + thumbnails: { path: string; content: Uint8Array }[] } interface ModalState { @@ -35,7 +36,7 @@ export type TreeNode = Omit & { children?: string[]; matc const FilesTree = Tree() -function ProjectView({ folders }: Props) { +function ProjectView({ folders, thumbnails }: Props) { const sdk = useSdk() const dispatch = useAppDispatch() const [open, setOpen] = useState(new Set()) @@ -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 ( <> @@ -236,6 +246,7 @@ function ProjectView({ folders }: Props) { getDragContext={handleDragContext} onSelect={handleClickFolder($)} onRemove={handleRemove} + getThumbnail={getThumbnail} dndType={DRAG_N_DROP_ASSET_KEY} /> )) @@ -247,6 +258,7 @@ function ProjectView({ folders }: Props) { getDragContext={handleDragContext} onSelect={handleClickFolder(selectedTreeNode.name)} onRemove={handleRemove} + getThumbnail={getThumbnail} dndType={DRAG_N_DROP_ASSET_KEY} /> )} diff --git a/packages/@dcl/inspector/src/components/ProjectAssetExplorer/Tile/Tile.css b/packages/@dcl/inspector/src/components/ProjectAssetExplorer/Tile/Tile.css index bd81ab7d7..d68f51c19 100644 --- a/packages/@dcl/inspector/src/components/ProjectAssetExplorer/Tile/Tile.css +++ b/packages/@dcl/inspector/src/components/ProjectAssetExplorer/Tile/Tile.css @@ -21,7 +21,7 @@ font-size: 10px; } -.Tile svg { +.Tile svg, .Tile img { width: 42px; height: 42px; } diff --git a/packages/@dcl/inspector/src/components/ProjectAssetExplorer/Tile/Tile.tsx b/packages/@dcl/inspector/src/components/ProjectAssetExplorer/Tile/Tile.tsx index ec0491bf3..0501b1dbf 100644 --- a/packages/@dcl/inspector/src/components/ProjectAssetExplorer/Tile/Tile.tsx +++ b/packages/@dcl/inspector/src/components/ProjectAssetExplorer/Tile/Tile.tsx @@ -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' @@ -13,7 +14,7 @@ import { Props } from './types' import './Tile.css' export const Tile = withContextMenu( - ({ 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]) @@ -24,6 +25,13 @@ export const Tile = withContextMenu( if (!value) return null + const renderThumbnail = () => { + if (value.type === 'folder') return + const thumbnail = getThumbnail(value.name) + if (thumbnail) return {value.name} + return + } + return ( <> {/* TODO: support removing folders */} @@ -43,7 +51,7 @@ export const Tile = withContextMenu( data-test-id={valueId} data-test-label={value.name} > - {value.type === 'folder' ? : } + {renderThumbnail()} {value.name}
diff --git a/packages/@dcl/inspector/src/components/ProjectAssetExplorer/Tile/types.ts b/packages/@dcl/inspector/src/components/ProjectAssetExplorer/Tile/types.ts index 66ba8f355..0b7e504d2 100644 --- a/packages/@dcl/inspector/src/components/ProjectAssetExplorer/Tile/types.ts +++ b/packages/@dcl/inspector/src/components/ProjectAssetExplorer/Tile/types.ts @@ -7,4 +7,5 @@ export interface Props { onSelect: () => void onRemove: (value: string) => void dndType: string + getThumbnail: (value: string) => Uint8Array | undefined } diff --git a/packages/@dcl/inspector/src/components/ProjectAssetExplorer/types.ts b/packages/@dcl/inspector/src/components/ProjectAssetExplorer/types.ts index 315f95a3e..301d90f7e 100644 --- a/packages/@dcl/inspector/src/components/ProjectAssetExplorer/types.ts +++ b/packages/@dcl/inspector/src/components/ProjectAssetExplorer/types.ts @@ -4,7 +4,6 @@ import { CoreComponents } from '../../lib/sdk/components' export interface IAsset { src: string type: 'unknown' | 'gltf' | 'composite' | 'audio' - thumbnail?: string } export interface FolderCellProp { diff --git a/packages/@dcl/inspector/src/components/Warnings/SocketConnection/SocketConnection.tsx b/packages/@dcl/inspector/src/components/Warnings/SocketConnection/SocketConnection.tsx index be33caef3..55b5b5798 100644 --- a/packages/@dcl/inspector/src/components/Warnings/SocketConnection/SocketConnection.tsx +++ b/packages/@dcl/inspector/src/components/Warnings/SocketConnection/SocketConnection.tsx @@ -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 = () => { diff --git a/packages/@dcl/inspector/src/lib/babylon/decentraland/sdkComponents/gltf-container.ts b/packages/@dcl/inspector/src/lib/babylon/decentraland/sdkComponents/gltf-container.ts index 415550b6c..db383e20a 100644 --- a/packages/@dcl/inspector/src/lib/babylon/decentraland/sdkComponents/gltf-container.ts +++ b/packages/@dcl/inspector/src/lib/babylon/decentraland/sdkComponents/gltf-container.ts @@ -118,14 +118,13 @@ async function tryLoadGltfAsync(sceneId: string, entity: EcsEntity, filePath: st const file = new File([content], finalSrc) const extension = filePath.toLowerCase().endsWith('.gltf') ? '.gltf' : '.glb' - BABYLON.SceneLoader.LoadAssetContainer( - '', + loadAssetContainer( file, entity.getScene(), (assetContainer) => { - processGLTFAssetContainer(assetContainer, entity) + processGLTFAssetContainer(assetContainer) - // Fin the main mesh and add it as the BasicShape.nameInEntity component. + // Find the main mesh and add it as the BasicShape.nameInEntity component. assetContainer.meshes .filter(($) => $.name === '__root__') .forEach((mesh) => { @@ -136,7 +135,7 @@ async function tryLoadGltfAsync(sceneId: string, entity: EcsEntity, filePath: st entity.gltfAssetContainer = assetContainer entity.resolveGltfPathLoading(filePath) }, - null, + undefined, (_scene, _message, _exception) => { console.error('Error while calling LoadAssetContainer: ', _message, _exception) entity.resolveGltfPathLoading(filePath) @@ -151,7 +150,19 @@ async function tryLoadGltfAsync(sceneId: string, entity: EcsEntity, filePath: st ) } -export function processGLTFAssetContainer(assetContainer: BABYLON.AssetContainer, entity: EcsEntity) { +export function loadAssetContainer( + file: File, + scene: BABYLON.Scene, + onSuccess?: (assetContainer: BABYLON.AssetContainer) => void, + onProgress?: (event: BABYLON.ISceneLoaderProgressEvent) => void, + onError?: (scene: BABYLON.Scene, message: string, exception?: any) => void, + pluginExtension?: string, + name?: string +) { + BABYLON.SceneLoader.LoadAssetContainer('', file, scene, onSuccess, onProgress, onError, pluginExtension, name) +} + +export function processGLTFAssetContainer(assetContainer: BABYLON.AssetContainer) { assetContainer.meshes.forEach((mesh) => { if (mesh instanceof BABYLON.Mesh) { if (mesh.geometry && !assetContainer.geometries.includes(mesh.geometry)) { @@ -172,7 +183,7 @@ export function processGLTFAssetContainer(assetContainer: BABYLON.AssetContainer }) }) - processColliders(assetContainer, entity) + processColliders(assetContainer) // Find all the materials from all the meshes and add to $.materials assetContainer.meshes.forEach((mesh) => { @@ -274,7 +285,7 @@ export function cleanupAssetContainer(scene: BABYLON.Scene, $: BABYLON.AssetCont } } -function processColliders($: BABYLON.AssetContainer, _entity: EcsEntity) { +function processColliders($: BABYLON.AssetContainer) { for (let i = 0; i < $.meshes.length; i++) { const mesh = $.meshes[i] diff --git a/packages/@dcl/inspector/src/lib/babylon/setup/input.ts b/packages/@dcl/inspector/src/lib/babylon/setup/input.ts index 784811d93..103f696e7 100644 --- a/packages/@dcl/inspector/src/lib/babylon/setup/input.ts +++ b/packages/@dcl/inspector/src/lib/babylon/setup/input.ts @@ -1,4 +1,5 @@ import * as BABYLON from '@babylonjs/core' + import { EcsEntity } from '../decentraland/EcsEntity' import { snapManager } from '../decentraland/snap-manager' import { keyState, Keys } from '../decentraland/keys' diff --git a/packages/@dcl/inspector/src/lib/data-layer/client/feeded-local-fs.ts b/packages/@dcl/inspector/src/lib/data-layer/client/feeded-local-fs.ts index 9393a02f0..e3a6558b7 100644 --- a/packages/@dcl/inspector/src/lib/data-layer/client/feeded-local-fs.ts +++ b/packages/@dcl/inspector/src/lib/data-layer/client/feeded-local-fs.ts @@ -176,6 +176,10 @@ export async function feededFileSystem(mappings: Record = builde const storage = createInMemoryStorage({ ...assets, + 'thumbnails/example.png': Buffer.from( + 'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII=', + 'base64' + ), 'assets/scene/main.composite': Buffer.from(JSON.stringify(composite), 'utf-8'), 'scene.json': Buffer.from(JSON.stringify(scene), 'utf-8') }) diff --git a/packages/@dcl/inspector/src/lib/data-layer/host/fs-utils.ts b/packages/@dcl/inspector/src/lib/data-layer/host/fs-utils.ts index 12215f01b..79d300a27 100644 --- a/packages/@dcl/inspector/src/lib/data-layer/host/fs-utils.ts +++ b/packages/@dcl/inspector/src/lib/data-layer/host/fs-utils.ts @@ -6,26 +6,31 @@ export async function getFilesInDirectory( files: string[], recursive: boolean = true, ignore: string[] = [] // This functionality can be extended, now only 'absolute' path can be ignored -) { - const currentDirFiles = await fs.readdir(dirPath) - for (const currentPath of currentDirFiles) { - if (ignore.includes(currentPath.name)) continue - - const slashIfRequire = (dirPath.length && !dirPath.endsWith('/') && '/') || '' - const fullPath = dirPath + slashIfRequire + currentPath.name - - if (currentPath.isDirectory && recursive) { - await getFilesInDirectory(fs, fullPath, files, recursive) - } else { - files.push(fullPath) +): Promise { + try { + const currentDirFiles = await fs.readdir(dirPath) + for (const currentPath of currentDirFiles) { + if (ignore.includes(currentPath.name)) continue + + const slashIfRequire = (dirPath.length && !dirPath.endsWith('/') && '/') || '' + const fullPath = dirPath + slashIfRequire + currentPath.name + + if (currentPath.isDirectory && recursive) { + await getFilesInDirectory(fs, fullPath, files, recursive) + } else { + files.push(fullPath) + } } + return files + } catch (_) { + return [] } - return files } export const DIRECTORY = { ASSETS: 'assets', - SCENE: 'scene' + SCENE: 'scene', + THUMBNAILS: 'thumbnails' } export const EXTENSIONS = ['.glb', '.png', '.composite', '.composite.bin', '.gltf', '.jpg', '.mp3', '.ogg', '.wav'] @@ -42,3 +47,12 @@ export function getFileName(fileName: string, ext: string) { export function getCurrentCompositePath() { return withAssetDir(`${DIRECTORY.SCENE}/main.composite`) } + +export function transformBinaryToBase64Resource(content: Uint8Array): string { + return `data:image/png;base64,${Buffer.from(content).toString('base64')}` +} + +export function transformBase64ResourceToBinary(base64Resource: string): Buffer { + const header = 'data:image/png;base64,' + return Buffer.from(base64Resource.slice(header.length), 'base64') +} diff --git a/packages/@dcl/inspector/src/lib/data-layer/host/rpc-methods.ts b/packages/@dcl/inspector/src/lib/data-layer/host/rpc-methods.ts index a89504ecd..62a1354b8 100644 --- a/packages/@dcl/inspector/src/lib/data-layer/host/rpc-methods.ts +++ b/packages/@dcl/inspector/src/lib/data-layer/host/rpc-methods.ts @@ -68,16 +68,34 @@ export async function initRpcMethods( throw new Error(`Couldn't find the asset ${req.path}`) }, + async getFiles({ path, ignore = [] }) { + const filesInDir = await getFilesInDirectory(fs, path, [], true, ignore) + const files = await Promise.all( + filesInDir.map(async ($) => ({ + path: $, + content: await fs.readFile($) + })) + ) + return { files } + }, + async saveFile({ path, content }) { + // TODO: overwrite exception? + await fs.writeFile(path, Buffer.from(content)) + return {} + }, + // TODO: we are calling this method in several sagas and considering + // that we could be using HTTP requests as data-layer mechanism + // this should be optimized async getAssetCatalog() { const ignore = ['.git', 'node_modules'] const basePath = withAssetDir() - const files = (await getFilesInDirectory(fs, basePath, [], true, ignore)).filter((item) => { + const assets = (await getFilesInDirectory(fs, basePath, [], true, ignore)).filter((item) => { const itemLower = item.toLowerCase() return EXTENSIONS.some((ext) => itemLower.endsWith(ext)) }) - return { basePath, assets: files.map(($) => ({ path: $ })) } + return { basePath, assets: assets.map(($) => ({ path: $ })) } }, /** * Import asset into the file system. diff --git a/packages/@dcl/inspector/src/lib/data-layer/proto/data-layer.proto b/packages/@dcl/inspector/src/lib/data-layer/proto/data-layer.proto index b064a4be2..80b4026bd 100644 --- a/packages/@dcl/inspector/src/lib/data-layer/proto/data-layer.proto +++ b/packages/@dcl/inspector/src/lib/data-layer/proto/data-layer.proto @@ -15,12 +15,30 @@ message AssetData { bytes data = 1; } +message GetFilesRequest { + string path = 1; + repeated string ignore = 2; +} + +message GetFilesResponse { + message File { + string path = 1; + bytes content = 2; + } + repeated File files = 1; +} + +message SaveFileRequest { + string path = 1; + bytes content = 2; +} + message Asset { string path = 1; } message AssetCatalogResponse { - string base_path = 1; + string basePath = 1; repeated Asset assets = 2; } @@ -40,6 +58,8 @@ service DataService { rpc Undo(Empty) returns (UndoRedoResponse) {} rpc Redo(Empty) returns (UndoRedoResponse) {} + rpc getFiles(GetFilesRequest) returns (GetFilesResponse) {} + rpc saveFile(SaveFileRequest) returns (Empty) {} rpc GetAssetCatalog(Empty) returns (AssetCatalogResponse) {} rpc GetAssetData(Asset) returns (AssetData) {} rpc ImportAsset(ImportAssetRequest) returns (Empty) {} diff --git a/packages/@dcl/inspector/src/lib/logic/storage/in-memory.ts b/packages/@dcl/inspector/src/lib/logic/storage/in-memory.ts index 86bc43897..ab402bb6c 100644 --- a/packages/@dcl/inspector/src/lib/logic/storage/in-memory.ts +++ b/packages/@dcl/inspector/src/lib/logic/storage/in-memory.ts @@ -40,6 +40,7 @@ export function createInMemoryStorage(initialFs: Record = {}): S files.push({ name: fileName, isDirectory: false }) } } + return files } } diff --git a/packages/@dcl/inspector/src/redux/app/index.ts b/packages/@dcl/inspector/src/redux/app/index.ts index c556c364f..b6ae6077e 100644 --- a/packages/@dcl/inspector/src/redux/app/index.ts +++ b/packages/@dcl/inspector/src/redux/app/index.ts @@ -1,19 +1,21 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit' import { RootState } from '../store' import { InspectorPreferences } from '../../lib/logic/preferences/types' -import { AssetCatalogResponse } from '../../lib/data-layer/remote-data-layer' +import { AssetCatalogResponse, GetFilesResponse } from '../../lib/data-layer/remote-data-layer' export interface AppState { canSave: boolean preferences: InspectorPreferences | undefined assetsCatalog: AssetCatalogResponse | undefined + thumbnails: GetFilesResponse['files'] } export const initialState: AppState = { // dirty engine canSave: false, preferences: undefined, - assetsCatalog: undefined + assetsCatalog: undefined, + thumbnails: [] } export const appState = createSlice({ @@ -30,12 +32,15 @@ export const appState = createSlice({ }, updateAssetCatalog: (state, { payload }: PayloadAction<{ assets: AssetCatalogResponse }>) => { state.assetsCatalog = payload.assets + }, + updateThumbnails: (state, { payload }: PayloadAction) => { + state.thumbnails = payload.files } } }) // Actions -export const { updateCanSave, updatePreferences, updateAssetCatalog } = appState.actions +export const { updateCanSave, updatePreferences, updateAssetCatalog, updateThumbnails } = appState.actions // Selectors export const selectCanSave = (state: RootState): boolean => state.app.canSave @@ -43,6 +48,7 @@ export const selectInspectorPreferences = (state: RootState): InspectorPreferenc return state.app.preferences } export const selectAssetCatalog = (state: RootState) => state.app.assetsCatalog +export const selectThumbnails = (state: RootState) => state.app.thumbnails // Reducer export default appState.reducer diff --git a/packages/@dcl/inspector/src/redux/data-layer/index.ts b/packages/@dcl/inspector/src/redux/data-layer/index.ts index e4d925a97..804bd6953 100644 --- a/packages/@dcl/inspector/src/redux/data-layer/index.ts +++ b/packages/@dcl/inspector/src/redux/data-layer/index.ts @@ -2,7 +2,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { RootState } from '../../redux/store' import { DataLayerRpcClient } from '../../lib/data-layer/types' import { InspectorPreferences } from '../../lib/logic/preferences/types' -import { Asset, ImportAssetRequest } from '../../lib/data-layer/remote-data-layer' +import { Asset, ImportAssetRequest, SaveFileRequest } from '../../lib/data-layer/remote-data-layer' export enum ErrorType { Disconnected = 'disconnected', @@ -14,7 +14,9 @@ export enum ErrorType { Undo = 'undo', Redo = 'redo', ImportAsset = 'import-asset', - RemoveAsset = 'remove-asset' + RemoveAsset = 'remove-asset', + SaveThumbnail = 'save-thumbnail', + GetThumbnails = 'get-thumbnails' } let dataLayerInterface: DataLayerRpcClient | undefined @@ -65,7 +67,9 @@ export const dataLayer = createSlice({ undo: () => {}, redo: () => {}, importAsset: (_state, _payload: PayloadAction) => {}, - removeAsset: (_state, _payload: PayloadAction) => {} + removeAsset: (_state, _payload: PayloadAction) => {}, + saveThumbnail: (_state, _payload: PayloadAction) => {}, + getThumbnails: () => {} } }) @@ -82,7 +86,9 @@ export const { undo, redo, importAsset, - removeAsset + removeAsset, + saveThumbnail, + getThumbnails } = dataLayer.actions // Selectors diff --git a/packages/@dcl/inspector/src/redux/data-layer/sagas/connected.ts b/packages/@dcl/inspector/src/redux/data-layer/sagas/connected.ts index e47546248..367cd40d7 100644 --- a/packages/@dcl/inspector/src/redux/data-layer/sagas/connected.ts +++ b/packages/@dcl/inspector/src/redux/data-layer/sagas/connected.ts @@ -1,10 +1,11 @@ import { call, put } from 'redux-saga/effects' -import { IDataLayer, getAssetCatalog, getDataLayerInterface, getInspectorPreferences } from '../' +import { IDataLayer, getAssetCatalog, getDataLayerInterface, getInspectorPreferences, getThumbnails } from '../' export function* connectedSaga() { const dataLayer: IDataLayer = yield call(getDataLayerInterface) if (!dataLayer) return yield put(getInspectorPreferences()) yield put(getAssetCatalog()) + yield put(getThumbnails()) } diff --git a/packages/@dcl/inspector/src/redux/data-layer/sagas/get-thumbnails.ts b/packages/@dcl/inspector/src/redux/data-layer/sagas/get-thumbnails.ts new file mode 100644 index 000000000..e81d8f9fe --- /dev/null +++ b/packages/@dcl/inspector/src/redux/data-layer/sagas/get-thumbnails.ts @@ -0,0 +1,19 @@ +import { call, put } from 'redux-saga/effects' + +import { ErrorType, IDataLayer, error, getDataLayerInterface } from '..' +import { GetFilesResponse } from '../../../lib/data-layer/remote-data-layer' +import { updateThumbnails } from '../../app' +import { DIRECTORY } from '../../../lib/data-layer/host/fs-utils' + +export function* getThumbnailsSaga() { + const dataLayer: IDataLayer = yield call(getDataLayerInterface) + if (!dataLayer) return + try { + const thumbnails: GetFilesResponse = yield call(dataLayer.getFiles, { path: DIRECTORY.THUMBNAILS, ignore: [] }) + + // Fetch asset catalog again + yield put(updateThumbnails(thumbnails)) + } catch (e) { + yield put(error({ error: ErrorType.GetThumbnails })) + } +} diff --git a/packages/@dcl/inspector/src/redux/data-layer/sagas/index.ts b/packages/@dcl/inspector/src/redux/data-layer/sagas/index.ts index 1967d7a02..30130a87d 100644 --- a/packages/@dcl/inspector/src/redux/data-layer/sagas/index.ts +++ b/packages/@dcl/inspector/src/redux/data-layer/sagas/index.ts @@ -10,7 +10,9 @@ import { getAssetCatalog, undo, redo, - importAsset + importAsset, + getThumbnails, + saveThumbnail } from '..' import { connectSaga } from './connect' import { reconnectSaga } from './reconnect' @@ -21,6 +23,8 @@ import { getAssetCatalogSaga } from './get-asset-catalog' import { redoSaga, undoSaga } from './undo-redo' import { importAssetSaga } from './import-asset' import { connectedSaga } from './connected' +import { getThumbnailsSaga } from './get-thumbnails' +import { saveThumbnailSaga } from './save-thumbnail' export function* dataLayerSaga() { yield takeEvery(connect.type, connectSaga) @@ -33,6 +37,8 @@ export function* dataLayerSaga() { yield takeEvery(undo.type, undoSaga) yield takeEvery(redo.type, redoSaga) yield takeEvery(importAsset.type, importAssetSaga) + yield takeEvery(getThumbnails.type, getThumbnailsSaga) + yield takeEvery(saveThumbnail.type, saveThumbnailSaga) } export default dataLayerSaga diff --git a/packages/@dcl/inspector/src/redux/data-layer/sagas/save-thumbnail.ts b/packages/@dcl/inspector/src/redux/data-layer/sagas/save-thumbnail.ts new file mode 100644 index 000000000..e23169e4e --- /dev/null +++ b/packages/@dcl/inspector/src/redux/data-layer/sagas/save-thumbnail.ts @@ -0,0 +1,17 @@ +import { call, put } from 'redux-saga/effects' + +import { ErrorType, IDataLayer, error, getDataLayerInterface, getThumbnails, saveThumbnail } from '..' +import { Empty } from '../../../lib/data-layer/remote-data-layer' + +export function* saveThumbnailSaga(action: ReturnType) { + const dataLayer: IDataLayer = yield call(getDataLayerInterface) + if (!dataLayer) return + try { + const _response: Empty = yield call(dataLayer.saveFile, action.payload) + + // Fetch thumbnails again + yield put(getThumbnails()) + } catch (e) { + yield put(error({ error: ErrorType.SaveThumbnail })) + } +}