Overhaul social card (again) #541
@@ -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()
|
||||
|
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. 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>
|
||||
|
even though we're using the canvas directly (passed in through 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,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}`}
|
||||
|
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.
This is a great decoupling move! This is a great decoupling move!
|
||||
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
|
||||
|
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 {
|
||||
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: {
|
||||
|
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) 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
|
||||
}
|
||||
|
||||
@Irev-Dev could this component be used for a /view endpoint?
Yeah I suppose so 👍