Front end changes for cadquery
Basic changes to get the proof of concept working. Lots of attention was given to the store/reducer to solve existing problems with async code and stale closures, it seems even today how to handle this with use reducer is not quiet settle, I guess because once an app reaches a certain level of maturity everyone grabs an off the shelf solution to state management. I ended up implementing thunks because they are really rather simple. Interesting thread about it all here: https://gist.github.com/astoilkov/013c513e33fe95fa8846348038d8fe42#gistcomment-3377800 I also move some of settings that were persisted in the openScad controller into the data store as I ulimately thing what I was doing in that file was very confusing, with the fact that it had to be called multiple times with different information before it would be able to render something properly.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { useContext, useRef, useEffect } from 'react'
|
||||
import { Mosaic, MosaicWindow } from 'react-mosaic-component'
|
||||
import { IdeContext } from 'src/components/IdeToolbarNew'
|
||||
import { requestRender } from 'src/helpers/hooks/useIdeState'
|
||||
import IdeEditor from 'src/components/IdeEditor'
|
||||
import IdeViewer from 'src/components/IdeViewer'
|
||||
import IdeConsole from 'src/components/IdeConsole'
|
||||
@@ -13,7 +14,7 @@ const ELEMENT_MAP = {
|
||||
}
|
||||
|
||||
const IdeContainer = () => {
|
||||
const { state, dispatch } = useContext(IdeContext)
|
||||
const { state, thunkDispatch } = useContext(IdeContext)
|
||||
const viewerDOM = useRef(null)
|
||||
const debounceTimeoutId = useRef
|
||||
|
||||
@@ -22,12 +23,22 @@ const IdeContainer = () => {
|
||||
function handleViewerSizeUpdate() {
|
||||
if (viewerDOM !== null && viewerDOM.current) {
|
||||
const { width, height } = viewerDOM.current.getBoundingClientRect()
|
||||
dispatch({
|
||||
type: 'render',
|
||||
payload: {
|
||||
code: state.code,
|
||||
viewerSize: { width, height },
|
||||
},
|
||||
thunkDispatch({
|
||||
type: 'updateViewerSize',
|
||||
payload: { viewerSize: { width, height } },
|
||||
})
|
||||
thunkDispatch((dispatch, getState) => {
|
||||
const state = getState()
|
||||
if (state.ideType === 'openScad') {
|
||||
dispatch({ type: 'setLoading' })
|
||||
requestRender({
|
||||
state,
|
||||
dispatch,
|
||||
code: state.code,
|
||||
viewerSize: { width, height },
|
||||
camera: state.camera,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -49,20 +60,27 @@ const IdeContainer = () => {
|
||||
return (
|
||||
<div id="cadhub-ide" className="flex-auto h-full">
|
||||
<Mosaic
|
||||
renderTile={(id, path) => (
|
||||
<MosaicWindow path={path} title={id} className={id.toLowerCase()}>
|
||||
{id === 'Viewer' ? (
|
||||
<div id="view-wrapper" className="h-full" ref={viewerDOM}>
|
||||
{ELEMENT_MAP[id]}
|
||||
</div>
|
||||
) : (
|
||||
ELEMENT_MAP[id]
|
||||
)}
|
||||
</MosaicWindow>
|
||||
)}
|
||||
renderTile={(id, path) => {
|
||||
const title = id === 'Editor' ? `${id} (${state.ideType})` : id
|
||||
return (
|
||||
<MosaicWindow
|
||||
path={path}
|
||||
title={title}
|
||||
className={id.toLowerCase()}
|
||||
>
|
||||
{id === 'Viewer' ? (
|
||||
<div id="view-wrapper" className="h-full" ref={viewerDOM}>
|
||||
{ELEMENT_MAP[id]}
|
||||
</div>
|
||||
) : (
|
||||
ELEMENT_MAP[id]
|
||||
)}
|
||||
</MosaicWindow>
|
||||
)
|
||||
}}
|
||||
value={state.layout}
|
||||
onChange={(newLayout) =>
|
||||
dispatch({ type: 'setLayout', payload: { message: newLayout } })
|
||||
thunkDispatch({ type: 'setLayout', payload: { message: newLayout } })
|
||||
}
|
||||
onRelease={handleViewerSizeUpdate}
|
||||
/>
|
||||
|
||||
@@ -2,10 +2,15 @@ import { useContext, useEffect, Suspense, lazy } from 'react'
|
||||
import { isBrowser } from '@redwoodjs/prerender/browserUtils'
|
||||
import { IdeContext } from 'src/components/IdeToolbarNew'
|
||||
import { codeStorageKey } from 'src/helpers/hooks/useIdeState'
|
||||
import { requestRender } from 'src/helpers/hooks/useIdeState'
|
||||
const Editor = lazy(() => import('@monaco-editor/react'))
|
||||
|
||||
const IdeEditor = () => {
|
||||
const { state, dispatch } = useContext(IdeContext)
|
||||
const { state, thunkDispatch } = useContext(IdeContext)
|
||||
const ideTypeToLanguageMap = {
|
||||
cadQuery: 'python',
|
||||
openScad: 'cpp',
|
||||
}
|
||||
|
||||
const scriptKey = 'encoded_script'
|
||||
useEffect(() => {
|
||||
@@ -17,7 +22,7 @@ const IdeEditor = () => {
|
||||
const [key, scriptBase64] = hash.slice(1).split('=')
|
||||
if (key === scriptKey) {
|
||||
const script = atob(scriptBase64)
|
||||
dispatch({ type: 'updateCode', payload: script })
|
||||
thunkDispatch({ type: 'updateCode', payload: script })
|
||||
}
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
@@ -27,25 +32,38 @@ const IdeEditor = () => {
|
||||
}, [state.code])
|
||||
|
||||
function handleCodeChange(value, _event) {
|
||||
dispatch({ type: 'updateCode', payload: value })
|
||||
thunkDispatch({ type: 'updateCode', payload: value })
|
||||
}
|
||||
function handleSaveHotkey(event) {
|
||||
//ctrl|meta + s is very intuitive for most devs
|
||||
const { key, ctrlKey, metaKey } = event
|
||||
if (key === 's' && (ctrlKey || metaKey)) {
|
||||
event.preventDefault()
|
||||
dispatch({ type: 'render', payload: { code: state.code } })
|
||||
thunkDispatch((dispatch, getState) => {
|
||||
const state = getState()
|
||||
dispatch({ type: 'setLoading' })
|
||||
requestRender({
|
||||
state,
|
||||
dispatch,
|
||||
code: state.code,
|
||||
viewerSize: state.viewerSize,
|
||||
camera: state.camera,
|
||||
})
|
||||
})
|
||||
localStorage.setItem(codeStorageKey, state.code)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full" onKeyDown={handleSaveHotkey}>
|
||||
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
|
||||
className="h-full"
|
||||
onKeyDown={handleSaveHotkey}
|
||||
>
|
||||
<Suspense fallback={<div>. . . loading</div>}>
|
||||
<Editor
|
||||
defaultValue={state.code}
|
||||
// TODO #247 cpp seems better than js for the time being
|
||||
defaultLanguage="cpp"
|
||||
defaultLanguage={ideTypeToLanguageMap[state.ideType] || 'cpp'}
|
||||
language={ideTypeToLanguageMap[state.ideType] || 'cpp'}
|
||||
onChange={handleCodeChange}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
@@ -3,15 +3,26 @@ import IdeContainer from 'src/components/IdeContainer'
|
||||
import { isBrowser } from '@redwoodjs/prerender/browserUtils'
|
||||
import { useIdeState, codeStorageKey } from 'src/helpers/hooks/useIdeState'
|
||||
import { copyTextToClipboard } from 'src/helpers/clipboard'
|
||||
import { requestRender } from 'src/helpers/hooks/useIdeState'
|
||||
|
||||
export const IdeContext = createContext()
|
||||
const IdeToolbarNew = () => {
|
||||
const [state, dispatch] = useIdeState()
|
||||
const [state, thunkDispatch] = useIdeState()
|
||||
function setIdeType(ide) {
|
||||
dispatch({ type: 'setIdeType', payload: { message: ide } })
|
||||
thunkDispatch({ type: 'setIdeType', payload: { message: ide } })
|
||||
}
|
||||
function handleRender() {
|
||||
dispatch({ type: 'render', payload: { code: state.code } })
|
||||
thunkDispatch((dispatch, getState) => {
|
||||
const state = getState()
|
||||
dispatch({ type: 'setLoading' })
|
||||
requestRender({
|
||||
state,
|
||||
dispatch,
|
||||
code: state.code,
|
||||
viewerSize: state.viewerSize,
|
||||
camera: state.camera,
|
||||
})
|
||||
})
|
||||
localStorage.setItem(codeStorageKey, state.code)
|
||||
}
|
||||
function handleMakeLink() {
|
||||
@@ -23,21 +34,17 @@ const IdeToolbarNew = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<IdeContext.Provider value={{ state, dispatch }}>
|
||||
<IdeContext.Provider value={{ state, thunkDispatch: thunkDispatch }}>
|
||||
<div className="h-full flex flex-col">
|
||||
<nav className="flex">
|
||||
{/* <button
|
||||
onClick={() => setIdeType('openCascade')}
|
||||
<button
|
||||
onClick={() =>
|
||||
setIdeType(state.ideType === 'openScad' ? 'cadQuery' : 'openScad')
|
||||
}
|
||||
className="p-2 br-2 border-2 m-2 bg-blue-200"
|
||||
>
|
||||
Switch to OpenCascade
|
||||
Switch to {state.ideType === 'openScad' ? 'CadQuery' : 'OpenSCAD'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIdeType('openScad')}
|
||||
className="p-2 br-2 border-2 m-2 bg-indigo-200"
|
||||
>
|
||||
Switch to OpenSCAD
|
||||
</button> */}
|
||||
<button onClick={handleRender} className="p-2 br-2 border-2 m-2">
|
||||
Render
|
||||
</button>
|
||||
|
||||
@@ -1,11 +1,41 @@
|
||||
import { IdeContext } from 'src/components/IdeToolbarNew'
|
||||
import { useRef, useState, useEffect, useContext } from 'react'
|
||||
import { Canvas, extend, useFrame, useThree } from 'react-three-fiber'
|
||||
import {
|
||||
Canvas,
|
||||
extend,
|
||||
useFrame,
|
||||
useThree,
|
||||
useUpdate,
|
||||
} from 'react-three-fiber'
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
|
||||
import { Vector3 } from 'three'
|
||||
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
|
||||
import { requestRender } from 'src/helpers/hooks/useIdeState'
|
||||
|
||||
extend({ OrbitControls })
|
||||
|
||||
function Asset({ url }) {
|
||||
const [loadedGeometry, setLoadedGeometry] = useState()
|
||||
const mesh = useRef()
|
||||
const ref = useUpdate((geometry) => {
|
||||
geometry.attributes = loadedGeometry.attributes
|
||||
})
|
||||
useEffect(() => {
|
||||
if (url) {
|
||||
const decoded = atob(url)
|
||||
const loader = new STLLoader()
|
||||
setLoadedGeometry(loader.parse(decoded))
|
||||
}
|
||||
}, [url])
|
||||
if (!loadedGeometry) return null
|
||||
return (
|
||||
<mesh ref={mesh} scale={[1, 1, 1]}>
|
||||
<bufferGeometry attach="geometry" ref={ref} />
|
||||
<meshStandardMaterial color="#F472B6" />
|
||||
</mesh>
|
||||
)
|
||||
}
|
||||
|
||||
let debounceTimeoutId
|
||||
function Controls({ onCameraChange, onDragStart }) {
|
||||
const controls = useRef()
|
||||
@@ -85,7 +115,7 @@ function Controls({ onCameraChange, onDragStart }) {
|
||||
}
|
||||
}, [])
|
||||
|
||||
useFrame(() => controls.current.update())
|
||||
useFrame(() => controls.current?.update())
|
||||
return (
|
||||
<orbitControls
|
||||
ref={controls}
|
||||
@@ -115,9 +145,8 @@ function Sphere(props) {
|
||||
</mesh>
|
||||
)
|
||||
}
|
||||
let currentCode // I have no idea why this works and using state.code is the dispatch doesn't but it was always stale
|
||||
const IdeViewer = () => {
|
||||
const { state, dispatch } = useContext(IdeContext)
|
||||
const { state, thunkDispatch } = useContext(IdeContext)
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [image, setImage] = useState()
|
||||
|
||||
@@ -127,8 +156,7 @@ const IdeViewer = () => {
|
||||
'data:image/png;base64,' + state.objectData?.data
|
||||
)
|
||||
setIsDragging(false)
|
||||
}, [state.objectData])
|
||||
currentCode = state.code
|
||||
}, [state.objectData?.type, state.objectData?.data])
|
||||
|
||||
const openSCADDeepOceanThemeBackground = '#323232'
|
||||
// the following are tailwind colors in hex, can't use these classes to color three.js meshes.
|
||||
@@ -151,41 +179,64 @@ const IdeViewer = () => {
|
||||
isDragging ? 'opacity-25' : 'opacity-100'
|
||||
}`}
|
||||
>
|
||||
<img src={image} className="h-full w-full" />
|
||||
<img alt="code-cad preview" src={image} className="h-full w-full" />
|
||||
</div>
|
||||
)}
|
||||
{state.isLoading && (
|
||||
<div className="inset-0 absolute flex items-center justify-center">
|
||||
<div className="h-16 w-16 bg-pink-600 rounded-full animate-ping"></div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
|
||||
className={`opacity-0 absolute inset-0 transition-opacity duration-500 ${
|
||||
isDragging ? 'opacity-100' : 'hover:opacity-50'
|
||||
!(isDragging || state.ideType !== 'openScad')
|
||||
? 'hover:opacity-50'
|
||||
: 'opacity-100'
|
||||
}`}
|
||||
onMouseDown={() => setIsDragging(true)}
|
||||
>
|
||||
<Canvas>
|
||||
<Controls
|
||||
onDragStart={() => setIsDragging(true)}
|
||||
onCameraChange={(camera) =>
|
||||
dispatch({
|
||||
type: 'render',
|
||||
payload: {
|
||||
code: currentCode,
|
||||
camera,
|
||||
},
|
||||
onCameraChange={(camera) => {
|
||||
thunkDispatch({
|
||||
type: 'updateCamera',
|
||||
payload: { camera },
|
||||
})
|
||||
}
|
||||
thunkDispatch((dispatch, getState) => {
|
||||
const state = getState()
|
||||
if (state.ideType === 'openScad') {
|
||||
dispatch({ type: 'setLoading' })
|
||||
requestRender({
|
||||
state,
|
||||
dispatch,
|
||||
code: state.code,
|
||||
viewerSize: state.viewerSize,
|
||||
camera,
|
||||
})
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<ambientLight />
|
||||
<pointLight position={[15, 5, 10]} />
|
||||
<Sphere position={[0, 0, 0]} color={pink400} />
|
||||
<Box position={[0, 50, 0]} size={[1, 100, 1]} color={indigo900} />
|
||||
<Box position={[0, 0, -50]} size={[1, 1, 100]} color={indigo300} />
|
||||
<Box position={[50, 0, 0]} size={[100, 1, 1]} color={pink400} />
|
||||
{state.ideType === 'openScad' && (
|
||||
<>
|
||||
<Sphere position={[0, 0, 0]} color={pink400} />
|
||||
<Box position={[0, 50, 0]} size={[1, 100, 1]} color={indigo900} />
|
||||
<Box
|
||||
position={[0, 0, -50]}
|
||||
size={[1, 1, 100]}
|
||||
color={indigo300}
|
||||
/>
|
||||
<Box position={[50, 0, 0]} size={[100, 1, 1]} color={pink400} />
|
||||
</>
|
||||
)}
|
||||
{state.ideType === 'cadQuery' && (
|
||||
<Asset url={state.objectData?.data} />
|
||||
)}
|
||||
</Canvas>
|
||||
</div>
|
||||
{state.isLoading && (
|
||||
<div className="inset-0 absolute flex items-center justify-center">
|
||||
<div className="h-16 w-16 bg-pink-600 rounded-full animate-ping"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user