diff --git a/app/api/src/docker/openscad/runScad.ts b/app/api/src/docker/openscad/runScad.ts index fa724b9..c176931 100644 --- a/app/api/src/docker/openscad/runScad.ts +++ b/app/api/src/docker/openscad/runScad.ts @@ -11,6 +11,7 @@ const cleanOpenScadError = (error) => export const runScad = async ({ file, settings: { + viewAll = false, size: { x = 500, y = 500 } = {}, parameters, camera: { @@ -44,10 +45,13 @@ export const runScad = async ({ const fullPath = `/tmp/${tempFile}/output.gz` const imPath = `/tmp/${tempFile}/output.png` const customizerPath = `/tmp/${tempFile}/customizer.param` + const summaryPath = `/tmp/${tempFile}/summary.json` // contains camera info const command = [ OPENSCAD_COMMON, `-o ${customizerPath}`, `-o ${imPath}`, + `--summary camera --summary-file ${summaryPath}`, + viewAll ? '--viewall' : '', `-p /tmp/${tempFile}/params.json -P default`, cameraArg, `--imgsize=${x},${y}`, @@ -58,14 +62,20 @@ export const runScad = async ({ try { const consoleMessage = await runCommand(command, 15000) - const params = JSON.parse( - await readFile(customizerPath, { encoding: 'ascii' }) - ).parameters + const files: string[] = await Promise.all( + [customizerPath, summaryPath].map((path) => + readFile(path, { encoding: 'ascii' }) + ) + ) + const [params, cameraInfo] = files.map((fileStr: string) => + JSON.parse(fileStr) + ) await writeFiles( [ { file: JSON.stringify({ - customizerParams: params, + cameraInfo: viewAll ? cameraInfo.camera : undefined, + customizerParams: params.parameters, consoleMessage, type: 'png', }), diff --git a/app/web/src/App.tsx b/app/web/src/App.tsx index 61cd7fa..4906ec5 100644 --- a/app/web/src/App.tsx +++ b/app/web/src/App.tsx @@ -5,7 +5,7 @@ import { RedwoodProvider } from '@redwoodjs/web' import FatalErrorBoundary from 'src/components/FatalErrorBoundary/FatalErrorBoundary' import { RedwoodApolloProvider } from '@redwoodjs/web/apollo' import FatalErrorPage from 'src/pages/FatalErrorPage' -import { createMuiTheme } from '@material-ui/core/styles' +import { createTheme } from '@material-ui/core/styles' import { ThemeProvider } from '@material-ui/styles' import ReactGA from 'react-ga' @@ -22,7 +22,7 @@ const goTrueClient = new GoTrue({ setCookie: true, }) -const theme = createMuiTheme({ +const theme = createTheme({ palette: { type: 'dark', primary: { diff --git a/app/web/src/components/IdeViewer/IdeViewer.tsx b/app/web/src/components/IdeViewer/IdeViewer.tsx index 2fd2350..31e31dd 100644 --- a/app/web/src/components/IdeViewer/IdeViewer.tsx +++ b/app/web/src/components/IdeViewer/IdeViewer.tsx @@ -16,6 +16,7 @@ import texture from './dullFrontLitMetal.png' import Customizer from 'src/components/Customizer/Customizer' import DelayedPingAnimation from 'src/components/DelayedPingAnimation/DelayedPingAnimation' import type { ArtifactTypes } from 'src/helpers/cadPackages/common' +import { State } from 'src/helpers/hooks/useIdeState' const thresholdAngle = 12 @@ -24,9 +25,9 @@ function Asset({ dataType, controlsRef, }: { - geometry: any + geometry: any // eslint-disable-line @typescript-eslint/no-explicit-any dataType: 'INIT' | ArtifactTypes - controlsRef: React.MutableRefObject + controlsRef: React.MutableRefObject // eslint-disable-line @typescript-eslint/no-explicit-any }) { const threeInstance = useThree() const [initZoom, setInitZoom] = useState(true) @@ -75,7 +76,7 @@ function Asset({ zoomToFit() setInitZoom(false) } - }, [incomingGeo, dataType]) + }, [incomingGeo, dataType, controlsRef, initZoom, threeInstance]) const PrimitiveArray = React.useMemo( () => dataType === 'primitive-array' && @@ -112,15 +113,29 @@ function Asset({ } let debounceTimeoutId -function Controls({ onCameraChange, onDragStart, onInit, controlsRef }) { +function Controls({ + onCameraChange, + onDragStart, + onInit, + controlsRef, + camera: scadCamera, +}: { + onCameraChange: Function + onDragStart: (a: any) => void + onInit: Function + controlsRef: React.MutableRefObject + camera: State['camera'] +}) { const threeInstance = useThree() const { camera, gl } = threeInstance useEffect(() => { + // setup three to openscad camera sync + onInit(threeInstance) // init camera position - camera.position.x = 200 - camera.position.y = 140 - camera.position.z = 20 + camera.position.x = 80 + camera.position.y = 50 + camera.position.z = 50 camera.far = 10000 camera.fov = 22.5 // matches default openscad fov camera.updateProjectionMatrix() @@ -146,8 +161,8 @@ function Controls({ onCameraChange, onDragStart, onInit, controlsRef }) { ) const { x, y, z } = head2Head.add(camera.position) return { - position: { x, y, z }, - dist: camera.position.length(), + position: { x: x / 2, y: y / 2, z: z / 2 }, + dist: camera.position.length() / 2, } } @@ -180,6 +195,34 @@ function Controls({ onCameraChange, onDragStart, onInit, controlsRef }) { } }, [camera, controlsRef]) + useEffect(() => { + if (!scadCamera?.isScadUpdate || !scadCamera?.position) { + return + } + // sync Three camera to OpenSCAD + const { x, y, z } = scadCamera.position || {} + const scadCameraPos = new Vector3(x * 2, y * 2, z * 2) + const cameraViewVector = new Vector3(0, 0, 1) + const { x: rx, y: ry, z: rz } = scadCamera.rotation || {} + const scadCameraEuler = new THREE.Euler( + ...[rx, ry, rz].map((r) => (r * Math.PI) / 180), + 'YZX' + ) // I don't know why it seems to like 'YZX' order + cameraViewVector.applyEuler(scadCameraEuler) + cameraViewVector.multiplyScalar(scadCamera.dist * 2) + + const scadToThreeCameraPosition = new Vector3().subVectors( + // I have no idea why this works + cameraViewVector.clone().add(scadCameraPos), + cameraViewVector + ) + scadToThreeCameraPosition.multiplyScalar( + scadCamera.dist / scadToThreeCameraPosition.length() + ) + camera.position.copy(scadToThreeCameraPosition.clone()) + camera.updateProjectionMatrix() + }, [scadCamera, camera]) + return ( { if (handleOwnCamera) { - console.log('yo') return } thunkDispatch({ @@ -362,6 +407,7 @@ const IdeViewer = ({ state, dispatch, camera, + viewAll: state?.objectData?.type === 'INIT', }) } }) @@ -374,6 +420,7 @@ const IdeViewer = ({ onInit={onInit} onCameraChange={onCameraChange} isLoading={state.isLoading} + camera={state?.camera} /> ) } diff --git a/app/web/src/helpers/cadPackages/common.ts b/app/web/src/helpers/cadPackages/common.ts index 4edbd8b..bacd017 100644 --- a/app/web/src/helpers/cadPackages/common.ts +++ b/app/web/src/helpers/cadPackages/common.ts @@ -1,10 +1,11 @@ import { STLLoader } from 'three/examples/jsm/loaders/STLLoader' import { State } from 'src/helpers/hooks/useIdeState' import { CadhubParams } from 'src/components/Customizer/customizerConverter' +import type { Camera } from 'src/helpers/hooks/useIdeState' export const lambdaBaseURL = process.env.CAD_LAMBDA_BASE_URL || - 'https://2inlbple1b.execute-api.us-east-1.amazonaws.com/prod2' + 'https://oxt2p7ddgj.execute-api.us-east-1.amazonaws.com/prod' export const stlToGeometry = (url) => new Promise((resolve, reject) => { @@ -18,6 +19,7 @@ export interface RenderArgs { camera: State['camera'] viewerSize: State['viewerSize'] quality: State['objectData']['quality'] + viewAll: boolean } } @@ -36,6 +38,7 @@ export interface HealthyResponse { } customizerParams?: any[] currentParameters?: RawCustomizerParams + camera?: Camera } export interface RawCustomizerParams { @@ -48,12 +51,14 @@ export function createHealthyResponse({ consoleMessage, type, customizerParams, + camera, }: { date: Date data: any consoleMessage: string type: HealthyResponse['objectData']['type'] customizerParams?: CadhubParams[] + camera?: Camera }): HealthyResponse { return { status: 'healthy', @@ -66,6 +71,7 @@ export function createHealthyResponse({ message: consoleMessage, time: date, }, + camera, customizerParams, } } diff --git a/app/web/src/helpers/cadPackages/jsCad/jsCadController.tsx b/app/web/src/helpers/cadPackages/jsCad/jsCadController.tsx index e24ebb1..bfdcf97 100644 --- a/app/web/src/helpers/cadPackages/jsCad/jsCadController.tsx +++ b/app/web/src/helpers/cadPackages/jsCad/jsCadController.tsx @@ -99,7 +99,7 @@ class WorkerHelper { } render = ( code: string, - parameters: { [key: string]: any } + parameters: { [key: string]: any } // eslint-disable-line @typescript-eslint/no-explicit-any ): Promise => { const response: Promise = new Promise( (resolve: ResolveFn) => { diff --git a/app/web/src/helpers/cadPackages/openScad/openScadController.ts b/app/web/src/helpers/cadPackages/openScad/openScadController.ts index ceb00e5..86190d8 100644 --- a/app/web/src/helpers/cadPackages/openScad/openScadController.ts +++ b/app/web/src/helpers/cadPackages/openScad/openScadController.ts @@ -8,6 +8,7 @@ import { splitGziped, } from '../common' import { openScadToCadhubParams } from './openScadParams' +import type { XYZ, Camera } from 'src/helpers/hooks/useIdeState' export const render = async ({ code, settings }: RenderArgs) => { const pixelRatio = window.devicePixelRatio || 1 @@ -19,6 +20,7 @@ export const render = async ({ code, settings }: RenderArgs) => { const body = JSON.stringify({ settings: { size, + viewAll: settings.viewAll, parameters: settings.parameters, camera: { // rounding to give our caching a chance to sometimes work @@ -59,7 +61,21 @@ export const render = async ({ code, settings }: RenderArgs) => { } const blob = await response.blob() const text = await new Response(blob).text() - const { consoleMessage, customizerParams, type } = splitGziped(text) + const { consoleMessage, customizerParams, type, cameraInfo } = + splitGziped(text) + const vecArray2Obj = (arr: number[]): XYZ => ({ + x: arr[0], + y: arr[1], + z: arr[2], + }) + const camera: Camera = cameraInfo + ? { + dist: cameraInfo?.distance, + position: vecArray2Obj(cameraInfo?.translation), + rotation: vecArray2Obj(cameraInfo?.rotation), + isScadUpdate: true, + } + : undefined return createHealthyResponse({ type: type !== 'stl' ? 'png' : 'geometry', data: @@ -67,6 +83,7 @@ export const render = async ({ code, settings }: RenderArgs) => { ? blob : await stlToGeometry(window.URL.createObjectURL(blob)), consoleMessage, + camera, date: new Date(), customizerParams: openScadToCadhubParams(customizerParams || []), }) diff --git a/app/web/src/helpers/hooks/use3dViewerResize.ts b/app/web/src/helpers/hooks/use3dViewerResize.ts index 6266a57..aeb160f 100644 --- a/app/web/src/helpers/hooks/use3dViewerResize.ts +++ b/app/web/src/helpers/hooks/use3dViewerResize.ts @@ -18,12 +18,13 @@ export const use3dViewerResize = () => { }) thunkDispatch((dispatch, getState) => { const state = getState() - if (['png', 'INIT'].includes(state.objectData?.type)) { + if (state.objectData?.type === 'png') { dispatch({ type: 'setLoading' }) requestRender({ state, dispatch, viewerSize: { width, height }, + viewAll: state.objectData?.type === 'INIT', }) } }) diff --git a/app/web/src/helpers/hooks/useIdeState.ts b/app/web/src/helpers/hooks/useIdeState.ts index f5ec407..47b2773 100644 --- a/app/web/src/helpers/hooks/useIdeState.ts +++ b/app/web/src/helpers/hooks/useIdeState.ts @@ -19,11 +19,17 @@ const codeStorageKey = 'Last-editor-code' export const makeCodeStoreKey = (ideType) => `${codeStorageKey}-${ideType}` let mutableState: State = null -interface XYZ { +export interface XYZ { x: number y: number z: number } +export interface Camera { + dist?: number + position?: XYZ + rotation?: XYZ + isScadUpdate?: boolean +} export interface MosaicTree { first: string | MosaicTree @@ -69,11 +75,7 @@ export interface State { currentParameters?: RawCustomizerParams isCustomizerOpen: boolean layout: MosaicTree - camera: { - dist?: number - position?: XYZ - rotation?: XYZ - } + camera: Camera viewerSize: { width: number; height: number } isLoading: boolean threeInstance: RootState @@ -165,6 +167,7 @@ const reducer = (state: State, { type, payload }): State => { ? [...state.consoleMessages, payload.message] : payload.message, isLoading: false, + camera: payload.camera || state.camera, } } case 'errorRender': @@ -308,6 +311,7 @@ export const useIdeState = (): [State, (actionOrThunk: any) => any] => { interface RequestRenderArgsStateless { state: State camera?: State['camera'] + viewAll?: boolean viewerSize?: State['viewerSize'] quality?: State['objectData']['quality'] specialCadProcess?: string @@ -317,6 +321,7 @@ interface RequestRenderArgsStateless { export const requestRenderStateless = ({ state, camera, + viewAll, viewerSize, quality = 'low', specialCadProcess = null, @@ -339,6 +344,7 @@ export const requestRenderStateless = ({ parameters: state.isCustomizerOpen ? parameters || state.currentParameters : {}, + viewAll, camera: camera || state.camera, viewerSize: viewerSize || state.viewerSize, quality, @@ -363,6 +369,7 @@ export const requestRender = ({ dispatch, ...rest }: RequestRenderArgs) => { status, customizerParams, currentParameters, + camera, }) => { if (status === 'error') { dispatch({ @@ -378,6 +385,7 @@ export const requestRender = ({ dispatch, ...rest }: RequestRenderArgs) => { lastRunCode: code, customizerParams, currentParameters, + camera, }, }) return objectData