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 }