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:
@@ -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 })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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' })}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user