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:
Kurt Hutten
2021-04-18 12:28:23 +10:00
parent 1a702ddae1
commit 2153e5b1bf
9 changed files with 295 additions and 162 deletions

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
)
}