Add Capture button to IDE toolbar to update main image of Part #209

Merged
franknoirot merged 6 commits from main into main 2021-02-27 03:24:38 +01:00
6 changed files with 191 additions and 1 deletions

View File

@@ -3,6 +3,7 @@ import CascadeController from 'src/helpers/cascadeController'
import IdeToolbar from 'src/components/IdeToolbar'
import { useEffect, useState } from 'react'
import { threejsViewport } from 'src/cascade/js/MainPage/CascadeState'
import { uploadToCloudinary } from 'src/helpers/cloudinary'
const defaultExampleCode = `// Welcome to Cascade Studio! Here are some useful functions:
// Translate(), Rotate(), Scale(), Union(), Difference(), Intersection()
@@ -70,6 +71,41 @@ const IdeCascadeStudio = ({ part, saveCode, loading }) => {
partTitle: part?.title,
image: part?.user?.image,
}}
onCapture={ async () => {
const config = {
currImage: part?.mainImage,
callback: uploadAndUpdateImage,
cloudinaryImgURL: '',
updated: false,
}
// Get the canvas image as a Data URL
config.image = await CascadeController.capture(threejsViewport.environment)
config.imageObjectURL = window.URL.createObjectURL(config.image)
async function uploadAndUpdateImage(){
// Upload the image to Cloudinary
const cloudinaryImgURL = await uploadToCloudinary(config.image)
// Save the screenshot as the mainImage
saveCode({
input: {
mainImage: cloudinaryImgURL.public_id,
},
id: part?.id,
isFork: !canEdit,
})
return cloudinaryImgURL
}
// if there isn't a screenshot saved yet, just go ahead and save right away
if (!part || !part.mainImage) {
config.cloudinaryImgURL = await uploadAndUpdateImage().public_id
config.updated = true
}
return config
}}
/>
</div>
</>

View File

@@ -50,7 +50,7 @@ export const Success = ({ part, refetch }) => {
const { user } = useUser()
const [updatePart, { loading, error }] = useMutation(UPDATE_PART_MUTATION, {
onCompleted: () => {
addMessage('Part updated.', { classes: 'rw-flash-success' })
addMessage('Part updated.', { classes: 'rw-flash-success fixed w-screen z-10' })
},
})
const [forkPart] = useMutation(FORK_PART_MUTATION, {

View File

@@ -23,6 +23,7 @@ const IdeToolbar = ({
userNamePart,
isDraft,
code,
onCapture,
}) => {
const [anchorEl, setAnchorEl] = useState(null)
const [whichPopup, setWhichPopup] = useState(null)
@@ -30,6 +31,7 @@ const IdeToolbar = ({
const { isAuthenticated, currentUser } = useAuth()
const showForkButton = !(canEdit || isDraft)
const [title, setTitle] = useState('untitled-part')
const [captureState, setCaptureState] = useState(false)
const { user } = useUser()
useKeyPress((e) => {
const rx = /INPUT|SELECT|TEXTAREA/i
@@ -104,6 +106,16 @@ const IdeToolbar = ({
setIsLoginModalOpen(true)
}
const handleDownload = (url) => {
const aTag = document.createElement('a')
document.body.appendChild(aTag)
aTag.href= url
aTag.style.display = 'none'
aTag.download = `CadHub_${ Date.now() }.jpg`
aTag.click()
document.body.removeChild(aTag)
}
const anchorOrigin = {
vertical: 'bottom',
horizontal: 'center',
@@ -219,6 +231,59 @@ const IdeToolbar = ({
</Popover>
</div>
<div className="ml-auto flex items-center">
{/* Capture Screenshot link. Should only appear if part has been saved and is editable. */}
{ !isDraft && canEdit && <div>
<button
onClick={async event => {
handleClick({ event, whichPopup: 'capture' })
setCaptureState(await onCapture())
}}
className="text-indigo-300 flex items-center pr-6"
>
Save Part Image <Svg name="camera" className="pl-2 w-8" />
</button>
<Popover
id={id}
open={whichPopup === 'capture'}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={anchorOrigin}
transformOrigin={transformOrigin}
className="material-ui-overrides transform translate-y-4"
>
<div className="text-sm p-2 text-gray-500">
{ !captureState
? 'Loading...'
: <div className="grid grid-cols-2">
<div className="rounded m-auto" style={{width: 'fit-content', overflow: 'hidden'}}>
<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()
setCaptureState({...captureState, currImage: cloudinaryImg.public_id, updated: true })
}}>
<Svg name="refresh" className="mr-2 w-4 text-indigo-600"/> Update Part Image
</button>
: <div className="flex justify-center mb-4">
<Svg name="checkmark" className="mr-2 w-6 text-indigo-600"/> Part Image Updated
</div>
}
<Button
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"
shouldAnimateHover
onClick={() => handleDownload(captureState.imageObjectURL)}>
Download
</Button>
</div>
</div>
}
</div>
</Popover>
</div> }
<div>
<button
onClick={(event) => handleClick({ event, whichPopup: 'tips' })}

View File

@@ -30,6 +30,40 @@ const Svg = ({ name, className: className2, strokeWidth = 2 }) => {
/>
</svg>
),
'camera': (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 21">
<path
d="M6 5H4C2.34315 5 1 6.34315 1 8V17C1 18.6569 2.34315 20 4 20H20C21.6569 20 23 18.6569 23 17V8C23 6.34315 21.6569 5 20 5H18"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"/>
<circle
cx="12"
cy="11"
r="5"
stroke="currentColor"
strokeWidth="2"/>
<path
d="M16 2.68641C14.8716 1.61443 13.5582 1 12.1563 1C10.6229 1 9.19532 1.7351 8 3"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"/>
</svg>
),
'checkmark': (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 21 20"
fill="none">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10.3438 19.6875C15.7803 19.6875 20.1875 15.2803 20.1875 9.84375C20.1875 4.4072 15.7803 0 10.3438 0C4.9072 0 0.5 4.4072 0.5 9.84375C0.5 15.2803 4.9072 19.6875 10.3438 19.6875ZM15.3321 6.5547C15.6384 6.09517 15.5142 5.4743 15.0547 5.16795C14.5952 4.8616 13.9743 4.98577 13.6679 5.4453L9.34457 11.9304L7.20711 9.79289C6.81658 9.40237 6.18342 9.40237 5.79289 9.79289C5.40237 10.1834 5.40237 10.8166 5.79289 11.2071L8.79289 14.2071C9.00474 14.419 9.3004 14.5247 9.59854 14.4951C9.89667 14.4656 10.1659 14.304 10.3321 14.0547L15.3321 6.5547Z"
fill="currentColor"/>
</svg>),
'chevron-down': (
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -277,6 +311,18 @@ const Svg = ({ name, className: className2, strokeWidth = 2 }) => {
/>
</svg>
),
refresh: (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 14 17"
fill="none">
<path
d="M13 9.9271C13 13.189 10.3137 15.8333 7 15.8333C3.68629 15.8333 1 13.189 1 9.9271C1 6.66517 3.68629 4.02085 7 4.02085C9 4.02085 10.986 4.99917 12 5.77084M12 5.77084L8.33333 7.08334M12 5.77084L10.6667 1.83334"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"/>
</svg>
),
save: (
<svg
xmlns="http://www.w3.org/2000/svg"

View File

@@ -23,6 +23,28 @@ class CascadeController {
}
onInit()
}
capture(environment, width = 512, height = 384) {
environment.camera.aspect = width / height;
environment.camera.updateProjectionMatrix();
environment.renderer.setSize(width, height);
environment.renderer.render(environment.scene, environment.camera, null, false);
let imgBlob = new Promise((resolve, reject) => {
environment.renderer.domElement.toBlob(
(blob) => {
blob.name = `part_capture-${ Date.now() }`
resolve(blob)
},
'image/jpeg',
1
);
})
// Return to original dimensions
environment.onWindowResize();
return imgBlob
}
}
export default new CascadeController()

View File

@@ -0,0 +1,21 @@
// TODO: create a tidy util for uploading to Cloudinary and returning the public ID
import axios from 'axios'
const CLOUDINARY_UPLOAD_PRESET = 'CadHub_project_images'
const CLOUDINARY_UPLOAD_URL = 'https://api.cloudinary.com/v1_1/irevdev/upload'
export async function uploadToCloudinary(imgBlob) {
const imageData = new FormData()
imageData.append('upload_preset', CLOUDINARY_UPLOAD_PRESET)
imageData.append('file', imgBlob)
let upload = axios.post(CLOUDINARY_UPLOAD_URL, imageData)
try {
const { data } = await upload
if (data && data.public_id !== '') {
return data
}
} catch (e) {
console.error('ERROR', e)
}
}