Merge pull request #210 from Irev-Dev/kurt/capture-part-image-on-first-save

Capture image on initial save
This commit was merged in pull request #210.
This commit is contained in:
Kurt Hutten
2021-02-27 14:21:28 +11:00
committed by GitHub
4 changed files with 117 additions and 66 deletions

View File

@@ -51,7 +51,7 @@ export const createPart = async ({ input }) => {
} }
export const forkPart = async ({ input }) => { export const forkPart = async ({ input }) => {
// Only difference between create nda clone part is that clone part will generate a unique title // Only difference between create and fork part is that fork part will generate a unique title
// (for the user) if there is a conflict // (for the user) if there is a conflict
const isUniqueCallback = async (seed) => const isUniqueCallback = async (seed) =>
db.part.findOne({ db.part.findOne({
@@ -83,6 +83,7 @@ export const updatePart = async ({ id, input }) => {
where: { id }, where: { id },
}) })
if (imageToDestroy) { if (imageToDestroy) {
console.log(`image destroyed, publicId: ${imageToDestroy}, partId: ${id}`)
// destroy after the db has been updated // destroy after the db has been updated
destroyImage({ publicId: imageToDestroy }) destroyImage({ publicId: imageToDestroy })
} }

View File

@@ -3,7 +3,10 @@ import CascadeController from 'src/helpers/cascadeController'
import IdeToolbar from 'src/components/IdeToolbar' import IdeToolbar from 'src/components/IdeToolbar'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { threejsViewport } from 'src/cascade/js/MainPage/CascadeState' import { threejsViewport } from 'src/cascade/js/MainPage/CascadeState'
import { uploadToCloudinary } from 'src/helpers/cloudinary' import {
uploadToCloudinary,
captureAndSaveViewport,
} from 'src/helpers/cloudinary'
const defaultExampleCode = `// Welcome to Cascade Studio! Here are some useful functions: const defaultExampleCode = `// Welcome to Cascade Studio! Here are some useful functions:
// Translate(), Rotate(), Scale(), Union(), Difference(), Intersection() // Translate(), Rotate(), Scale(), Union(), Difference(), Intersection()
@@ -53,16 +56,22 @@ const IdeCascadeStudio = ({ part, saveCode, loading }) => {
isChanges={isChanges && !loading} isChanges={isChanges && !loading}
isDraft={isDraft} isDraft={isDraft}
code={code} code={code}
onSave={() => { onSave={async () => {
const input = {
code,
title: part?.title,
userId: currentUser?.sub,
description: part?.description,
}
const isFork = !canEdit
if (isFork) {
const { publicId } = await captureAndSaveViewport()
input.mainImage = publicId
}
saveCode({ saveCode({
input: { input,
code,
title: part?.title,
userId: currentUser?.sub,
description: part?.description,
},
id: part.id, id: part.id,
isFork: !canEdit, isFork,
}) })
}} }}
onExport={(type) => threejsViewport[`saveShape${type}`]()} onExport={(type) => threejsViewport[`saveShape${type}`]()}
@@ -71,7 +80,7 @@ const IdeCascadeStudio = ({ part, saveCode, loading }) => {
partTitle: part?.title, partTitle: part?.title,
image: part?.user?.image, image: part?.user?.image,
}} }}
onCapture={ async () => { onCapture={async () => {
const config = { const config = {
currImage: part?.mainImage, currImage: part?.mainImage,
callback: uploadAndUpdateImage, callback: uploadAndUpdateImage,
@@ -79,10 +88,12 @@ const IdeCascadeStudio = ({ part, saveCode, loading }) => {
updated: false, updated: false,
} }
// Get the canvas image as a Data URL // Get the canvas image as a Data URL
config.image = await CascadeController.capture(threejsViewport.environment) config.image = await CascadeController.capture(
threejsViewport.environment
)
config.imageObjectURL = window.URL.createObjectURL(config.image) config.imageObjectURL = window.URL.createObjectURL(config.image)
async function uploadAndUpdateImage(){ async function uploadAndUpdateImage() {
// Upload the image to Cloudinary // Upload the image to Cloudinary
const cloudinaryImgURL = await uploadToCloudinary(config.image) const cloudinaryImgURL = await uploadToCloudinary(config.image)

View File

@@ -14,6 +14,7 @@ import { FORK_PART_MUTATION } from 'src/components/IdePartCell'
import { QUERY as UsersPartsQuery } from 'src/components/PartsOfUserCell' import { QUERY as UsersPartsQuery } from 'src/components/PartsOfUserCell'
import useUser from 'src/helpers/hooks/useUser' import useUser from 'src/helpers/hooks/useUser'
import useKeyPress from 'src/helpers/hooks/useKeyPress' import useKeyPress from 'src/helpers/hooks/useKeyPress'
import { captureAndSaveViewport } from 'src/helpers/cloudinary'
const IdeToolbar = ({ const IdeToolbar = ({
canEdit, canEdit,
@@ -62,16 +63,19 @@ const IdeToolbar = ({
setWhichPopup(null) setWhichPopup(null)
} }
const saveFork = () => const saveFork = async () => {
forkPart({ const { publicId } = await captureAndSaveViewport()
return forkPart({
variables: { variables: {
input: { input: {
userId: currentUser.sub, userId: currentUser.sub,
title, title,
code, code,
mainImage: publicId,
}, },
}, },
}) })
}
const handleSave = async () => { const handleSave = async () => {
if (isDraft && isAuthenticated) { if (isDraft && isAuthenticated) {
@@ -109,9 +113,9 @@ const IdeToolbar = ({
const handleDownload = (url) => { const handleDownload = (url) => {
const aTag = document.createElement('a') const aTag = document.createElement('a')
document.body.appendChild(aTag) document.body.appendChild(aTag)
aTag.href= url aTag.href = url
aTag.style.display = 'none' aTag.style.display = 'none'
aTag.download = `CadHub_${ Date.now() }.jpg` aTag.download = `CadHub_${Date.now()}.jpg`
aTag.click() aTag.click()
document.body.removeChild(aTag) document.body.removeChild(aTag)
} }
@@ -232,58 +236,82 @@ const IdeToolbar = ({
</div> </div>
<div className="ml-auto flex items-center"> <div className="ml-auto flex items-center">
{/* Capture Screenshot link. Should only appear if part has been saved and is editable. */} {/* Capture Screenshot link. Should only appear if part has been saved and is editable. */}
{ !isDraft && canEdit && <div> {!isDraft && canEdit && (
<button <div>
onClick={async event => { <button
handleClick({ event, whichPopup: 'capture' }) onClick={async (event) => {
setCaptureState(await onCapture()) handleClick({ event, whichPopup: 'capture' })
}} setCaptureState(await onCapture())
className="text-indigo-300 flex items-center pr-6" }}
> className="text-indigo-300 flex items-center pr-6"
Save Part Image <Svg name="camera" className="pl-2 w-8" /> >
</button> Save Part Image <Svg name="camera" className="pl-2 w-8" />
<Popover </button>
id={id} <Popover
open={whichPopup === 'capture'} id={id}
anchorEl={anchorEl} open={whichPopup === 'capture'}
onClose={handleClose} anchorEl={anchorEl}
anchorOrigin={anchorOrigin} onClose={handleClose}
transformOrigin={transformOrigin} anchorOrigin={anchorOrigin}
className="material-ui-overrides transform translate-y-4" transformOrigin={transformOrigin}
> className="material-ui-overrides transform translate-y-4"
<div className="text-sm p-2 text-gray-500"> >
{ !captureState <div className="text-sm p-2 text-gray-500">
? 'Loading...' {!captureState ? (
: <div className="grid grid-cols-2"> 'Loading...'
<div className="rounded m-auto" style={{width: 'fit-content', overflow: 'hidden'}}> ) : (
<img src={ captureState.imageObjectURL } className="w-32" /> <div className="grid grid-cols-2">
</div> <div
<div className="p-2 text-indigo-800"> className="rounded m-auto"
{ (captureState.currImage && !captureState.updated) style={{ width: 'fit-content', overflow: 'hidden' }}
? <button className="flex justify-center mb-4" >
onClick={ async () => { <img src={captureState.imageObjectURL} className="w-32" />
</div>
<div className="p-2 text-indigo-800">
{captureState.currImage && !captureState.updated ? (
<button
className="flex justify-center mb-4"
onClick={async () => {
const cloudinaryImg = await captureState.callback() const cloudinaryImg = await captureState.callback()
setCaptureState({...captureState, currImage: cloudinaryImg.public_id, updated: true }) setCaptureState({
}}> ...captureState,
<Svg name="refresh" className="mr-2 w-4 text-indigo-600"/> Update Part Image currImage: cloudinaryImg.public_id,
updated: true,
})
}}
>
<Svg
name="refresh"
className="mr-2 w-4 text-indigo-600"
/>{' '}
Update Part Image
</button> </button>
: <div className="flex justify-center mb-4"> ) : (
<Svg name="checkmark" className="mr-2 w-6 text-indigo-600"/> Part Image Updated <div className="flex justify-center mb-4">
<Svg
name="checkmark"
className="mr-2 w-6 text-indigo-600"
/>{' '}
Part Image Updated
</div> </div>
} )}
<Button <Button
iconName="save" iconName="save"
className="shadow-md hover:shadow-lg border-indigo-600 border-2 border-opacity-0 hover:border-opacity-100 bg-indigo-800 text-indigo-100 text-opacity-100 bg-opacity-80" className="shadow-md hover:shadow-lg border-indigo-600 border-2 border-opacity-0 hover:border-opacity-100 bg-indigo-800 text-indigo-100 text-opacity-100 bg-opacity-80"
shouldAnimateHover shouldAnimateHover
onClick={() => handleDownload(captureState.imageObjectURL)}> onClick={() =>
Download handleDownload(captureState.imageObjectURL)
</Button> }
>
Download
</Button>
</div>
</div> </div>
</div> )}
} </div>
</div> </Popover>
</Popover> </div>
</div> } )}
<div> <div>
<button <button
onClick={(event) => handleClick({ event, whichPopup: 'tips' })} onClick={(event) => handleClick({ event, whichPopup: 'tips' })}

View File

@@ -1,5 +1,7 @@
// TODO: create a tidy util for uploading to Cloudinary and returning the public ID // TODO: create a tidy util for uploading to Cloudinary and returning the public ID
import axios from 'axios' import axios from 'axios'
import { threejsViewport } from 'src/cascade/js/MainPage/CascadeState'
import CascadeController from 'src/helpers/cascadeController'
const CLOUDINARY_UPLOAD_PRESET = 'CadHub_project_images' const CLOUDINARY_UPLOAD_PRESET = 'CadHub_project_images'
const CLOUDINARY_UPLOAD_URL = 'https://api.cloudinary.com/v1_1/irevdev/upload' const CLOUDINARY_UPLOAD_URL = 'https://api.cloudinary.com/v1_1/irevdev/upload'
@@ -19,3 +21,12 @@ export async function uploadToCloudinary(imgBlob) {
console.error('ERROR', e) console.error('ERROR', e)
} }
} }
export const captureAndSaveViewport = async () => {
// Get the canvas image as a Data URL
const imgBlob = await CascadeController.capture(threejsViewport.environment)
// Upload the image to Cloudinary
const { public_id: publicId } = await uploadToCloudinary(imgBlob)
return { publicId, imgBlob }
}