Overhaul social card (again) #541

Merged
Irev-Dev merged 1 commits from kurt/overhaul-social-card into main 2021-10-11 21:09:56 +02:00
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
/>
)
}
function TabContent() {
return (
<div className="bg-ch-gray-800 h-full overflow-y-auto px-8 pb-16">
<IsolatedCanvas
size={{ width: 500, height: 375 }}
uploadKey="mainImage64"
RenderComponent={ThumbnailViewer}
/>
<IsolatedCanvas
canvasRatio={2}
size={captureSize}
uploadKey="socialCard64"
RenderComponent={SocialCardLiveViewer}
/> />
<Popover
id={'capture-popover'}
open={whichPopup === 'capture'}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={anchorOrigin}
transformOrigin={transformOrigin}
className="material-ui-overrides transform translate-y-4"
>
<div className="text-sm p-4 text-gray-500">
{!captureState ? (
'Loading...'
) : (
<div className="">
<div className="text-lg">Thumbnail</div>
<div
className="rounded"
style={{ width: 'fit-content', overflow: 'hidden' }}
>
<img src={captureState.imageObjectURL} className="w-32" />
</div> </div>
</div> )
)} }
<div className="text-lg mt-4">Social Media Card</div>
<div className="rounded-lg shadow-md overflow-hidden"> 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 <div
className="transform scale-50 origin-top-left" className="transform scale-50 origin-top-left"
style={{ width: '600px', height: '315px' }} style={{ width: '600px', height: '315px' }}
> >
<div style={{ width: '1200px', height: '630px' }} ref={ref}> <div style={{ width: '1200px', height: '630px' }} ref={forwardRef}>
<SocialCardCell <SocialCardCell
userName={userName} userName={project.user.userName}
projectTitle={projectTitle} projectTitle={project.title}
image64={captureState.image64} image64={partSnapShot64}
LiveProjectViewer={() => children}
/> />
</div> </div>
</div> </div>
</div> </div>
<div className="mt-4 text-indigo-800"> </div>
{captureState.currImage && !captureState.updated ? ( <button className="bg-gray-200 p-2 rounded-sm" onClick={onUpload}>
<Button save image
iconName="refresh" </button>
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({ function ThumbnailViewer({ forwardRef, onUpload, children, partSnapShot64 }) {
...captureState, return (
currImage: cloudinaryImg, <>
updated: true, <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"
>
franknoirot commented 2021-10-10 23:04:25 +02:00 (Migrated from github.com)
Review

@Irev-Dev could this component be used for a /view endpoint?

@Irev-Dev could this component be used for a /view endpoint?
Irev-Dev commented 2021-10-11 21:07:18 +02:00 (Migrated from github.com)
Review

Yeah I suppose so 👍

Yeah I suppose so 👍
<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()
Irev-Dev commented 2021-10-09 20:37:42 +02:00 (Migrated from github.com)
Review

This might have been more effort than what it was worth. The social image need to be captured at a rather large size 1200x630, so the aim here is to scale ti down by half so that for the viewer so that it's not dominating the UI.
However the three canvas can gets confused and ends up halving again, which is why you'll see the canvasRatio sprinkled around to try and fix things.

This might have been more effort than what it was worth. The social image need to be captured at a rather large size 1200x630, so the aim here is to scale ti down by half so that for the viewer so that it's not dominating the UI. However the three canvas can gets confused and ends up halving again, which is why you'll see the `canvasRatio` sprinkled around to try and fix things.
})
toast.promise(uploadPromise, {
loading: 'Saving Image',
success: (finalImg: string) => (
<div className="flex flex-col items-center">
<b className="py-2">Image saved!</b>
Irev-Dev commented 2021-10-09 20:41:29 +02:00 (Migrated from github.com)
Review

even though we're using the canvas directly (passed in through LiveProjectViewer), this doesn't work when capturing with import { toJpeg } from 'html-to-image' and so the canvas need to be captured, than the image inserted over the canvas via partSnapShot64 so that we can than use toJpeg

even though we're using the canvas directly (passed in through `LiveProjectViewer`), this doesn't work when capturing with `import { toJpeg } from 'html-to-image'` and so the canvas need to be captured, than the image inserted over the canvas via `partSnapShot64` so that we can than use `toJpeg`
<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`,
}} }}
> >
Update Project Images <CaptureButtonViewer
</Button> onInit={onInit}
) : ( onScadImage={scadSnapShot64Setter}
<div className="flex justify-center mb-4"> canvasRatio={canvasRatio}
<Svg />
name="checkmark"
className="mr-2 w-6 text-indigo-600"
/>{' '}
Project Images Updated
</div> </div>
)} </RenderComponent>
</div>
</div>
</Popover>
</div>
)}
</div> </div>
) )
} }
export default CaptureButton 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'
}`} }`}
Irev-Dev commented 2021-10-09 20:25:56 +02:00 (Migrated from github.com)
Review

Changes the IDE view so that it doesn't rely on state/context so that multiple can be used together, it can than be wrapped in a component that provides it with state for the main IDE case.

Changes the IDE view so that it doesn't rely on state/context so that multiple can be used together, it can than be wrapped in a component that provides it with state for the main IDE case.
franknoirot commented 2021-10-10 23:14:15 +02:00 (Migrated from github.com)
Review

This is a great decoupling move!

This is a great decoupling move!
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
Irev-Dev commented 2021-10-09 20:29:42 +02:00 (Migrated from github.com)
Review

Added a tab type that will render a component.

Added a tab type that will render a component.
}
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,
@@ -351,4 +379,3 @@ 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
} }
}