diff --git a/app/web/src/components/CaptureButton/CaptureButton.tsx b/app/web/src/components/CaptureButton/CaptureButton.tsx index f8ac5e6..8f9ee01 100644 --- a/app/web/src/components/CaptureButton/CaptureButton.tsx +++ b/app/web/src/components/CaptureButton/CaptureButton.tsx @@ -1,14 +1,15 @@ import { useState } from 'react' import { toast } from '@redwoodjs/web/toast' -import Popover from '@material-ui/core/Popover' -import Svg from 'src/components/Svg/Svg' -import Button from 'src/components/Button/Button' +import { toJpeg } from 'html-to-image' + import { useIdeContext } from 'src/helpers/hooks/useIdeContext' import { canvasToBlob, blobTo64 } from 'src/helpers/canvasToBlob' import { useUpdateProjectImages } from 'src/helpers/hooks/useUpdateProjectImages' - +import { requestRenderStateless } from 'src/helpers/hooks/useIdeState' +import { PureIdeViewer } from 'src/components/IdeViewer/IdeViewer' import SocialCardCell from 'src/components/SocialCardCell/SocialCardCell' -import { toJpeg } from 'html-to-image' + +export const captureSize = { width: 500, height: 522 } const anchorOrigin = { vertical: 'bottom', @@ -19,175 +20,247 @@ const transformOrigin = { horizontal: 'center', } -const CaptureButton = ({ - canEdit, - TheButton, - shouldUpdateImage, - projectTitle, - userName, +export const CaptureButtonViewer = ({ + onInit, + onScadImage, + canvasRatio = 1, +}: { + onInit: (a: any) => void + onScadImage: (a: any) => void + canvasRatio: number }) => { - const [captureState, setCaptureState] = useState({}) - const [anchorEl, setAnchorEl] = useState(null) - const [whichPopup, setWhichPopup] = useState(null) - const { state, project } = useIdeContext() - const ref = React.useRef(null) - const { updateProjectImages } = useUpdateProjectImages({}) - - const onCapture = async () => { - const threeInstance = state.threeInstance - const isOpenScadImage = state?.objectData?.type === 'png' - let imgBlob - let image64 - if (!isOpenScadImage) { - imgBlob = canvasToBlob(threeInstance, { width: 500, height: 375 }) - image64 = blobTo64( - await canvasToBlob(threeInstance, { width: 500, height: 522 }) - ) - } else { - imgBlob = state.objectData.data - image64 = blobTo64(state.objectData.data) - } - const config = { - image: await imgBlob, - currImage: project?.mainImage, - imageObjectURL: window.URL.createObjectURL(await imgBlob), - callback: uploadAndUpdateImage, - cloudinaryImgURL: '', - updated: false, - image64: await image64, - } - setCaptureState(config) - - async function uploadAndUpdateImage() { - const upload = async () => { - const socialCard64 = toJpeg(ref.current, { - cacheBust: true, - quality: 0.7, - }) - - // uploading in two separate mutations because of the 100kb limit of the lambda functions - const imageUploadPromise1 = updateProjectImages({ - variables: { - id: project?.id, - mainImage64: await config.image64, - }, - }) - const imageUploadPromise2 = updateProjectImages({ - variables: { - id: project?.id, - socialCard64: await socialCard64, - }, - }) - return Promise.all([imageUploadPromise2, imageUploadPromise1]) - } - const promise = upload() - toast.promise(promise, { - loading: 'Saving Image/s', - success: Image/s saved!, - error: Problem saving., + const { state } = useIdeContext() + const threeInstance = React.useRef(null) + const [dataType, dataTypeSetter] = useState(state?.objectData?.type) + const [artifact, artifactSetter] = useState(state?.objectData?.data) + const [isLoading, isLoadingSetter] = useState(false) + const getThreeInstance = (_threeInstance) => { + threeInstance.current = _threeInstance + onInit(_threeInstance) + } + const onCameraChange = (camera) => { + const renderPromise = + state.ideType === 'openscad' && + requestRenderStateless({ + state, + camera, + viewerSize: { + width: threeInstance.current.size.width * canvasRatio, + height: threeInstance.current.size.height * canvasRatio, + }, }) - const [{ data }] = await promise - return data?.updateProjectImages?.mainImage - } - - // if there isn't a screenshot saved yet, just go ahead and save right away - if (shouldUpdateImage) { - config.cloudinaryImgURL = await uploadAndUpdateImage() - config.updated = true - setCaptureState(config) + if (!renderPromise) { + return } + isLoadingSetter(true) + renderPromise.then(async ({ objectData }) => { + isLoadingSetter(false) + dataTypeSetter(objectData?.type) + artifactSetter(objectData?.data) + if (objectData?.type === 'png') { + onScadImage(await blobTo64(objectData?.data)) + } + }) } - const handleClick = ({ event, whichPopup }) => { - setAnchorEl(event.currentTarget) - setWhichPopup(whichPopup) - } - - const handleClose = () => { - setAnchorEl(null) - setWhichPopup(null) - } return ( -
- {canEdit && ( -
- { - handleClick({ event, whichPopup: 'capture' }) - onCapture() - }} - /> - -
- {!captureState ? ( - 'Loading...' - ) : ( -
-
Thumbnail
-
- -
-
- )} -
Social Media Card
-
-
-
- -
-
-
-
- {captureState.currImage && !captureState.updated ? ( - - ) : ( -
- {' '} - Project Images Updated -
- )} -
-
-
-
- )} + + ) +} + +function TabContent() { + return ( +
+ +
) } -export default CaptureButton +function SocialCardLiveViewer({ + forwardRef, + onUpload, + children, + partSnapShot64, +}) { + const { project } = useIdeContext() + return ( + <> +

Set social Image

+
+
+
+
+ children} + /> +
+
+
+
+ + + ) +} + +function ThumbnailViewer({ forwardRef, onUpload, children, partSnapShot64 }) { + return ( + <> +

Set thumbnail

+
+
+ {children} + {partSnapShot64 && ( + + )} +
+
+ + + ) +} + +function IsolatedCanvas({ + RenderComponent, + canvasRatio = 1, + size, + uploadKey, +}: { + canvasRatio?: number + uploadKey: 'socialCard64' | 'mainImage64' + size: { + width: number + height: number + } + RenderComponent: React.FC<{ + forwardRef: React.Ref + children: React.ReactNode + partSnapShot64: string + onUpload: (a: any) => void + }> +}) { + const { project } = useIdeContext() + const { updateProjectImages } = useUpdateProjectImages({}) + const [partSnapShot64, partSnapShot64Setter] = React.useState('') + const [scadSnapShot64, scadSnapShot64Setter] = React.useState('') + + const captureRef = React.useRef(null) + + const threeInstance = React.useRef(null) + const onInit = (_threeInstance) => (threeInstance.current = _threeInstance) + const upload = async () => { + const uploadPromise = new Promise((resolve, reject) => { + const asyncHelper = async () => { + if (!scadSnapShot64) { + partSnapShot64Setter( + await blobTo64(await canvasToBlob(threeInstance.current, size)) + ) + } else { + partSnapShot64Setter(scadSnapShot64) + } + + setTimeout(async () => { + const capturedImage = await toJpeg(captureRef.current, { + cacheBust: true, + quality: 0.7, + }) + await updateProjectImages({ + variables: { + id: project?.id, + [uploadKey]: capturedImage, + }, + }) + partSnapShot64Setter('') + resolve(capturedImage) + }) + } + asyncHelper() + }) + toast.promise(uploadPromise, { + loading: 'Saving Image', + success: (finalImg: string) => ( +
+ Image saved! + +
+ ), + error: Problem saving., + }) + } + + return ( +
+ +
+ +
+
+
+ ) +} + +export default function CaptureButton({ TheButton }) { + const { state, thunkDispatch } = useIdeContext() + + return ( + { + thunkDispatch({ + type: 'addEditorModel', + payload: { + type: 'component', + label: 'Social Media Card', + Component: TabContent, + }, + }) + thunkDispatch({ + type: 'switchEditorModel', + payload: state.editorTabs.length, + }) + }} + /> + ) +} diff --git a/app/web/src/components/EditorMenu/EditorMenu.tsx b/app/web/src/components/EditorMenu/EditorMenu.tsx index 7475e11..242d49c 100644 --- a/app/web/src/components/EditorMenu/EditorMenu.tsx +++ b/app/web/src/components/EditorMenu/EditorMenu.tsx @@ -62,7 +62,7 @@ const EditorMenu = () => { }) thunkDispatch({ type: 'switchEditorModel', - payload: state.models.length, + payload: state.editorTabs.length, }) }} /> diff --git a/app/web/src/components/EditorMenu/helpers.ts b/app/web/src/components/EditorMenu/helpers.ts index 032a311..466a58a 100644 --- a/app/web/src/components/EditorMenu/helpers.ts +++ b/app/web/src/components/EditorMenu/helpers.ts @@ -74,12 +74,8 @@ export const makeStlDownloadHandler = requestRender({ state, dispatch, - code: state.code, - viewerSize: state.viewerSize, - camera: state.camera, quality: 'high', specialCadProcess, - parameters: state.currentParameters, }).then( (result) => result && saveFile(makeStlBlobFromGeo(result.data)) ) diff --git a/app/web/src/components/IdeContainer/IdeContainer.tsx b/app/web/src/components/IdeContainer/IdeContainer.tsx index 0286838..b67be61 100644 --- a/app/web/src/components/IdeContainer/IdeContainer.tsx +++ b/app/web/src/components/IdeContainer/IdeContainer.tsx @@ -8,7 +8,7 @@ import PanelToolbar from 'src/components/PanelToolbar/PanelToolbar' import { use3dViewerResize } from 'src/helpers/hooks/use3dViewerResize' const IdeEditor = lazy(() => import('src/components/IdeEditor/IdeEditor')) -const IdeViewer = lazy(() => import('src/components/IdeViewer/IdeViewer')) +import IdeViewer from 'src/components/IdeViewer/IdeViewer' const SmallLoadingPing = (
@@ -33,11 +33,7 @@ const ELEMENT_MAP = { ), - Viewer: ( - - - - ), + Viewer: , Console: , } diff --git a/app/web/src/components/IdeEditor/IdeEditor.tsx b/app/web/src/components/IdeEditor/IdeEditor.tsx index 7840d4c..2e6ed8f 100644 --- a/app/web/src/components/IdeEditor/IdeEditor.tsx +++ b/app/web/src/components/IdeEditor/IdeEditor.tsx @@ -60,24 +60,21 @@ const IdeEditor = ({ Loading }) => { requestRender({ state, dispatch, - code: state.code, - viewerSize: state.viewerSize, - camera: state.camera, - parameters: state.currentParameters, }) }) localStorage.setItem(makeCodeStoreKey(state.ideType), state.code) } } + const currentTab = state.editorTabs[state.currentModel] return (
- {state.models.length > 1 && ( + {state.editorTabs.length > 1 && (
- {state.models.map((model, i) => ( + {state.editorTabs.map((model, i) => (
)} - {state.models[state.currentModel].type === 'code' ? ( + {currentTab.type === 'code' && ( { language={ideTypeToLanguageMap[state.ideType] || 'cpp'} onChange={handleCodeChange} /> - ) : ( + )} + {currentTab.type === 'guide' && (
- +
)} + {currentTab.type === 'component' && }
) } diff --git a/app/web/src/components/IdeHeader/IdeHeader.tsx b/app/web/src/components/IdeHeader/IdeHeader.tsx index ecc4863..9dcd01d 100644 --- a/app/web/src/components/IdeHeader/IdeHeader.tsx +++ b/app/web/src/components/IdeHeader/IdeHeader.tsx @@ -12,6 +12,7 @@ import CaptureButton from 'src/components/CaptureButton/CaptureButton' import { useIdeContext } from 'src/helpers/hooks/useIdeContext' import Gravatar from 'src/components/Gravatar/Gravatar' import EditableProjectTitle from 'src/components/EditableProjecTitle/EditableProjecTitle' +import SocialCardModal from 'src/components/SocialCardModal/SocialCardModal' const FORK_PROJECT_MUTATION = gql` mutation ForkProjectMutation($input: ForkProjectInput!) { @@ -121,10 +122,6 @@ export default function IdeHeader({
{canEdit && !isProfile && ( ( @@ -148,42 +148,53 @@ function Sphere(props) { ) } -const IdeViewer = ({ Loading }) => { - const { state, thunkDispatch } = useIdeContext() +export function PureIdeViewer({ + dataType, + artifact, + onInit, + onCameraChange, + isLoading, + isMinimal = false, + scadRatio = 1, +}: { + dataType: 'INIT' | ArtifactTypes + artifact: any + isLoading: boolean + onInit: Function + onCameraChange: Function + isMinimal?: boolean + scadRatio?: number +}) { const [isDragging, setIsDragging] = useState(false) const [image, setImage] = useState() - const onInit = (threeInstance) => { - thunkDispatch({ type: 'setThreeInstance', payload: threeInstance }) - } - useEffect(() => { - setImage(state.objectData?.type === 'png' && state.objectData?.data) + setImage(dataType === 'png' && artifact) setIsDragging(false) - }, [state.objectData?.type, state.objectData?.data]) + }, [dataType, artifact]) const PrimitiveArray = React.useMemo( () => - state.objectData?.type === 'primitive-array' && state.objectData?.data, - [state.objectData?.type, state.objectData?.data] + dataType === 'primitive-array' && artifact?.map((mesh) => mesh.clone()), + [dataType, artifact] ) // the following are tailwind colors in hex, can't use these classes to color three.js meshes. const pink400 = '#F472B6' const indigo300 = '#A5B4FC' const indigo900 = '#312E81' - const jscadLightIntensity = - state.objectData?.type === 'geometry' && - state.objectData?.data && - state.objectData?.data.length - ? 0.5 - : 1.2 + const jscadLightIntensity = PrimitiveArray ? 0.5 : 1.1 return ( -
+
{image && (
code-cad preview { )}
{ setIsDragging(true)} onInit={onInit} - onCameraChange={(camera) => { - thunkDispatch({ - type: 'updateCamera', - payload: { camera }, - }) - thunkDispatch((dispatch, getState) => { - const state = getState() - if (['png', 'INIT'].includes(state.objectData?.type)) { - dispatch({ type: 'setLoading' }) - requestRender({ - state, - dispatch, - code: state.code, - viewerSize: state.viewerSize, - camera, - parameters: state.currentParameters, - }) - } - }) - }} + onCameraChange={onCameraChange} /> { intensity={jscadLightIntensity} /> - - + { material-transparent rotation-x={Math.PI / 2} /> - - - - {state.objectData?.type === 'png' && ( + {!isMinimal && ( + + + + )} + {dataType === 'png' && ( <> @@ -264,8 +257,10 @@ const IdeViewer = ({ Loading }) => { )} - {state.objectData?.type === 'geometry' && state.objectData?.data && ( - + {dataType === 'geometry' && artifact && ( + + + )} {PrimitiveArray && PrimitiveArray.map((mesh, index) => ( @@ -273,10 +268,55 @@ const IdeViewer = ({ Loading }) => { ))}
- - + + {!isMinimal && }
) } +const IdeViewer = ({ + handleOwnCamera = false, +}: { + handleOwnCamera?: boolean +}) => { + const { state, thunkDispatch } = useIdeContext() + const dataType = state.objectData?.type + const artifact = state.objectData?.data + + const onInit = (threeInstance) => { + thunkDispatch({ type: 'setThreeInstance', payload: threeInstance }) + } + const onCameraChange = (camera) => { + if (handleOwnCamera) { + console.log('yo') + return + } + thunkDispatch({ + type: 'updateCamera', + payload: { camera }, + }) + thunkDispatch((dispatch, getState) => { + const state = getState() + if (['png', 'INIT'].includes(state?.objectData?.type)) { + dispatch({ type: 'setLoading' }) + requestRender({ + state, + dispatch, + camera, + }) + } + }) + } + + return ( + + ) +} + export default IdeViewer diff --git a/app/web/src/components/IdeWrapper/useRender.ts b/app/web/src/components/IdeWrapper/useRender.ts index cec3da8..2d766b2 100644 --- a/app/web/src/components/IdeWrapper/useRender.ts +++ b/app/web/src/components/IdeWrapper/useRender.ts @@ -10,9 +10,6 @@ export const useRender = () => { requestRender({ state, dispatch, - code: state.code, - viewerSize: state.viewerSize, - camera: state.camera, parameters: disableParams ? {} : state.currentParameters, }) }) diff --git a/app/web/src/components/ProfileViewer/ProfileViewer.tsx b/app/web/src/components/ProfileViewer/ProfileViewer.tsx index 4caa79f..b675197 100644 --- a/app/web/src/components/ProfileViewer/ProfileViewer.tsx +++ b/app/web/src/components/ProfileViewer/ProfileViewer.tsx @@ -1,15 +1,11 @@ -import { lazy, Suspense } from 'react' -const IdeViewer = lazy(() => import('src/components/IdeViewer/IdeViewer')) +import IdeViewer from 'src/components/IdeViewer/IdeViewer' import { use3dViewerResize } from 'src/helpers/hooks/use3dViewerResize' -import { BigLoadingPing } from 'src/components/IdeContainer/IdeContainer' const ProfileViewer = () => { const { viewerDomRef } = use3dViewerResize() return (
- - - +
) } diff --git a/app/web/src/components/SocialCardCell/SocialCardCell.tsx b/app/web/src/components/SocialCardCell/SocialCardCell.tsx index 83f4c7f..6341bc8 100644 --- a/app/web/src/components/SocialCardCell/SocialCardCell.tsx +++ b/app/web/src/components/SocialCardCell/SocialCardCell.tsx @@ -37,7 +37,7 @@ export const Failure = ({ error }: CellFailureProps) => ( export const Success = ({ userProject, - variables: { image64 }, + variables: { image64, LiveProjectViewer }, }: CellSuccessProps) => { const image = userProject?.Project?.mainImage const gravatar = userProject?.image @@ -47,7 +47,7 @@ export const Success = ({ : userProject?.Project?.description || '' return (
@@ -96,6 +96,18 @@ export const Success = ({ /> )}
+ +
+ {LiveProjectViewer && ( +
+ +
+ )} +
{ resolve(blob) }, - 'image/jpeg', + 'image/png', 0.75 ) }) diff --git a/app/web/src/helpers/hooks/use3dViewerResize.ts b/app/web/src/helpers/hooks/use3dViewerResize.ts index dd5fe24..6266a57 100644 --- a/app/web/src/helpers/hooks/use3dViewerResize.ts +++ b/app/web/src/helpers/hooks/use3dViewerResize.ts @@ -23,10 +23,7 @@ export const use3dViewerResize = () => { requestRender({ state, dispatch, - code: state.code, viewerSize: { width, height }, - camera: state.camera, - parameters: state.currentParameters, }) } }) diff --git a/app/web/src/helpers/hooks/useIdeState.ts b/app/web/src/helpers/hooks/useIdeState.ts index aaa880f..6b3aff0 100644 --- a/app/web/src/helpers/hooks/useIdeState.ts +++ b/app/web/src/helpers/hooks/useIdeState.ts @@ -25,19 +25,33 @@ interface XYZ { z: number } -interface EditorModel { - type: 'code' | 'guide' +interface CodeTab { + type: 'code' label: string content?: string } +interface GuideTab { + type: 'guide' + label: string + content: string +} + +interface ComponentTab { + type: 'component' + label: string + Component: React.FC +} + +type EditorTab = GuideTab | CodeTab | ComponentTab + export interface State { ideType: 'INIT' | CadPackageType viewerContext: 'ide' | 'viewer' ideGuide?: string consoleMessages: { type: 'message' | 'error'; message: string; time: Date }[] code: string - models: EditorModel[] + editorTabs: EditorTab[] currentModel: number objectData: { type: 'INIT' | ArtifactTypes @@ -78,7 +92,7 @@ export const initialState: State = { { type: 'message', message: 'Initialising', time: new Date() }, ], code, - models: [{ type: 'code', label: 'Code' }], + editorTabs: [{ type: 'code', label: 'Code' }], currentModel: 0, objectData: { type: 'INIT', @@ -240,14 +254,14 @@ const reducer = (state: State, { type, payload }): State => { case 'addEditorModel': return { ...state, - models: [...state.models, payload], + editorTabs: [...state.editorTabs, payload], } case 'removeEditorModel': return { ...state, - models: [ - ...state.models.slice(0, payload), - ...state.models.slice(payload + 1), + editorTabs: [ + ...state.editorTabs.slice(0, payload), + ...state.editorTabs.slice(payload + 1), ], currentModel: payload === 0 ? 0 : payload - 1, } @@ -284,71 +298,84 @@ export const useIdeState = (): [State, (actionOrThunk: any) => any] => { return [state, thunkDispatch] } -interface RequestRenderArgs { +interface RequestRenderArgsStateless { state: State - dispatch: any - parameters: any - code: State['code'] - camera: State['camera'] - viewerSize: State['viewerSize'] + camera?: State['camera'] + viewerSize?: State['viewerSize'] quality?: State['objectData']['quality'] specialCadProcess?: string + parameters?: { [key: string]: any } } -export const requestRender = ({ +export const requestRenderStateless = ({ state, - dispatch, - code, camera, viewerSize, quality = 'low', specialCadProcess = null, parameters, -}: RequestRenderArgs) => { +}: RequestRenderArgsStateless): null | Promise => { if ( - state.ideType !== 'INIT' && - (!state.isLoading || state.objectData?.type === 'INIT') + !( + state.ideType !== 'INIT' && + (!state.isLoading || state.objectData?.type === 'INIT') + ) ) { - const renderFn = specialCadProcess - ? cadPackages[state.ideType][specialCadProcess] - : cadPackages[state.ideType].render - return renderFn({ - code, - settings: { - parameters: state.isCustomizerOpen ? parameters : {}, - camera, - viewerSize, - quality, - }, - }) - .then( - ({ - objectData, - message, - status, - customizerParams, - currentParameters, - }) => { - if (status === 'error') { - dispatch({ - type: 'errorRender', - payload: { message }, - }) - } else { - dispatch({ - type: 'healthyRender', - payload: { - objectData, - message, - lastRunCode: code, - customizerParams, - currentParameters, - }, - }) - return objectData - } - } - ) - .catch(() => dispatch({ type: 'resetLoading' })) // TODO should probably display something to the user here + return null } + const renderFn = specialCadProcess + ? cadPackages[state.ideType][specialCadProcess] + : cadPackages[state.ideType].render + return renderFn({ + code: state.code, + settings: { + parameters: state.isCustomizerOpen + ? parameters || state.currentParameters + : {}, + camera: camera || state.camera, + viewerSize: viewerSize || state.viewerSize, + quality, + }, + }) +} + +interface RequestRenderArgs extends RequestRenderArgsStateless { + dispatch: any +} + +export const requestRender = ({ dispatch, ...rest }: RequestRenderArgs) => { + const renderPromise = requestRenderStateless(rest) + if (!renderPromise) { + return + } + renderPromise + .then( + ({ + objectData, + message, + status, + customizerParams, + currentParameters, + }) => { + if (status === 'error') { + dispatch({ + type: 'errorRender', + payload: { message }, + }) + } else { + dispatch({ + type: 'healthyRender', + payload: { + objectData, + message, + lastRunCode: code, + customizerParams, + currentParameters, + }, + }) + return objectData + } + } + ) + .catch(() => dispatch({ type: 'resetLoading' })) // TODO should probably display something to the user here }