Skip to content

Commit

Permalink
feat: SDK7 thumbnails (#2817)
Browse files Browse the repository at this point in the history
* feat: add support for screenshots in sdk7 projects

* fix: take screenshot of newly created project
  • Loading branch information
cazala authored Aug 4, 2023
1 parent 36e7918 commit ef00135
Show file tree
Hide file tree
Showing 7 changed files with 433 additions and 330 deletions.
527 changes: 273 additions & 254 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
"@dcl/content-hash-tree": "^1.1.3",
"@dcl/crypto": "^3.0.1",
"@dcl/hashing": "^3.0.4",
"@dcl/mini-rpc": "^1.0.6",
"@dcl/schemas": "^8.2.2",
"@dcl/sdk": "^7.3.3",
"@dcl/sdk": "^7.3.6",
"@dcl/ui-env": "^1.2.0",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^11.0.0",
Expand Down
54 changes: 35 additions & 19 deletions src/modules/editor/sagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ import {
ROTATION_GRID_RESOLUTION,
fromCatalystWearableToWearable
} from './utils'
import { MessageTransport } from '@dcl/mini-rpc'
import { CameraClient } from '@dcl/inspector'
const editorWindow = window as EditorWindow

export function* editorSaga() {
Expand Down Expand Up @@ -565,30 +567,44 @@ function* handleScreenshot(_: TakeScreenshotAction) {
const currentProject: Project | null = yield select(getCurrentProject)
if (!currentProject) return

// wait for editor to be ready
let ready: boolean = yield select(isReady)
while (!ready) {
const readyAction: SetEditorReadyAction = yield take(SET_EDITOR_READY)
ready = readyAction.payload.isReady
}
const scene: Scene | null = yield select(getCurrentScene)
if (!scene) return

// wait for assets to load
let loading: boolean = yield select(isLoading)
while (loading) {
const loadingAction: SetEditorLoadingAction = yield take(SET_EDITOR_LOADING)
loading = loadingAction.payload.isLoading
}
if (scene.sdk6) {
// wait for editor to be ready
let ready: boolean = yield select(isReady)
while (!ready) {
const readyAction: SetEditorReadyAction = yield take(SET_EDITOR_READY)
ready = readyAction.payload.isReady
}

// rendering leeway
yield delay(2000)
// wait for assets to load
let loading: boolean = yield select(isLoading)
while (loading) {
const loadingAction: SetEditorLoadingAction = yield take(SET_EDITOR_LOADING)
loading = loadingAction.payload.isLoading
}

// rendering leeway
yield delay(2000)

const screenshot: string = yield call(() => editorWindow.editor.takeScreenshot())
if (!screenshot) return
const screenshot: string = yield call(() => editorWindow.editor.takeScreenshot())
if (!screenshot) return

const thumbnail: string | null = yield call(() => resizeScreenshot(screenshot, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT))
if (!thumbnail) return
const thumbnail: string | null = yield call(() => resizeScreenshot(screenshot, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT))
if (!thumbnail) return

yield put(editProjectThumbnail(currentProject.id, thumbnail))
yield put(editProjectThumbnail(currentProject.id, thumbnail))
} else {
const iframe = document.getElementById('inspector') as HTMLIFrameElement | null
if (!iframe || !iframe.contentWindow!) return
const transport = new MessageTransport(window, iframe.contentWindow)
const camera = new CameraClient(transport)
const screenshot: string = yield call([camera, 'takeScreenshot'], +iframe.width, +iframe.height)
const thumbnail: string | null = yield call(resizeScreenshot, screenshot, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT)
if (!thumbnail) return
yield put(editProjectThumbnail(currentProject.id, thumbnail))
}
} catch (e) {
// skip screenshot
}
Expand Down
4 changes: 4 additions & 0 deletions src/modules/inspector/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ export const rpcFailure = (
nonce: number
) => action(RPC_FAILURE, { method, params, error, nonce })
export type RPCFailureAction = ReturnType<typeof rpcFailure>

export const TOGGLE_SCREENSHOT = 'Toggle Screenshot'
export const toggleScreenshot = (enabled: boolean) => action(TOGGLE_SCREENSHOT, { enabled })
export type ToggleScreenshotAction = ReturnType<typeof toggleScreenshot>
19 changes: 14 additions & 5 deletions src/modules/inspector/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import { Action } from 'typesafe-actions'
import { ToggleScreenshotAction, TOGGLE_SCREENSHOT } from './actions'

/* eslint-disable @typescript-eslint/ban-types */
export type InspectorState = {}
export type InspectorState = {
screenshotEnabled: boolean
}

const INITIAL_STATE: InspectorState = {}
const INITIAL_STATE: InspectorState = {
screenshotEnabled: true
}

type InspectorReducerAction = Action
type InspectorReducerAction = ToggleScreenshotAction

export function inspectorReducer(state = INITIAL_STATE, action: InspectorReducerAction) {
switch (action.type) {
case TOGGLE_SCREENSHOT: {
return {
...state,
screenshotEnabled: action.payload.enabled
}
}
default:
return state
}
Expand Down
155 changes: 104 additions & 51 deletions src/modules/inspector/sagas.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import { call, put, race, select, take, takeEvery } from 'redux-saga/effects'
import { call, delay, put, race, select, take, takeEvery } from 'redux-saga/effects'
import { future, IFuture } from 'fp-future'
import { hashV1 } from '@dcl/hashing'
import { LoginFailureAction, LoginSuccessAction, LOGIN_FAILURE, LOGIN_SUCCESS } from 'modules/identity/actions'
Expand Down Expand Up @@ -33,17 +33,21 @@ import {
RPCSuccessAction,
RPC_FAILURE,
RPC_REQUEST,
RPC_SUCCESS
RPC_SUCCESS,
toggleScreenshot
} from './actions'
import { Project } from 'modules/project/types'
import { isLoadingType } from 'decentraland-dapps/dist/modules/loading/selectors'
import { IframeStorage, MessageTransport } from '@dcl/inspector'
import { IframeStorage } from '@dcl/inspector'
import { MessageTransport } from '@dcl/mini-rpc'
import { getParcels } from './utils'
import { BuilderAPI, getContentsStorageUrl } from 'lib/api/builder'
import { NO_CACHE_HEADERS } from 'lib/headers'
import { Scene, SceneSDK7 } from 'modules/scene/types'
import { updateScene } from 'modules/scene/actions'
import { updateScene, UPDATE_SCENE } from 'modules/scene/actions'
import { RootStore } from 'modules/common/types'
import { takeScreenshot } from 'modules/editor/actions'
import { isScreenshotEnabled } from './selectors'

let nonces = 0
const getNonce = () => nonces++
Expand All @@ -56,6 +60,7 @@ export function* inspectorSaga(builder: BuilderAPI, store: RootStore) {
yield takeEvery(RPC_REQUEST, handleRpcRequest)
yield takeEvery(RPC_SUCCESS, handleRpcSuccess)
yield takeEvery(RPC_FAILURE, handleRpcFailure)
yield takeEvery(UPDATE_SCENE, handleUpdateScene)

function* handleOpenInspector(_action: OpenInspectorAction) {
try {
Expand Down Expand Up @@ -92,14 +97,17 @@ export function* inspectorSaga(builder: BuilderAPI, store: RootStore) {
}
}

function handleConnectInspector(action: ConnectInspectorAction) {
function* handleConnectInspector(action: ConnectInspectorAction) {
const { iframeId } = action.payload

const iframe = document.getElementById(iframeId) as HTMLIFrameElement | null
if (iframe === null) {
throw new Error(`Iframe with id="${iframeId}" not found`)
}

// disable the screenshots, turn it on once the scene is fully loaded
yield put(toggleScreenshot(false))

const transport = new MessageTransport(window, iframe.contentWindow!, '*')
const storage = new IframeStorage.Server(transport)

Expand All @@ -114,6 +122,21 @@ export function* inspectorSaga(builder: BuilderAPI, store: RootStore) {
return promise
})
}

// wait for RPC to be idle (3 seconds)
yield waitForRpcIdle(3000)

// turn on screenshots
yield put(toggleScreenshot(true))

// check if project doesn't have a thumbnail (ie. because it's new), and if so, take a screenshot
const project: Project | null = yield select(getCurrentProject)
if (project) {
const result: boolean = yield call(hasThumbnail, project)
if (!result) {
yield put(takeScreenshot())
}
}
}

function* handleRpcRequest(action: RPCRequestAction) {
Expand Down Expand Up @@ -143,6 +166,13 @@ export function* inspectorSaga(builder: BuilderAPI, store: RootStore) {
}
}

function* handleUpdateScene() {
const isEnabled: boolean = yield select(isScreenshotEnabled)
if (isEnabled) {
yield put(takeScreenshot())
}
}

// HANDLERS

const handlers: Record<`${IframeStorage.Method}`, (params: any) => Generator> = {
Expand Down Expand Up @@ -310,66 +340,64 @@ export function* inspectorSaga(builder: BuilderAPI, store: RootStore) {
// remove from memory
assets.delete(path)
}
}

// UTILS

function* getProject(projectId: string): any {
// grab projects from store
const projects: Record<string, Project> = yield select(getProjects)
const project = projects[projectId]

// if project is found in store, return it
if (project) {
return project
}
function* getProject(projectId: string): any {
// grab projects from store
const projects: Record<string, Project> = yield select(getProjects)
const project = projects[projectId]

// if project is not in the store, check if projects are being loaded
const projectsLoadingState: ReturnType<typeof getLoadingProjects> = yield select(getLoadingProjects)
const isLoading = isLoadingType(projectsLoadingState, LOAD_PROJECTS_REQUEST)
// if project is found in store, return it
if (project) {
return project
}

// if projects are not being loaded, then request them
if (!isLoading) {
yield put(loadProjectsRequest())
}
// if project is not in the store, check if projects are being loaded
const projectsLoadingState: ReturnType<typeof getLoadingProjects> = yield select(getLoadingProjects)
const isLoading = isLoadingType(projectsLoadingState, LOAD_PROJECTS_REQUEST)

// wait for projects to be loaded
const result: { success?: LoadProjectsSuccessAction; failure?: LoadProjectsFailureAction } = yield race({
success: take(LOAD_PROJECTS_SUCCESS),
failure: take(LOAD_PROJECTS_FAILURE)
})
// if projects are not being loaded, then request them
if (!isLoading) {
yield put(loadProjectsRequest())
}

// if load is successful try getting the project again
if (result.success) {
const _project: Project = yield getProject(projectId)
return _project
}
// wait for projects to be loaded
const result: { success?: LoadProjectsSuccessAction; failure?: LoadProjectsFailureAction } = yield race({
success: take(LOAD_PROJECTS_SUCCESS),
failure: take(LOAD_PROJECTS_FAILURE)
})

// if load fails then throw
if (result.failure) {
console.error(result.failure)
throw new Error(`Could not load project`)
}
// if load is successful try getting the project again
if (result.success) {
const _project: Project = yield getProject(projectId)
return _project
}

function* getScene() {
const project: Project = yield select(getCurrentProject)
// if load fails then throw
if (result.failure) {
console.error(result.failure)
throw new Error(`Could not load project`)
}
}

if (!project) {
throw new Error('Invalid project')
}
function* getScene() {
const project: Project = yield select(getCurrentProject)

const scene: Scene | null = yield select(getCurrentScene)
if (!project) {
throw new Error('Invalid project')
}

if (!scene) {
throw new Error('Invalid scene')
}
const scene: Scene | null = yield select(getCurrentScene)

if (!scene.sdk7) {
throw new Error('Scene must be SDK7')
}
if (!scene) {
throw new Error('Invalid scene')
}

return scene.sdk7
if (!scene.sdk7) {
throw new Error('Scene must be SDK7')
}

return scene.sdk7
}

function* isContentUploaded(path: string, hash: string) {
Expand All @@ -383,3 +411,28 @@ function* isContentUploaded(path: string, hash: string) {
return false
}
}

function* waitForRpcIdle(ms: number) {
const { request }: { request: RPCRequestAction | null } = yield race({
request: take(RPC_REQUEST),
timeout: delay(ms, true)
})
if (request) {
let elapsed = 0
while (elapsed < ms) {
const timestamp = Date.now()
yield race([take(RPC_REQUEST), delay(ms, true)])
elapsed = Date.now() - timestamp
}
}
}

function* hasThumbnail(project: Project) {
try {
if (!project.thumbnail) return false
const response: Response = yield call(fetch, project.thumbnail, { headers: NO_CACHE_HEADERS })
return response.ok
} catch (error) {
return false
}
}
1 change: 1 addition & 0 deletions src/modules/inspector/selectors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { RootState } from 'modules/common/types'

export const getState = (state: RootState) => state.inspector
export const isScreenshotEnabled = (state: RootState) => getState(state).screenshotEnabled

1 comment on commit ef00135

@vercel
Copy link

@vercel vercel bot commented on ef00135 Aug 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

builder – ./

builder-decentraland1.vercel.app
builder-git-master-decentraland1.vercel.app

Please sign in to comment.