Put social media save popover into editor tab (#541)

and make them live
This commit was merged in pull request #541.
This commit is contained in:
Kurt Hutten
2021-10-12 06:09:56 +11:00
committed by GitHub
parent 6c093e65bf
commit 4804c3bfe9
13 changed files with 454 additions and 324 deletions

View File

@@ -1,14 +1,15 @@
import { useState } from 'react' import { useState } from 'react'
import { toast } from '@redwoodjs/web/toast' import { toast } from '@redwoodjs/web/toast'
import Popover from '@material-ui/core/Popover' import { toJpeg } from 'html-to-image'
import Svg from 'src/components/Svg/Svg'
import Button from 'src/components/Button/Button'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext' import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import { canvasToBlob, blobTo64 } from 'src/helpers/canvasToBlob' import { canvasToBlob, blobTo64 } from 'src/helpers/canvasToBlob'
import { useUpdateProjectImages } from 'src/helpers/hooks/useUpdateProjectImages' 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 SocialCardCell from 'src/components/SocialCardCell/SocialCardCell'
import { toJpeg } from 'html-to-image'
export const captureSize = { width: 500, height: 522 }
const anchorOrigin = { const anchorOrigin = {
vertical: 'bottom', vertical: 'bottom',
@@ -19,175 +20,247 @@ const transformOrigin = {
horizontal: 'center', horizontal: 'center',
} }
const CaptureButton = ({ export const CaptureButtonViewer = ({
canEdit, onInit,
TheButton, onScadImage,
shouldUpdateImage, canvasRatio = 1,
projectTitle, }: {
userName, onInit: (a: any) => void
onScadImage: (a: any) => void
canvasRatio: number
}) => { }) => {
const [captureState, setCaptureState] = useState<any>({}) const { state } = useIdeContext()
const [anchorEl, setAnchorEl] = useState(null) const threeInstance = React.useRef(null)
const [whichPopup, setWhichPopup] = useState(null) const [dataType, dataTypeSetter] = useState(state?.objectData?.type)
const { state, project } = useIdeContext() const [artifact, artifactSetter] = useState(state?.objectData?.data)
const ref = React.useRef<HTMLDivElement>(null) const [isLoading, isLoadingSetter] = useState(false)
const { updateProjectImages } = useUpdateProjectImages({}) const getThreeInstance = (_threeInstance) => {
threeInstance.current = _threeInstance
const onCapture = async () => { onInit(_threeInstance)
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 = { const onCameraChange = (camera) => {
image: await imgBlob, const renderPromise =
currImage: project?.mainImage, state.ideType === 'openscad' &&
imageObjectURL: window.URL.createObjectURL(await imgBlob), requestRenderStateless({
callback: uploadAndUpdateImage, state,
cloudinaryImgURL: '', camera,
updated: false, viewerSize: {
image64: await image64, width: threeInstance.current.size.width * canvasRatio,
} height: threeInstance.current.size.height * canvasRatio,
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({ if (!renderPromise) {
variables: { return
id: project?.id, }
socialCard64: await socialCard64, isLoadingSetter(true)
}, renderPromise.then(async ({ objectData }) => {
isLoadingSetter(false)
dataTypeSetter(objectData?.type)
artifactSetter(objectData?.data)
if (objectData?.type === 'png') {
onScadImage(await blobTo64(objectData?.data))
}
}) })
return Promise.all([imageUploadPromise2, imageUploadPromise1])
}
const promise = upload()
toast.promise(promise, {
loading: 'Saving Image/s',
success: <b>Image/s saved!</b>,
error: <b>Problem saving.</b>,
})
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)
}
}
const handleClick = ({ event, whichPopup }) => {
setAnchorEl(event.currentTarget)
setWhichPopup(whichPopup)
}
const handleClose = () => {
setAnchorEl(null)
setWhichPopup(null)
}
return ( return (
<div> <PureIdeViewer
{canEdit && ( scadRatio={canvasRatio}
<div> dataType={dataType}
<TheButton artifact={artifact}
onClick={async (event) => { onInit={getThreeInstance}
handleClick({ event, whichPopup: 'capture' }) onCameraChange={onCameraChange}
onCapture() isLoading={isLoading}
}} isMinimal
/> />
<Popover )
id={'capture-popover'} }
open={whichPopup === 'capture'}
anchorEl={anchorEl} function TabContent() {
onClose={handleClose} return (
anchorOrigin={anchorOrigin} <div className="bg-ch-gray-800 h-full overflow-y-auto px-8 pb-16">
transformOrigin={transformOrigin} <IsolatedCanvas
className="material-ui-overrides transform translate-y-4" size={{ width: 500, height: 375 }}
> uploadKey="mainImage64"
<div className="text-sm p-4 text-gray-500"> RenderComponent={ThumbnailViewer}
{!captureState ? ( />
'Loading...' <IsolatedCanvas
) : ( canvasRatio={2}
<div className=""> size={captureSize}
<div className="text-lg">Thumbnail</div> uploadKey="socialCard64"
<div RenderComponent={SocialCardLiveViewer}
className="rounded"
style={{ width: 'fit-content', overflow: 'hidden' }}
>
<img src={captureState.imageObjectURL} className="w-32" />
</div>
</div>
)}
<div className="text-lg mt-4">Social Media Card</div>
<div className="rounded-lg shadow-md overflow-hidden">
<div
className="transform scale-50 origin-top-left"
style={{ width: '600px', height: '315px' }}
>
<div style={{ width: '1200px', height: '630px' }} ref={ref}>
<SocialCardCell
userName={userName}
projectTitle={projectTitle}
image64={captureState.image64}
/> />
</div>
</div>
</div>
<div className="mt-4 text-indigo-800">
{captureState.currImage && !captureState.updated ? (
<Button
iconName="refresh"
className="shadow-md hover:shadow-lg border-indigo-600 border-2 border-opacity-0 hover:border-opacity-100 bg-indigo-200 text-indigo-100 text-opacity-100 bg-opacity-80"
shouldAnimateHover
onClick={async () => {
const cloudinaryImg = await captureState.callback()
setCaptureState({
...captureState,
currImage: cloudinaryImg,
updated: true,
})
}}
>
Update Project Images
</Button>
) : (
<div className="flex justify-center mb-4">
<Svg
name="checkmark"
className="mr-2 w-6 text-indigo-600"
/>{' '}
Project Images Updated
</div>
)}
</div>
</div>
</Popover>
</div>
)}
</div> </div>
) )
} }
export default CaptureButton function SocialCardLiveViewer({
forwardRef,
onUpload,
children,
partSnapShot64,
}) {
const { project } = useIdeContext()
return (
<>
<h3 className="text-2xl text-ch-gray-300 pt-4">Set social Image</h3>
<div className="flex py-4">
<div className="rounded-md shadow-ch border border-gray-400 overflow-hidden">
<div
className="transform scale-50 origin-top-left"
style={{ width: '600px', height: '315px' }}
>
<div style={{ width: '1200px', height: '630px' }} ref={forwardRef}>
<SocialCardCell
userName={project.user.userName}
projectTitle={project.title}
image64={partSnapShot64}
LiveProjectViewer={() => children}
/>
</div>
</div>
</div>
</div>
<button className="bg-gray-200 p-2 rounded-sm" onClick={onUpload}>
save image
</button>
</>
)
}
function ThumbnailViewer({ forwardRef, onUpload, children, partSnapShot64 }) {
return (
<>
<h3 className="text-2xl text-ch-gray-300 pt-4">Set thumbnail</h3>
<div
style={{ width: '500px', height: '375px' }}
className="rounded-md shadow-ch border border-gray-400 overflow-hidden my-4"
>
<div className="h-full w-full relative" ref={forwardRef}>
{children}
{partSnapShot64 && (
<img src={partSnapShot64} className="absolute inset-0" />
)}
</div>
</div>
<button className="bg-gray-200 p-2 rounded-sm" onClick={onUpload}>
save thumbnail
</button>
</>
)
}
function IsolatedCanvas({
RenderComponent,
canvasRatio = 1,
size,
uploadKey,
}: {
canvasRatio?: number
uploadKey: 'socialCard64' | 'mainImage64'
size: {
width: number
height: number
}
RenderComponent: React.FC<{
forwardRef: React.Ref<any>
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<HTMLDivElement>(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) => (
<div className="flex flex-col items-center">
<b className="py-2">Image saved!</b>
<img src={finalImg} />
</div>
),
error: <b>Problem saving.</b>,
})
}
return (
<div>
<RenderComponent
forwardRef={captureRef}
onUpload={upload}
partSnapShot64={partSnapShot64}
>
<div
style={{
width: `${size.width * canvasRatio}px`,
height: `${size.height * canvasRatio}px`,
}}
>
<CaptureButtonViewer
onInit={onInit}
onScadImage={scadSnapShot64Setter}
canvasRatio={canvasRatio}
/>
</div>
</RenderComponent>
</div>
)
}
export default function CaptureButton({ TheButton }) {
const { state, thunkDispatch } = useIdeContext()
return (
<TheButton
onClick={() => {
thunkDispatch({
type: 'addEditorModel',
payload: {
type: 'component',
label: 'Social Media Card',
Component: TabContent,
},
})
thunkDispatch({
type: 'switchEditorModel',
payload: state.editorTabs.length,
})
}}
/>
)
}

View File

@@ -62,7 +62,7 @@ const EditorMenu = () => {
}) })
thunkDispatch({ thunkDispatch({
type: 'switchEditorModel', type: 'switchEditorModel',
payload: state.models.length, payload: state.editorTabs.length,
}) })
}} }}
/> />

View File

@@ -74,12 +74,8 @@ export const makeStlDownloadHandler =
requestRender({ requestRender({
state, state,
dispatch, dispatch,
code: state.code,
viewerSize: state.viewerSize,
camera: state.camera,
quality: 'high', quality: 'high',
specialCadProcess, specialCadProcess,
parameters: state.currentParameters,
}).then( }).then(
(result) => result && saveFile(makeStlBlobFromGeo(result.data)) (result) => result && saveFile(makeStlBlobFromGeo(result.data))
) )

View File

@@ -8,7 +8,7 @@ import PanelToolbar from 'src/components/PanelToolbar/PanelToolbar'
import { use3dViewerResize } from 'src/helpers/hooks/use3dViewerResize' import { use3dViewerResize } from 'src/helpers/hooks/use3dViewerResize'
const IdeEditor = lazy(() => import('src/components/IdeEditor/IdeEditor')) 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 = ( const SmallLoadingPing = (
<div className="bg-ch-gray-800 text-gray-200 font-ropa-sans relative w-full h-full flex justify-center items-center"> <div className="bg-ch-gray-800 text-gray-200 font-ropa-sans relative w-full h-full flex justify-center items-center">
@@ -33,11 +33,7 @@ const ELEMENT_MAP = {
<IdeEditor Loading={SmallLoadingPing} /> <IdeEditor Loading={SmallLoadingPing} />
</Suspense> </Suspense>
), ),
Viewer: ( Viewer: <IdeViewer />,
<Suspense fallback={BigLoadingPing}>
<IdeViewer Loading={BigLoadingPing} />
</Suspense>
),
Console: <IdeConsole />, Console: <IdeConsole />,
} }

View File

@@ -60,24 +60,21 @@ const IdeEditor = ({ Loading }) => {
requestRender({ requestRender({
state, state,
dispatch, dispatch,
code: state.code,
viewerSize: state.viewerSize,
camera: state.camera,
parameters: state.currentParameters,
}) })
}) })
localStorage.setItem(makeCodeStoreKey(state.ideType), state.code) localStorage.setItem(makeCodeStoreKey(state.ideType), state.code)
} }
} }
const currentTab = state.editorTabs[state.currentModel]
return ( return (
<div // eslint-disable-line jsx-a11y/no-static-element-interactions <div // eslint-disable-line jsx-a11y/no-static-element-interactions
className="h-full" className="h-full"
onKeyDown={handleSaveHotkey} onKeyDown={handleSaveHotkey}
> >
{state.models.length > 1 && ( {state.editorTabs.length > 1 && (
<fieldset className="bg-ch-gray-700 text-ch-gray-300 flex m-0 p-0"> <fieldset className="bg-ch-gray-700 text-ch-gray-300 flex m-0 p-0">
{state.models.map((model, i) => ( {state.editorTabs.map((model, i) => (
<label <label
key={model.type + '-' + i} key={model.type + '-' + i}
className={ className={
@@ -117,7 +114,7 @@ const IdeEditor = ({ Loading }) => {
))} ))}
</fieldset> </fieldset>
)} )}
{state.models[state.currentModel].type === 'code' ? ( {currentTab.type === 'code' && (
<Editor <Editor
defaultValue={state.code} defaultValue={state.code}
value={state.code} value={state.code}
@@ -128,11 +125,13 @@ const IdeEditor = ({ Loading }) => {
language={ideTypeToLanguageMap[state.ideType] || 'cpp'} language={ideTypeToLanguageMap[state.ideType] || 'cpp'}
onChange={handleCodeChange} onChange={handleCodeChange}
/> />
) : ( )}
{currentTab.type === 'guide' && (
<div className="bg-ch-gray-800 h-full"> <div className="bg-ch-gray-800 h-full">
<EditorGuide content={state.models[state.currentModel].content} /> <EditorGuide content={currentTab.content} />
</div> </div>
)} )}
{currentTab.type === 'component' && <currentTab.Component />}
</div> </div>
) )
} }

View File

@@ -12,6 +12,7 @@ import CaptureButton from 'src/components/CaptureButton/CaptureButton'
import { useIdeContext } from 'src/helpers/hooks/useIdeContext' import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import Gravatar from 'src/components/Gravatar/Gravatar' import Gravatar from 'src/components/Gravatar/Gravatar'
import EditableProjectTitle from 'src/components/EditableProjecTitle/EditableProjecTitle' import EditableProjectTitle from 'src/components/EditableProjecTitle/EditableProjecTitle'
import SocialCardModal from 'src/components/SocialCardModal/SocialCardModal'
const FORK_PROJECT_MUTATION = gql` const FORK_PROJECT_MUTATION = gql`
mutation ForkProjectMutation($input: ForkProjectInput!) { mutation ForkProjectMutation($input: ForkProjectInput!) {
@@ -121,10 +122,6 @@ export default function IdeHeader({
<div className="grid grid-flow-col-dense gap-4 items-center mr-4"> <div className="grid grid-flow-col-dense gap-4 items-center mr-4">
{canEdit && !isProfile && ( {canEdit && !isProfile && (
<CaptureButton <CaptureButton
canEdit={canEdit}
projectTitle={project?.title}
userName={project?.user?.userName}
shouldUpdateImage={!project?.mainImage}
TheButton={({ onClick }) => ( TheButton={({ onClick }) => (
<TopButton <TopButton
onClick={onClick} onClick={onClick}

View File

@@ -1,13 +1,12 @@
import { useIdeContext } from 'src/helpers/hooks/useIdeContext' import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
import * as THREE from 'three' import * as THREE from 'three'
import { useRef, useState, useEffect } from 'react' import { useRef, useState, useEffect, Suspense } from 'react'
import { Canvas, useThree } from '@react-three/fiber' import { Canvas, useThree } from '@react-three/fiber'
import { import {
PerspectiveCamera, PerspectiveCamera,
GizmoHelper, GizmoHelper,
GizmoViewport, GizmoViewport,
OrbitControls, OrbitControls,
Environment,
useTexture, useTexture,
} from '@react-three/drei' } from '@react-three/drei'
import { useEdgeSplit } from 'src/helpers/hooks/useEdgeSplit' import { useEdgeSplit } from 'src/helpers/hooks/useEdgeSplit'
@@ -16,6 +15,7 @@ import { requestRender } from 'src/helpers/hooks/useIdeState'
import texture from './dullFrontLitMetal.png' import texture from './dullFrontLitMetal.png'
import Customizer from 'src/components/Customizer/Customizer' import Customizer from 'src/components/Customizer/Customizer'
import DelayedPingAnimation from 'src/components/DelayedPingAnimation/DelayedPingAnimation' import DelayedPingAnimation from 'src/components/DelayedPingAnimation/DelayedPingAnimation'
import type { ArtifactTypes } from 'src/helpers/cadPackages/common'
const thresholdAngle = 12 const thresholdAngle = 12
@@ -35,13 +35,13 @@ function Asset({ geometry: incomingGeo }) {
<group dispose={null}> <group dispose={null}>
<mesh ref={mesh} scale={[1, 1, 1]} geometry={incomingGeo}> <mesh ref={mesh} scale={[1, 1, 1]} geometry={incomingGeo}>
<meshPhysicalMaterial <meshPhysicalMaterial
envMapIntensity={2} envMapIntensity={0.1}
color="#F472B6" color="#F472B6"
map={colorMap} map={colorMap}
clearcoat={0.1} clearcoat={0.1}
clearcoatRoughness={0.2} clearcoatRoughness={0.2}
roughness={10} roughness={10}
metalness={0.9} metalness={0.7}
smoothShading smoothShading
/> />
</mesh> </mesh>
@@ -148,42 +148,53 @@ function Sphere(props) {
) )
} }
const IdeViewer = ({ Loading }) => { export function PureIdeViewer({
const { state, thunkDispatch } = useIdeContext() 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 [isDragging, setIsDragging] = useState(false)
const [image, setImage] = useState() const [image, setImage] = useState()
const onInit = (threeInstance) => {
thunkDispatch({ type: 'setThreeInstance', payload: threeInstance })
}
useEffect(() => { useEffect(() => {
setImage(state.objectData?.type === 'png' && state.objectData?.data) setImage(dataType === 'png' && artifact)
setIsDragging(false) setIsDragging(false)
}, [state.objectData?.type, state.objectData?.data]) }, [dataType, artifact])
const PrimitiveArray = React.useMemo( const PrimitiveArray = React.useMemo(
() => () =>
state.objectData?.type === 'primitive-array' && state.objectData?.data, dataType === 'primitive-array' && artifact?.map((mesh) => mesh.clone()),
[state.objectData?.type, state.objectData?.data] [dataType, artifact]
) )
// the following are tailwind colors in hex, can't use these classes to color three.js meshes. // the following are tailwind colors in hex, can't use these classes to color three.js meshes.
const pink400 = '#F472B6' const pink400 = '#F472B6'
const indigo300 = '#A5B4FC' const indigo300 = '#A5B4FC'
const indigo900 = '#312E81' const indigo900 = '#312E81'
const jscadLightIntensity = const jscadLightIntensity = PrimitiveArray ? 0.5 : 1.1
state.objectData?.type === 'geometry' &&
state.objectData?.data &&
state.objectData?.data.length
? 0.5
: 1.2
return ( return (
<div className="relative h-full bg-ch-gray-800"> <div className="relative h-full bg-ch-gray-800 cursor-grab">
{image && ( {image && (
<div <div
className={`absolute inset-0 transition-opacity duration-500 ${ className={`absolute inset-0 transition-opacity duration-500 ${
isDragging ? 'opacity-25' : 'opacity-100' isDragging ? 'opacity-25' : 'opacity-100'
}`} }`}
style={{
transform: `translate(${
scadRatio !== 1 ? '-250px, -261px' : '0px, 0px'
})`,
}}
> >
<img <img
alt="code-cad preview" alt="code-cad preview"
@@ -195,7 +206,7 @@ const IdeViewer = ({ Loading }) => {
)} )}
<div // eslint-disable-line jsx-a11y/no-static-element-interactions <div // eslint-disable-line jsx-a11y/no-static-element-interactions
className={`opacity-0 absolute inset-0 transition-opacity duration-500 ${ className={`opacity-0 absolute inset-0 transition-opacity duration-500 ${
!(isDragging || state.objectData?.type !== 'png') !(isDragging || dataType !== 'png')
? 'hover:opacity-50' ? 'hover:opacity-50'
: 'opacity-100' : 'opacity-100'
}`} }`}
@@ -205,26 +216,7 @@ const IdeViewer = ({ Loading }) => {
<Controls <Controls
onDragStart={() => setIsDragging(true)} onDragStart={() => setIsDragging(true)}
onInit={onInit} onInit={onInit}
onCameraChange={(camera) => { onCameraChange={onCameraChange}
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,
})
}
})
}}
/> />
<PerspectiveCamera makeDefault up={[0, 0, 1]}> <PerspectiveCamera makeDefault up={[0, 0, 1]}>
<pointLight <pointLight
@@ -232,17 +224,16 @@ const IdeViewer = ({ Loading }) => {
intensity={jscadLightIntensity} intensity={jscadLightIntensity}
/> />
</PerspectiveCamera> </PerspectiveCamera>
<ambientLight intensity={0.3} /> <ambientLight intensity={2 * jscadLightIntensity} />
<Environment preset="warehouse" />
<pointLight <pointLight
position={[-1000, -1000, -1000]} position={[-1000, -1000, -1000]}
color="#5555FF" color="#5555FF"
intensity={0.5} intensity={1 * jscadLightIntensity}
/> />
<pointLight <pointLight
position={[-1000, 0, 1000]} position={[-1000, 0, 1000]}
color="#5555FF" color="#5555FF"
intensity={0.5} intensity={1 * jscadLightIntensity}
/> />
<gridHelper <gridHelper
args={[200, 20, 0xff5555, 0x555555]} args={[200, 20, 0xff5555, 0x555555]}
@@ -250,13 +241,15 @@ const IdeViewer = ({ Loading }) => {
material-transparent material-transparent
rotation-x={Math.PI / 2} rotation-x={Math.PI / 2}
/> />
{!isMinimal && (
<GizmoHelper alignment={'top-left'} margin={[80, 80]}> <GizmoHelper alignment={'top-left'} margin={[80, 80]}>
<GizmoViewport <GizmoViewport
axisColors={['red', 'green', 'blue']} axisColors={['red', 'green', 'blue']}
labelColor="black" labelColor="black"
/> />
</GizmoHelper> </GizmoHelper>
{state.objectData?.type === 'png' && ( )}
{dataType === 'png' && (
<> <>
<Sphere position={[0, 0, 0]} color={pink400} /> <Sphere position={[0, 0, 0]} color={pink400} />
<Box position={[0, 50, 0]} size={[1, 100, 1]} color={indigo900} /> <Box position={[0, 50, 0]} size={[1, 100, 1]} color={indigo900} />
@@ -264,8 +257,10 @@ const IdeViewer = ({ Loading }) => {
<Box position={[50, 0, 0]} size={[100, 1, 1]} color={pink400} /> <Box position={[50, 0, 0]} size={[100, 1, 1]} color={pink400} />
</> </>
)} )}
{state.objectData?.type === 'geometry' && state.objectData?.data && ( {dataType === 'geometry' && artifact && (
<Asset geometry={state.objectData?.data} /> <Suspense fallback={null}>
<Asset geometry={artifact} />
</Suspense>
)} )}
{PrimitiveArray && {PrimitiveArray &&
PrimitiveArray.map((mesh, index) => ( PrimitiveArray.map((mesh, index) => (
@@ -273,10 +268,55 @@ const IdeViewer = ({ Loading }) => {
))} ))}
</Canvas> </Canvas>
</div> </div>
<DelayedPingAnimation isLoading={state.isLoading} /> <DelayedPingAnimation isLoading={isLoading} />
<Customizer /> {!isMinimal && <Customizer />}
</div> </div>
) )
} }
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 (
<PureIdeViewer
dataType={dataType}
artifact={artifact}
onInit={onInit}
onCameraChange={onCameraChange}
isLoading={state.isLoading}
/>
)
}
export default IdeViewer export default IdeViewer

View File

@@ -10,9 +10,6 @@ export const useRender = () => {
requestRender({ requestRender({
state, state,
dispatch, dispatch,
code: state.code,
viewerSize: state.viewerSize,
camera: state.camera,
parameters: disableParams ? {} : state.currentParameters, parameters: disableParams ? {} : state.currentParameters,
}) })
}) })

View File

@@ -1,15 +1,11 @@
import { lazy, Suspense } from 'react' import IdeViewer from 'src/components/IdeViewer/IdeViewer'
const IdeViewer = lazy(() => import('src/components/IdeViewer/IdeViewer'))
import { use3dViewerResize } from 'src/helpers/hooks/use3dViewerResize' import { use3dViewerResize } from 'src/helpers/hooks/use3dViewerResize'
import { BigLoadingPing } from 'src/components/IdeContainer/IdeContainer'
const ProfileViewer = () => { const ProfileViewer = () => {
const { viewerDomRef } = use3dViewerResize() const { viewerDomRef } = use3dViewerResize()
return ( return (
<div className="h-full" ref={viewerDomRef}> <div className="h-full" ref={viewerDomRef}>
<Suspense fallback={BigLoadingPing}> <IdeViewer />
<IdeViewer Loading={BigLoadingPing} />
</Suspense>
</div> </div>
) )
} }

View File

@@ -37,7 +37,7 @@ export const Failure = ({ error }: CellFailureProps) => (
export const Success = ({ export const Success = ({
userProject, userProject,
variables: { image64 }, variables: { image64, LiveProjectViewer },
}: CellSuccessProps<FindSocialCardQuery>) => { }: CellSuccessProps<FindSocialCardQuery>) => {
const image = userProject?.Project?.mainImage const image = userProject?.Project?.mainImage
const gravatar = userProject?.image const gravatar = userProject?.image
@@ -47,7 +47,7 @@ export const Success = ({
: userProject?.Project?.description || '' : userProject?.Project?.description || ''
return ( return (
<div <div
className="grid h-screen bg-ch-gray-800 text-ch-gray-300" className="grid h-full bg-ch-gray-800 text-ch-gray-300"
id="social-card-loaded" id="social-card-loaded"
style={{ gridTemplateRows: ' 555fr 18fr' }} style={{ gridTemplateRows: ' 555fr 18fr' }}
> >
@@ -96,6 +96,18 @@ export const Success = ({
/> />
)} )}
</div> </div>
<div
className={`absolute inset-0 flex items-center justify-center ${
image64 && 'opacity-0'
}`}
>
{LiveProjectViewer && (
<div className="w-full h-full" id="social-card-canvas">
<LiveProjectViewer />
</div>
)}
</div>
</div> </div>
</div> </div>
<div <div

View File

@@ -26,7 +26,7 @@ export const canvasToBlob = async (
(blob) => { (blob) => {
resolve(blob) resolve(blob)
}, },
'image/jpeg', 'image/png',
0.75 0.75
) )
}) })

View File

@@ -23,10 +23,7 @@ export const use3dViewerResize = () => {
requestRender({ requestRender({
state, state,
dispatch, dispatch,
code: state.code,
viewerSize: { width, height }, viewerSize: { width, height },
camera: state.camera,
parameters: state.currentParameters,
}) })
} }
}) })

View File

@@ -25,19 +25,33 @@ interface XYZ {
z: number z: number
} }
interface EditorModel { interface CodeTab {
type: 'code' | 'guide' type: 'code'
label: string label: string
content?: 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 { export interface State {
ideType: 'INIT' | CadPackageType ideType: 'INIT' | CadPackageType
viewerContext: 'ide' | 'viewer' viewerContext: 'ide' | 'viewer'
ideGuide?: string ideGuide?: string
consoleMessages: { type: 'message' | 'error'; message: string; time: Date }[] consoleMessages: { type: 'message' | 'error'; message: string; time: Date }[]
code: string code: string
models: EditorModel[] editorTabs: EditorTab[]
currentModel: number currentModel: number
objectData: { objectData: {
type: 'INIT' | ArtifactTypes type: 'INIT' | ArtifactTypes
@@ -78,7 +92,7 @@ export const initialState: State = {
{ type: 'message', message: 'Initialising', time: new Date() }, { type: 'message', message: 'Initialising', time: new Date() },
], ],
code, code,
models: [{ type: 'code', label: 'Code' }], editorTabs: [{ type: 'code', label: 'Code' }],
currentModel: 0, currentModel: 0,
objectData: { objectData: {
type: 'INIT', type: 'INIT',
@@ -240,14 +254,14 @@ const reducer = (state: State, { type, payload }): State => {
case 'addEditorModel': case 'addEditorModel':
return { return {
...state, ...state,
models: [...state.models, payload], editorTabs: [...state.editorTabs, payload],
} }
case 'removeEditorModel': case 'removeEditorModel':
return { return {
...state, ...state,
models: [ editorTabs: [
...state.models.slice(0, payload), ...state.editorTabs.slice(0, payload),
...state.models.slice(payload + 1), ...state.editorTabs.slice(payload + 1),
], ],
currentModel: payload === 0 ? 0 : payload - 1, currentModel: payload === 0 ? 0 : payload - 1,
} }
@@ -284,43 +298,57 @@ export const useIdeState = (): [State, (actionOrThunk: any) => any] => {
return [state, thunkDispatch] return [state, thunkDispatch]
} }
interface RequestRenderArgs { interface RequestRenderArgsStateless {
state: State state: State
dispatch: any camera?: State['camera']
parameters: any viewerSize?: State['viewerSize']
code: State['code']
camera: State['camera']
viewerSize: State['viewerSize']
quality?: State['objectData']['quality'] quality?: State['objectData']['quality']
specialCadProcess?: string specialCadProcess?: string
parameters?: { [key: string]: any }
} }
export const requestRender = ({ export const requestRenderStateless = ({
state, state,
dispatch,
code,
camera, camera,
viewerSize, viewerSize,
quality = 'low', quality = 'low',
specialCadProcess = null, specialCadProcess = null,
parameters, parameters,
}: RequestRenderArgs) => { }: RequestRenderArgsStateless): null | Promise<any> => {
if ( if (
!(
state.ideType !== 'INIT' && state.ideType !== 'INIT' &&
(!state.isLoading || state.objectData?.type === 'INIT') (!state.isLoading || state.objectData?.type === 'INIT')
)
) { ) {
return null
}
const renderFn = specialCadProcess const renderFn = specialCadProcess
? cadPackages[state.ideType][specialCadProcess] ? cadPackages[state.ideType][specialCadProcess]
: cadPackages[state.ideType].render : cadPackages[state.ideType].render
return renderFn({ return renderFn({
code, code: state.code,
settings: { settings: {
parameters: state.isCustomizerOpen ? parameters : {}, parameters: state.isCustomizerOpen
camera, ? parameters || state.currentParameters
viewerSize, : {},
camera: camera || state.camera,
viewerSize: viewerSize || state.viewerSize,
quality, quality,
}, },
}) })
}
interface RequestRenderArgs extends RequestRenderArgsStateless {
dispatch: any
}
export const requestRender = ({ dispatch, ...rest }: RequestRenderArgs) => {
const renderPromise = requestRenderStateless(rest)
if (!renderPromise) {
return
}
renderPromise
.then( .then(
({ ({
objectData, objectData,
@@ -350,5 +378,4 @@ export const requestRender = ({
} }
) )
.catch(() => dispatch({ type: 'resetLoading' })) // TODO should probably display something to the user here .catch(() => dispatch({ type: 'resetLoading' })) // TODO should probably display something to the user here
}
} }