From 2153e5b1bf83dc4a5164c6f117a7f45c83e3524a Mon Sep 17 00:00:00 2001 From: Kurt Hutten Date: Sun, 18 Apr 2021 12:28:23 +1000 Subject: [PATCH] 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. --- .../components/IdeContainer/IdeContainer.js | 56 ++++++---- web/src/components/IdeEditor/IdeEditor.js | 32 ++++-- .../components/IdeToolbarNew/IdeToolbarNew.js | 33 +++--- web/src/components/IdeViewer/IdeViewer.js | 103 +++++++++++++----- .../helpers/cadPackages/cadQueryController.js | 68 ++++++++++++ web/src/helpers/cadPackages/index.js | 6 +- .../cadPackages/newCascadeController.js | 35 ------ .../helpers/cadPackages/openScadController.js | 25 +---- web/src/helpers/hooks/useIdeState.js | 99 ++++++++++------- 9 files changed, 295 insertions(+), 162 deletions(-) create mode 100644 web/src/helpers/cadPackages/cadQueryController.js delete mode 100644 web/src/helpers/cadPackages/newCascadeController.js diff --git a/web/src/components/IdeContainer/IdeContainer.js b/web/src/components/IdeContainer/IdeContainer.js index e61b426..6f852aa 100644 --- a/web/src/components/IdeContainer/IdeContainer.js +++ b/web/src/components/IdeContainer/IdeContainer.js @@ -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 (
( - - {id === 'Viewer' ? ( -
- {ELEMENT_MAP[id]} -
- ) : ( - ELEMENT_MAP[id] - )} -
- )} + renderTile={(id, path) => { + const title = id === 'Editor' ? `${id} (${state.ideType})` : id + return ( + + {id === 'Viewer' ? ( +
+ {ELEMENT_MAP[id]} +
+ ) : ( + ELEMENT_MAP[id] + )} +
+ ) + }} value={state.layout} onChange={(newLayout) => - dispatch({ type: 'setLayout', payload: { message: newLayout } }) + thunkDispatch({ type: 'setLayout', payload: { message: newLayout } }) } onRelease={handleViewerSizeUpdate} /> diff --git a/web/src/components/IdeEditor/IdeEditor.js b/web/src/components/IdeEditor/IdeEditor.js index 7ad5ad5..bbc0edd 100644 --- a/web/src/components/IdeEditor/IdeEditor.js +++ b/web/src/components/IdeEditor/IdeEditor.js @@ -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 ( -
+
. . . loading
}> diff --git a/web/src/components/IdeToolbarNew/IdeToolbarNew.js b/web/src/components/IdeToolbarNew/IdeToolbarNew.js index 4920515..fa10a77 100644 --- a/web/src/components/IdeToolbarNew/IdeToolbarNew.js +++ b/web/src/components/IdeToolbarNew/IdeToolbarNew.js @@ -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 ( - +
)} - {state.isLoading && ( -
-
-
- )} -
setIsDragging(true)} > 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, + }) + } + }) + }} /> - - - - + {state.ideType === 'openScad' && ( + <> + + + + + + )} + {state.ideType === 'cadQuery' && ( + + )}
+ {state.isLoading && ( +
+
+
+ )}
) } diff --git a/web/src/helpers/cadPackages/cadQueryController.js b/web/src/helpers/cadPackages/cadQueryController.js new file mode 100644 index 0000000..b57a7a7 --- /dev/null +++ b/web/src/helpers/cadPackages/cadQueryController.js @@ -0,0 +1,68 @@ +let openScadBaseURL = process.env.CADQUERY_BASE_URL + +export const render = async ({ code }) => { + const body = JSON.stringify({ + settings: {}, + file: code, + }) + try { + const response = await fetch(openScadBaseURL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body, + }) + if (response.status === 400) { + // TODO add proper error messages for CadQuery + const { error } = await response.json() + const cleanedErrorMessage = error.replace( + /["|']\/tmp\/.+\/main.scad["|']/g, + "'main.scad'" + ) + return { + status: 'error', + message: { + type: 'error', + message: addDateToLog(cleanedErrorMessage), + }, + } + } + const data = await response.json() + return { + status: 'healthy', + objectData: { + type: 'stl', + data: data.imageBase64, + }, + message: { + type: 'message', + message: addDateToLog(data.result), + }, + } + } catch (e) { + // TODO handle errors better + // I think we should display something overlayed on the viewer window something like "network issue try again" + // and in future I think we need timeouts differently as they maybe from a user trying to render something too complex + // or something with minkowski in it :/ either way something like "render timed out, try again or here are tips to reduce part complexity" with a link talking about $fn and minkowski etc + return { + status: 'error', + message: { + type: 'error', + message: addDateToLog('network issue'), + }, + } + } +} + +const openScad = { + render, + // more functions to come +} + +export default openScad + +function addDateToLog(message) { + return `-> ${new Date().toLocaleString()} +${message}` +} diff --git a/web/src/helpers/cadPackages/index.js b/web/src/helpers/cadPackages/index.js index ab938ae..19494ad 100644 --- a/web/src/helpers/cadPackages/index.js +++ b/web/src/helpers/cadPackages/index.js @@ -1,7 +1,7 @@ import openScad from './openScadController' -import openCascade from './newCascadeController' +import cadQuery from './cadQueryController' export const cadPackages = { openScad, - openCascade, -} \ No newline at end of file + cadQuery, +} diff --git a/web/src/helpers/cadPackages/newCascadeController.js b/web/src/helpers/cadPackages/newCascadeController.js deleted file mode 100644 index 5367fc3..0000000 --- a/web/src/helpers/cadPackages/newCascadeController.js +++ /dev/null @@ -1,35 +0,0 @@ -// Rename this file to remove "new" once Cascade integration is complete - -export const render = async ({ code, settings }) => { - return new Promise((resolve, reject) => { - setTimeout(() => { - const shouldReject = Math.random() < 0.7 - if (shouldReject) { - resolve({ - objectData: { - type: 'stl', - data: ((Math.random() * 256 + 1) >>> 0).toString(2), // Randomized 8-bit numbers for funzies - }, - message: { - type: 'message', - message: `bodies rendered by: ${code}`, - }, - }) - } else { - reject({ - message: { - type: 'error', - message: 'unable to parse line: x', - }, - }) - } - }, 700) - }) -} - -const openCascade = { - render, - // More functions to come -} - -export default openCascade \ No newline at end of file diff --git a/web/src/helpers/cadPackages/openScadController.js b/web/src/helpers/cadPackages/openScadController.js index 167174b..9672c5d 100644 --- a/web/src/helpers/cadPackages/openScadController.js +++ b/web/src/helpers/cadPackages/openScadController.js @@ -2,33 +2,16 @@ let openScadBaseURL = process.env.OPENSCAD_BASE_URL || 'https://x2wvhihk56.execute-api.us-east-1.amazonaws.com/dev' -let lastViewPortSize = 'INIT' -let lastCameraSettings = 'INIT' - export const render = async ({ code, settings }) => { const pixelRatio = window.devicePixelRatio || 1 - const size = settings.viewerSize - ? { - x: Math.round(settings.viewerSize?.width * pixelRatio), - y: Math.round(settings.viewerSize?.height * pixelRatio), - } - : lastViewPortSize - const camera = settings.camera || lastCameraSettings - if (settings.camera) { - lastCameraSettings = settings.camera - } - if (settings.viewerSize) { - lastViewPortSize = size - } - if ([camera, size].includes('INIT')) { - return { - status: 'insufficient-preview-info', - } + const size = { + x: Math.round(settings.viewerSize?.width * pixelRatio), + y: Math.round(settings.viewerSize?.height * pixelRatio), } const body = JSON.stringify({ settings: { size, - camera, + camera: settings.camera, }, file: code, }) diff --git a/web/src/helpers/hooks/useIdeState.js b/web/src/helpers/hooks/useIdeState.js index a6e123b..971b7ee 100644 --- a/web/src/helpers/hooks/useIdeState.js +++ b/web/src/helpers/hooks/useIdeState.js @@ -1,6 +1,13 @@ import { useReducer } from 'react' import { cadPackages } from 'src/helpers/cadPackages' +function withThunk(dispatch, getState) { + return (actionOrThunk) => + typeof actionOrThunk === 'function' + ? actionOrThunk(dispatch, getState) + : dispatch(actionOrThunk) +} + const donutInitCode = ` color(c="DarkGoldenrod")rotate_extrude()translate([20,0])circle(d=30); donut(); @@ -17,16 +24,17 @@ module stick(basewid, angl){ }` export const codeStorageKey = 'Last-openscad-code' +let mutableState = null export const useIdeState = () => { const code = localStorage.getItem(codeStorageKey) || donutInitCode const initialState = { - ideType: 'openScad', + ideType: 'cadQuery', consoleMessages: [{ type: 'message', message: 'Initialising OpenSCAD' }], code, objectData: { type: 'stl', - data: 'some binary', + data: null, }, layout: { direction: 'row', @@ -38,6 +46,8 @@ export const useIdeState = () => { splitPercentage: 70, }, }, + camera: {}, + viewerSize: { width: 0, height: 0 }, isLoading: false, } const reducer = (state, { type, payload }) => { @@ -74,51 +84,64 @@ export const useIdeState = () => { ...state, layout: payload.message, } + case 'updateCamera': + return { + ...state, + camera: payload.camera, + } + case 'updateViewerSize': + return { + ...state, + viewerSize: payload.viewerSize, + } case 'setLoading': return { ...state, isLoading: true, } + case 'resetLoading': + return { + ...state, + isLoading: false, + } default: return state } } - function dispatchMiddleware(dispatch, state) { - return ({ type, payload }) => { - switch (type) { - case 'render': - cadPackages[state.ideType] - .render({ - code: payload.code, - settings: { - camera: payload.camera, - viewerSize: payload.viewerSize, - }, - }) - .then(({ objectData, message, status }) => { - if (status === 'insufficient-preview-info') return - if (status === 'error') { - dispatch({ - type: 'errorRender', - payload: { message }, - }) - } else { - dispatch({ - type: 'healthyRender', - payload: { objectData, message }, - }) - } - }) - dispatch({ type: 'setLoading' }) - break - - default: - return dispatch({ type, payload }) - } - } - } - const [state, dispatch] = useReducer(reducer, initialState) - return [state, dispatchMiddleware(dispatch, state)] + mutableState = state + const getState = () => mutableState + return [state, withThunk(dispatch, getState)] +} + +export const requestRender = ({ + state, + dispatch, + code, + camera, + viewerSize, +}) => { + cadPackages[state.ideType] + .render({ + code, + settings: { + camera, + viewerSize, + }, + }) + .then(({ objectData, message, status }) => { + if (status === 'error') { + dispatch({ + type: 'errorRender', + payload: { message }, + }) + } else { + dispatch({ + type: 'healthyRender', + payload: { objectData, message }, + }) + } + }) + .catch(() => dispatch({ type: 'resetLoading' })) // TODO should probably display something to the user here }