Overhaul social card (again) #541
@@ -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"
|
||||||
|
>
|
||||||
|
|
|||||||
|
<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({
|
thunkDispatch({
|
||||||
type: 'switchEditorModel',
|
type: 'switchEditorModel',
|
||||||
payload: state.models.length,
|
payload: state.editorTabs.length,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 />,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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'
|
||||||
}`}
|
}`}
|
||||||
|
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
|
<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
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const canvasToBlob = async (
|
|||||||
(blob) => {
|
(blob) => {
|
||||||
resolve(blob)
|
resolve(blob)
|
||||||
},
|
},
|
||||||
'image/jpeg',
|
'image/png',
|
||||||
0.75
|
0.75
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
|
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: {
|
||||||
|
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 👍