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' const onCameraChange = (camera) => {
let imgBlob const renderPromise =
let image64 state.ideType === 'openscad' &&
if (!isOpenScadImage) { requestRenderStateless({
imgBlob = canvasToBlob(threeInstance, { width: 500, height: 375 }) state,
image64 = blobTo64( camera,
await canvasToBlob(threeInstance, { width: 500, height: 522 }) viewerSize: {
) width: threeInstance.current.size.width * canvasRatio,
} else { height: threeInstance.current.size.height * canvasRatio,
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: <b>Image/s saved!</b>,
error: <b>Problem saving.</b>,
}) })
const [{ data }] = await promise if (!renderPromise) {
return data?.updateProjectImages?.mainImage return
}
// 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)
} }
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 ( 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"
>
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`,
}}
>
<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'
}`} }`}
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}
/> />
<GizmoHelper alignment={'top-left'} margin={[80, 80]}> {!isMinimal && (
<GizmoViewport <GizmoHelper alignment={'top-left'} margin={[80, 80]}>
axisColors={['red', 'green', 'blue']} <GizmoViewport
labelColor="black" axisColors={['red', 'green', 'blue']}
/> labelColor="black"
</GizmoHelper> />
{state.objectData?.type === 'png' && ( </GizmoHelper>
)}
{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,71 +298,84 @@ 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.isLoading || state.objectData?.type === 'INIT') state.ideType !== 'INIT' &&
(!state.isLoading || state.objectData?.type === 'INIT')
)
) { ) {
const renderFn = specialCadProcess return null
? 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
} }
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: {
Irev-Dev commented 2021-10-09 20:32:28 +02:00 (Migrated from github.com)
Review

Similar to PureIdeViewer, I needed a render function that didn't automatically update state so the the PureIdeViewer's could update they're model (mostly important for OpenSCAD since it needs a new image with perspective change)
It can than be wrapped to automatically update state so functionally is the same for the normal IDE viewer.

Similar to PureIdeViewer, I needed a render function that didn't automatically update state so the the PureIdeViewer's could update they're model (mostly important for OpenSCAD since it needs a new image with perspective change) It can than be wrapped to automatically update state so functionally is the same for the normal IDE viewer.
objectData,
message,
lastRunCode: code,
customizerParams,
currentParameters,
},
})
return objectData
}
}
)
.catch(() => dispatch({ type: 'resetLoading' })) // TODO should probably display something to the user here
} }