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:
@@ -1,14 +1,15 @@
|
||||
import { useState } from 'react'
|
||||
import { toast } from '@redwoodjs/web/toast'
|
||||
import Popover from '@material-ui/core/Popover'
|
||||
import Svg from 'src/components/Svg/Svg'
|
||||
import Button from 'src/components/Button/Button'
|
||||
import { toJpeg } from 'html-to-image'
|
||||
|
||||
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
|
||||
import { canvasToBlob, blobTo64 } from 'src/helpers/canvasToBlob'
|
||||
import { useUpdateProjectImages } from 'src/helpers/hooks/useUpdateProjectImages'
|
||||
|
||||
import { requestRenderStateless } from 'src/helpers/hooks/useIdeState'
|
||||
import { PureIdeViewer } from 'src/components/IdeViewer/IdeViewer'
|
||||
import SocialCardCell from 'src/components/SocialCardCell/SocialCardCell'
|
||||
import { toJpeg } from 'html-to-image'
|
||||
|
||||
export const captureSize = { width: 500, height: 522 }
|
||||
|
||||
const anchorOrigin = {
|
||||
vertical: 'bottom',
|
||||
@@ -19,175 +20,247 @@ const transformOrigin = {
|
||||
horizontal: 'center',
|
||||
}
|
||||
|
||||
const CaptureButton = ({
|
||||
canEdit,
|
||||
TheButton,
|
||||
shouldUpdateImage,
|
||||
projectTitle,
|
||||
userName,
|
||||
export const CaptureButtonViewer = ({
|
||||
onInit,
|
||||
onScadImage,
|
||||
canvasRatio = 1,
|
||||
}: {
|
||||
onInit: (a: any) => void
|
||||
onScadImage: (a: any) => void
|
||||
canvasRatio: number
|
||||
}) => {
|
||||
const [captureState, setCaptureState] = useState<any>({})
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const [whichPopup, setWhichPopup] = useState(null)
|
||||
const { state, project } = useIdeContext()
|
||||
const ref = React.useRef<HTMLDivElement>(null)
|
||||
const { updateProjectImages } = useUpdateProjectImages({})
|
||||
|
||||
const onCapture = async () => {
|
||||
const threeInstance = state.threeInstance
|
||||
const isOpenScadImage = state?.objectData?.type === 'png'
|
||||
let imgBlob
|
||||
let image64
|
||||
if (!isOpenScadImage) {
|
||||
imgBlob = canvasToBlob(threeInstance, { width: 500, height: 375 })
|
||||
image64 = blobTo64(
|
||||
await canvasToBlob(threeInstance, { width: 500, height: 522 })
|
||||
)
|
||||
} else {
|
||||
imgBlob = state.objectData.data
|
||||
image64 = blobTo64(state.objectData.data)
|
||||
}
|
||||
const config = {
|
||||
image: await imgBlob,
|
||||
currImage: project?.mainImage,
|
||||
imageObjectURL: window.URL.createObjectURL(await imgBlob),
|
||||
callback: uploadAndUpdateImage,
|
||||
cloudinaryImgURL: '',
|
||||
updated: false,
|
||||
image64: await image64,
|
||||
}
|
||||
setCaptureState(config)
|
||||
|
||||
async function uploadAndUpdateImage() {
|
||||
const upload = async () => {
|
||||
const socialCard64 = toJpeg(ref.current, {
|
||||
cacheBust: true,
|
||||
quality: 0.7,
|
||||
})
|
||||
|
||||
// uploading in two separate mutations because of the 100kb limit of the lambda functions
|
||||
const imageUploadPromise1 = updateProjectImages({
|
||||
variables: {
|
||||
id: project?.id,
|
||||
mainImage64: await config.image64,
|
||||
},
|
||||
})
|
||||
const imageUploadPromise2 = updateProjectImages({
|
||||
variables: {
|
||||
id: project?.id,
|
||||
socialCard64: await socialCard64,
|
||||
},
|
||||
})
|
||||
return Promise.all([imageUploadPromise2, imageUploadPromise1])
|
||||
}
|
||||
const promise = upload()
|
||||
toast.promise(promise, {
|
||||
loading: 'Saving Image/s',
|
||||
success: <b>Image/s saved!</b>,
|
||||
error: <b>Problem saving.</b>,
|
||||
const { state } = useIdeContext()
|
||||
const threeInstance = React.useRef(null)
|
||||
const [dataType, dataTypeSetter] = useState(state?.objectData?.type)
|
||||
const [artifact, artifactSetter] = useState(state?.objectData?.data)
|
||||
const [isLoading, isLoadingSetter] = useState(false)
|
||||
const getThreeInstance = (_threeInstance) => {
|
||||
threeInstance.current = _threeInstance
|
||||
onInit(_threeInstance)
|
||||
}
|
||||
const onCameraChange = (camera) => {
|
||||
const renderPromise =
|
||||
state.ideType === 'openscad' &&
|
||||
requestRenderStateless({
|
||||
state,
|
||||
camera,
|
||||
viewerSize: {
|
||||
width: threeInstance.current.size.width * canvasRatio,
|
||||
height: threeInstance.current.size.height * canvasRatio,
|
||||
},
|
||||
})
|
||||
const [{ data }] = await promise
|
||||
return data?.updateProjectImages?.mainImage
|
||||
}
|
||||
|
||||
// if there isn't a screenshot saved yet, just go ahead and save right away
|
||||
if (shouldUpdateImage) {
|
||||
config.cloudinaryImgURL = await uploadAndUpdateImage()
|
||||
config.updated = true
|
||||
setCaptureState(config)
|
||||
if (!renderPromise) {
|
||||
return
|
||||
}
|
||||
isLoadingSetter(true)
|
||||
renderPromise.then(async ({ objectData }) => {
|
||||
isLoadingSetter(false)
|
||||
dataTypeSetter(objectData?.type)
|
||||
artifactSetter(objectData?.data)
|
||||
if (objectData?.type === 'png') {
|
||||
onScadImage(await blobTo64(objectData?.data))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleClick = ({ event, whichPopup }) => {
|
||||
setAnchorEl(event.currentTarget)
|
||||
setWhichPopup(whichPopup)
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null)
|
||||
setWhichPopup(null)
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
{canEdit && (
|
||||
<div>
|
||||
<TheButton
|
||||
onClick={async (event) => {
|
||||
handleClick({ event, whichPopup: 'capture' })
|
||||
onCapture()
|
||||
}}
|
||||
/>
|
||||
<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 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>
|
||||
)}
|
||||
<PureIdeViewer
|
||||
scadRatio={canvasRatio}
|
||||
dataType={dataType}
|
||||
artifact={artifact}
|
||||
onInit={getThreeInstance}
|
||||
onCameraChange={onCameraChange}
|
||||
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}
|
||||
/>
|
||||
</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,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ const EditorMenu = () => {
|
||||
})
|
||||
thunkDispatch({
|
||||
type: 'switchEditorModel',
|
||||
payload: state.models.length,
|
||||
payload: state.editorTabs.length,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -74,12 +74,8 @@ export const makeStlDownloadHandler =
|
||||
requestRender({
|
||||
state,
|
||||
dispatch,
|
||||
code: state.code,
|
||||
viewerSize: state.viewerSize,
|
||||
camera: state.camera,
|
||||
quality: 'high',
|
||||
specialCadProcess,
|
||||
parameters: state.currentParameters,
|
||||
}).then(
|
||||
(result) => result && saveFile(makeStlBlobFromGeo(result.data))
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ import PanelToolbar from 'src/components/PanelToolbar/PanelToolbar'
|
||||
import { use3dViewerResize } from 'src/helpers/hooks/use3dViewerResize'
|
||||
|
||||
const IdeEditor = lazy(() => import('src/components/IdeEditor/IdeEditor'))
|
||||
const IdeViewer = lazy(() => import('src/components/IdeViewer/IdeViewer'))
|
||||
import IdeViewer from 'src/components/IdeViewer/IdeViewer'
|
||||
|
||||
const SmallLoadingPing = (
|
||||
<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} />
|
||||
</Suspense>
|
||||
),
|
||||
Viewer: (
|
||||
<Suspense fallback={BigLoadingPing}>
|
||||
<IdeViewer Loading={BigLoadingPing} />
|
||||
</Suspense>
|
||||
),
|
||||
Viewer: <IdeViewer />,
|
||||
Console: <IdeConsole />,
|
||||
}
|
||||
|
||||
|
||||
@@ -60,24 +60,21 @@ const IdeEditor = ({ Loading }) => {
|
||||
requestRender({
|
||||
state,
|
||||
dispatch,
|
||||
code: state.code,
|
||||
viewerSize: state.viewerSize,
|
||||
camera: state.camera,
|
||||
parameters: state.currentParameters,
|
||||
})
|
||||
})
|
||||
localStorage.setItem(makeCodeStoreKey(state.ideType), state.code)
|
||||
}
|
||||
}
|
||||
const currentTab = state.editorTabs[state.currentModel]
|
||||
|
||||
return (
|
||||
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
|
||||
className="h-full"
|
||||
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">
|
||||
{state.models.map((model, i) => (
|
||||
{state.editorTabs.map((model, i) => (
|
||||
<label
|
||||
key={model.type + '-' + i}
|
||||
className={
|
||||
@@ -117,7 +114,7 @@ const IdeEditor = ({ Loading }) => {
|
||||
))}
|
||||
</fieldset>
|
||||
)}
|
||||
{state.models[state.currentModel].type === 'code' ? (
|
||||
{currentTab.type === 'code' && (
|
||||
<Editor
|
||||
defaultValue={state.code}
|
||||
value={state.code}
|
||||
@@ -128,11 +125,13 @@ const IdeEditor = ({ Loading }) => {
|
||||
language={ideTypeToLanguageMap[state.ideType] || 'cpp'}
|
||||
onChange={handleCodeChange}
|
||||
/>
|
||||
) : (
|
||||
)}
|
||||
{currentTab.type === 'guide' && (
|
||||
<div className="bg-ch-gray-800 h-full">
|
||||
<EditorGuide content={state.models[state.currentModel].content} />
|
||||
<EditorGuide content={currentTab.content} />
|
||||
</div>
|
||||
)}
|
||||
{currentTab.type === 'component' && <currentTab.Component />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import CaptureButton from 'src/components/CaptureButton/CaptureButton'
|
||||
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
|
||||
import Gravatar from 'src/components/Gravatar/Gravatar'
|
||||
import EditableProjectTitle from 'src/components/EditableProjecTitle/EditableProjecTitle'
|
||||
import SocialCardModal from 'src/components/SocialCardModal/SocialCardModal'
|
||||
|
||||
const FORK_PROJECT_MUTATION = gql`
|
||||
mutation ForkProjectMutation($input: ForkProjectInput!) {
|
||||
@@ -121,10 +122,6 @@ export default function IdeHeader({
|
||||
<div className="grid grid-flow-col-dense gap-4 items-center mr-4">
|
||||
{canEdit && !isProfile && (
|
||||
<CaptureButton
|
||||
canEdit={canEdit}
|
||||
projectTitle={project?.title}
|
||||
userName={project?.user?.userName}
|
||||
shouldUpdateImage={!project?.mainImage}
|
||||
TheButton={({ onClick }) => (
|
||||
<TopButton
|
||||
onClick={onClick}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { useIdeContext } from 'src/helpers/hooks/useIdeContext'
|
||||
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 {
|
||||
PerspectiveCamera,
|
||||
GizmoHelper,
|
||||
GizmoViewport,
|
||||
OrbitControls,
|
||||
Environment,
|
||||
useTexture,
|
||||
} from '@react-three/drei'
|
||||
import { useEdgeSplit } from 'src/helpers/hooks/useEdgeSplit'
|
||||
@@ -16,6 +15,7 @@ import { requestRender } from 'src/helpers/hooks/useIdeState'
|
||||
import texture from './dullFrontLitMetal.png'
|
||||
import Customizer from 'src/components/Customizer/Customizer'
|
||||
import DelayedPingAnimation from 'src/components/DelayedPingAnimation/DelayedPingAnimation'
|
||||
import type { ArtifactTypes } from 'src/helpers/cadPackages/common'
|
||||
|
||||
const thresholdAngle = 12
|
||||
|
||||
@@ -35,13 +35,13 @@ function Asset({ geometry: incomingGeo }) {
|
||||
<group dispose={null}>
|
||||
<mesh ref={mesh} scale={[1, 1, 1]} geometry={incomingGeo}>
|
||||
<meshPhysicalMaterial
|
||||
envMapIntensity={2}
|
||||
envMapIntensity={0.1}
|
||||
color="#F472B6"
|
||||
map={colorMap}
|
||||
clearcoat={0.1}
|
||||
clearcoatRoughness={0.2}
|
||||
roughness={10}
|
||||
metalness={0.9}
|
||||
metalness={0.7}
|
||||
smoothShading
|
||||
/>
|
||||
</mesh>
|
||||
@@ -148,42 +148,53 @@ function Sphere(props) {
|
||||
)
|
||||
}
|
||||
|
||||
const IdeViewer = ({ Loading }) => {
|
||||
const { state, thunkDispatch } = useIdeContext()
|
||||
export function PureIdeViewer({
|
||||
dataType,
|
||||
artifact,
|
||||
onInit,
|
||||
onCameraChange,
|
||||
isLoading,
|
||||
isMinimal = false,
|
||||
scadRatio = 1,
|
||||
}: {
|
||||
dataType: 'INIT' | ArtifactTypes
|
||||
artifact: any
|
||||
isLoading: boolean
|
||||
onInit: Function
|
||||
onCameraChange: Function
|
||||
isMinimal?: boolean
|
||||
scadRatio?: number
|
||||
}) {
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [image, setImage] = useState()
|
||||
|
||||
const onInit = (threeInstance) => {
|
||||
thunkDispatch({ type: 'setThreeInstance', payload: threeInstance })
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setImage(state.objectData?.type === 'png' && state.objectData?.data)
|
||||
setImage(dataType === 'png' && artifact)
|
||||
setIsDragging(false)
|
||||
}, [state.objectData?.type, state.objectData?.data])
|
||||
}, [dataType, artifact])
|
||||
const PrimitiveArray = React.useMemo(
|
||||
() =>
|
||||
state.objectData?.type === 'primitive-array' && state.objectData?.data,
|
||||
[state.objectData?.type, state.objectData?.data]
|
||||
dataType === 'primitive-array' && artifact?.map((mesh) => mesh.clone()),
|
||||
[dataType, artifact]
|
||||
)
|
||||
|
||||
// the following are tailwind colors in hex, can't use these classes to color three.js meshes.
|
||||
const pink400 = '#F472B6'
|
||||
const indigo300 = '#A5B4FC'
|
||||
const indigo900 = '#312E81'
|
||||
const jscadLightIntensity =
|
||||
state.objectData?.type === 'geometry' &&
|
||||
state.objectData?.data &&
|
||||
state.objectData?.data.length
|
||||
? 0.5
|
||||
: 1.2
|
||||
const jscadLightIntensity = PrimitiveArray ? 0.5 : 1.1
|
||||
return (
|
||||
<div className="relative h-full bg-ch-gray-800">
|
||||
<div className="relative h-full bg-ch-gray-800 cursor-grab">
|
||||
{image && (
|
||||
<div
|
||||
className={`absolute inset-0 transition-opacity duration-500 ${
|
||||
isDragging ? 'opacity-25' : 'opacity-100'
|
||||
}`}
|
||||
style={{
|
||||
transform: `translate(${
|
||||
scadRatio !== 1 ? '-250px, -261px' : '0px, 0px'
|
||||
})`,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt="code-cad preview"
|
||||
@@ -195,7 +206,7 @@ const IdeViewer = ({ Loading }) => {
|
||||
)}
|
||||
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
|
||||
className={`opacity-0 absolute inset-0 transition-opacity duration-500 ${
|
||||
!(isDragging || state.objectData?.type !== 'png')
|
||||
!(isDragging || dataType !== 'png')
|
||||
? 'hover:opacity-50'
|
||||
: 'opacity-100'
|
||||
}`}
|
||||
@@ -205,26 +216,7 @@ const IdeViewer = ({ Loading }) => {
|
||||
<Controls
|
||||
onDragStart={() => setIsDragging(true)}
|
||||
onInit={onInit}
|
||||
onCameraChange={(camera) => {
|
||||
thunkDispatch({
|
||||
type: 'updateCamera',
|
||||
payload: { camera },
|
||||
})
|
||||
thunkDispatch((dispatch, getState) => {
|
||||
const state = getState()
|
||||
if (['png', 'INIT'].includes(state.objectData?.type)) {
|
||||
dispatch({ type: 'setLoading' })
|
||||
requestRender({
|
||||
state,
|
||||
dispatch,
|
||||
code: state.code,
|
||||
viewerSize: state.viewerSize,
|
||||
camera,
|
||||
parameters: state.currentParameters,
|
||||
})
|
||||
}
|
||||
})
|
||||
}}
|
||||
onCameraChange={onCameraChange}
|
||||
/>
|
||||
<PerspectiveCamera makeDefault up={[0, 0, 1]}>
|
||||
<pointLight
|
||||
@@ -232,17 +224,16 @@ const IdeViewer = ({ Loading }) => {
|
||||
intensity={jscadLightIntensity}
|
||||
/>
|
||||
</PerspectiveCamera>
|
||||
<ambientLight intensity={0.3} />
|
||||
<Environment preset="warehouse" />
|
||||
<ambientLight intensity={2 * jscadLightIntensity} />
|
||||
<pointLight
|
||||
position={[-1000, -1000, -1000]}
|
||||
color="#5555FF"
|
||||
intensity={0.5}
|
||||
intensity={1 * jscadLightIntensity}
|
||||
/>
|
||||
<pointLight
|
||||
position={[-1000, 0, 1000]}
|
||||
color="#5555FF"
|
||||
intensity={0.5}
|
||||
intensity={1 * jscadLightIntensity}
|
||||
/>
|
||||
<gridHelper
|
||||
args={[200, 20, 0xff5555, 0x555555]}
|
||||
@@ -250,13 +241,15 @@ const IdeViewer = ({ Loading }) => {
|
||||
material-transparent
|
||||
rotation-x={Math.PI / 2}
|
||||
/>
|
||||
<GizmoHelper alignment={'top-left'} margin={[80, 80]}>
|
||||
<GizmoViewport
|
||||
axisColors={['red', 'green', 'blue']}
|
||||
labelColor="black"
|
||||
/>
|
||||
</GizmoHelper>
|
||||
{state.objectData?.type === 'png' && (
|
||||
{!isMinimal && (
|
||||
<GizmoHelper alignment={'top-left'} margin={[80, 80]}>
|
||||
<GizmoViewport
|
||||
axisColors={['red', 'green', 'blue']}
|
||||
labelColor="black"
|
||||
/>
|
||||
</GizmoHelper>
|
||||
)}
|
||||
{dataType === 'png' && (
|
||||
<>
|
||||
<Sphere position={[0, 0, 0]} color={pink400} />
|
||||
<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} />
|
||||
</>
|
||||
)}
|
||||
{state.objectData?.type === 'geometry' && state.objectData?.data && (
|
||||
<Asset geometry={state.objectData?.data} />
|
||||
{dataType === 'geometry' && artifact && (
|
||||
<Suspense fallback={null}>
|
||||
<Asset geometry={artifact} />
|
||||
</Suspense>
|
||||
)}
|
||||
{PrimitiveArray &&
|
||||
PrimitiveArray.map((mesh, index) => (
|
||||
@@ -273,10 +268,55 @@ const IdeViewer = ({ Loading }) => {
|
||||
))}
|
||||
</Canvas>
|
||||
</div>
|
||||
<DelayedPingAnimation isLoading={state.isLoading} />
|
||||
<Customizer />
|
||||
<DelayedPingAnimation isLoading={isLoading} />
|
||||
{!isMinimal && <Customizer />}
|
||||
</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
|
||||
|
||||
@@ -10,9 +10,6 @@ export const useRender = () => {
|
||||
requestRender({
|
||||
state,
|
||||
dispatch,
|
||||
code: state.code,
|
||||
viewerSize: state.viewerSize,
|
||||
camera: state.camera,
|
||||
parameters: disableParams ? {} : state.currentParameters,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import { lazy, Suspense } from 'react'
|
||||
const IdeViewer = lazy(() => import('src/components/IdeViewer/IdeViewer'))
|
||||
import IdeViewer from 'src/components/IdeViewer/IdeViewer'
|
||||
import { use3dViewerResize } from 'src/helpers/hooks/use3dViewerResize'
|
||||
import { BigLoadingPing } from 'src/components/IdeContainer/IdeContainer'
|
||||
|
||||
const ProfileViewer = () => {
|
||||
const { viewerDomRef } = use3dViewerResize()
|
||||
return (
|
||||
<div className="h-full" ref={viewerDomRef}>
|
||||
<Suspense fallback={BigLoadingPing}>
|
||||
<IdeViewer Loading={BigLoadingPing} />
|
||||
</Suspense>
|
||||
<IdeViewer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ export const Failure = ({ error }: CellFailureProps) => (
|
||||
|
||||
export const Success = ({
|
||||
userProject,
|
||||
variables: { image64 },
|
||||
variables: { image64, LiveProjectViewer },
|
||||
}: CellSuccessProps<FindSocialCardQuery>) => {
|
||||
const image = userProject?.Project?.mainImage
|
||||
const gravatar = userProject?.image
|
||||
@@ -47,7 +47,7 @@ export const Success = ({
|
||||
: userProject?.Project?.description || ''
|
||||
return (
|
||||
<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"
|
||||
style={{ gridTemplateRows: ' 555fr 18fr' }}
|
||||
>
|
||||
@@ -96,6 +96,18 @@ export const Success = ({
|
||||
/>
|
||||
)}
|
||||
</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
|
||||
|
||||
@@ -26,7 +26,7 @@ export const canvasToBlob = async (
|
||||
(blob) => {
|
||||
resolve(blob)
|
||||
},
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
0.75
|
||||
)
|
||||
})
|
||||
|
||||
@@ -23,10 +23,7 @@ export const use3dViewerResize = () => {
|
||||
requestRender({
|
||||
state,
|
||||
dispatch,
|
||||
code: state.code,
|
||||
viewerSize: { width, height },
|
||||
camera: state.camera,
|
||||
parameters: state.currentParameters,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -25,19 +25,33 @@ interface XYZ {
|
||||
z: number
|
||||
}
|
||||
|
||||
interface EditorModel {
|
||||
type: 'code' | 'guide'
|
||||
interface CodeTab {
|
||||
type: 'code'
|
||||
label: string
|
||||
content?: string
|
||||
}
|
||||
|
||||
interface GuideTab {
|
||||
type: 'guide'
|
||||
label: string
|
||||
content: string
|
||||
}
|
||||
|
||||
interface ComponentTab {
|
||||
type: 'component'
|
||||
label: string
|
||||
Component: React.FC
|
||||
}
|
||||
|
||||
type EditorTab = GuideTab | CodeTab | ComponentTab
|
||||
|
||||
export interface State {
|
||||
ideType: 'INIT' | CadPackageType
|
||||
viewerContext: 'ide' | 'viewer'
|
||||
ideGuide?: string
|
||||
consoleMessages: { type: 'message' | 'error'; message: string; time: Date }[]
|
||||
code: string
|
||||
models: EditorModel[]
|
||||
editorTabs: EditorTab[]
|
||||
currentModel: number
|
||||
objectData: {
|
||||
type: 'INIT' | ArtifactTypes
|
||||
@@ -78,7 +92,7 @@ export const initialState: State = {
|
||||
{ type: 'message', message: 'Initialising', time: new Date() },
|
||||
],
|
||||
code,
|
||||
models: [{ type: 'code', label: 'Code' }],
|
||||
editorTabs: [{ type: 'code', label: 'Code' }],
|
||||
currentModel: 0,
|
||||
objectData: {
|
||||
type: 'INIT',
|
||||
@@ -240,14 +254,14 @@ const reducer = (state: State, { type, payload }): State => {
|
||||
case 'addEditorModel':
|
||||
return {
|
||||
...state,
|
||||
models: [...state.models, payload],
|
||||
editorTabs: [...state.editorTabs, payload],
|
||||
}
|
||||
case 'removeEditorModel':
|
||||
return {
|
||||
...state,
|
||||
models: [
|
||||
...state.models.slice(0, payload),
|
||||
...state.models.slice(payload + 1),
|
||||
editorTabs: [
|
||||
...state.editorTabs.slice(0, payload),
|
||||
...state.editorTabs.slice(payload + 1),
|
||||
],
|
||||
currentModel: payload === 0 ? 0 : payload - 1,
|
||||
}
|
||||
@@ -284,71 +298,84 @@ export const useIdeState = (): [State, (actionOrThunk: any) => any] => {
|
||||
return [state, thunkDispatch]
|
||||
}
|
||||
|
||||
interface RequestRenderArgs {
|
||||
interface RequestRenderArgsStateless {
|
||||
state: State
|
||||
dispatch: any
|
||||
parameters: any
|
||||
code: State['code']
|
||||
camera: State['camera']
|
||||
viewerSize: State['viewerSize']
|
||||
camera?: State['camera']
|
||||
viewerSize?: State['viewerSize']
|
||||
quality?: State['objectData']['quality']
|
||||
specialCadProcess?: string
|
||||
parameters?: { [key: string]: any }
|
||||
}
|
||||
|
||||
export const requestRender = ({
|
||||
export const requestRenderStateless = ({
|
||||
state,
|
||||
dispatch,
|
||||
code,
|
||||
camera,
|
||||
viewerSize,
|
||||
quality = 'low',
|
||||
specialCadProcess = null,
|
||||
parameters,
|
||||
}: RequestRenderArgs) => {
|
||||
}: RequestRenderArgsStateless): null | Promise<any> => {
|
||||
if (
|
||||
state.ideType !== 'INIT' &&
|
||||
(!state.isLoading || state.objectData?.type === 'INIT')
|
||||
!(
|
||||
state.ideType !== 'INIT' &&
|
||||
(!state.isLoading || state.objectData?.type === 'INIT')
|
||||
)
|
||||
) {
|
||||
const renderFn = specialCadProcess
|
||||
? cadPackages[state.ideType][specialCadProcess]
|
||||
: cadPackages[state.ideType].render
|
||||
return renderFn({
|
||||
code,
|
||||
settings: {
|
||||
parameters: state.isCustomizerOpen ? parameters : {},
|
||||
camera,
|
||||
viewerSize,
|
||||
quality,
|
||||
},
|
||||
})
|
||||
.then(
|
||||
({
|
||||
objectData,
|
||||
message,
|
||||
status,
|
||||
customizerParams,
|
||||
currentParameters,
|
||||
}) => {
|
||||
if (status === 'error') {
|
||||
dispatch({
|
||||
type: 'errorRender',
|
||||
payload: { message },
|
||||
})
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'healthyRender',
|
||||
payload: {
|
||||
objectData,
|
||||
message,
|
||||
lastRunCode: code,
|
||||
customizerParams,
|
||||
currentParameters,
|
||||
},
|
||||
})
|
||||
return objectData
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch(() => dispatch({ type: 'resetLoading' })) // TODO should probably display something to the user here
|
||||
return null
|
||||
}
|
||||
const renderFn = specialCadProcess
|
||||
? cadPackages[state.ideType][specialCadProcess]
|
||||
: cadPackages[state.ideType].render
|
||||
return renderFn({
|
||||
code: state.code,
|
||||
settings: {
|
||||
parameters: state.isCustomizerOpen
|
||||
? parameters || state.currentParameters
|
||||
: {},
|
||||
camera: camera || state.camera,
|
||||
viewerSize: viewerSize || state.viewerSize,
|
||||
quality,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
interface RequestRenderArgs extends RequestRenderArgsStateless {
|
||||
dispatch: any
|
||||
}
|
||||
|
||||
export const requestRender = ({ dispatch, ...rest }: RequestRenderArgs) => {
|
||||
const renderPromise = requestRenderStateless(rest)
|
||||
if (!renderPromise) {
|
||||
return
|
||||
}
|
||||
renderPromise
|
||||
.then(
|
||||
({
|
||||
objectData,
|
||||
message,
|
||||
status,
|
||||
customizerParams,
|
||||
currentParameters,
|
||||
}) => {
|
||||
if (status === 'error') {
|
||||
dispatch({
|
||||
type: 'errorRender',
|
||||
payload: { message },
|
||||
})
|
||||
} else {
|
||||
dispatch({
|
||||
type: 'healthyRender',
|
||||
payload: {
|
||||
objectData,
|
||||
message,
|
||||
lastRunCode: code,
|
||||
customizerParams,
|
||||
currentParameters,
|
||||
},
|
||||
})
|
||||
return objectData
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch(() => dispatch({ type: 'resetLoading' })) // TODO should probably display something to the user here
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user