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/CaptureButton/CaptureButton.tsx b/app/web/src/components/CaptureButton/CaptureButton.tsx index 8f9ee01..598a0d3 100644 --- a/app/web/src/components/CaptureButton/CaptureButton.tsx +++ b/app/web/src/components/CaptureButton/CaptureButton.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import { toast } from '@redwoodjs/web/toast' import { toJpeg } from 'html-to-image' @@ -6,21 +6,13 @@ import { useIdeContext } from 'src/helpers/hooks/useIdeContext' import { canvasToBlob, blobTo64 } from 'src/helpers/canvasToBlob' import { useUpdateProjectImages } from 'src/helpers/hooks/useUpdateProjectImages' import { requestRenderStateless } from 'src/helpers/hooks/useIdeState' -import { PureIdeViewer } from 'src/components/IdeViewer/IdeViewer' +import { PureIdeViewer } from 'src/components/IdeViewer/PureIdeViewer' +import { State } from 'src/helpers/hooks/useIdeState' import SocialCardCell from 'src/components/SocialCardCell/SocialCardCell' export const captureSize = { width: 500, height: 522 } -const anchorOrigin = { - vertical: 'bottom', - horizontal: 'center', -} -const transformOrigin = { - vertical: 'top', - horizontal: 'center', -} - -export const CaptureButtonViewer = ({ +const CaptureButtonViewer = ({ onInit, onScadImage, canvasRatio = 1, @@ -34,11 +26,12 @@ export const CaptureButtonViewer = ({ const [dataType, dataTypeSetter] = useState(state?.objectData?.type) const [artifact, artifactSetter] = useState(state?.objectData?.data) const [isLoading, isLoadingSetter] = useState(false) + const [camera, cameraSetter] = useState(null) const getThreeInstance = (_threeInstance) => { threeInstance.current = _threeInstance onInit(_threeInstance) } - const onCameraChange = (camera) => { + const onCameraChange = (camera, isFirstCameraChange) => { const renderPromise = state.ideType === 'openscad' && requestRenderStateless({ @@ -48,12 +41,16 @@ export const CaptureButtonViewer = ({ width: threeInstance.current.size.width * canvasRatio, height: threeInstance.current.size.height * canvasRatio, }, + viewAll: isFirstCameraChange, }) if (!renderPromise) { return } isLoadingSetter(true) - renderPromise.then(async ({ objectData }) => { + renderPromise.then(async ({ objectData, camera }) => { + if (camera?.isScadUpdate) { + cameraSetter(camera) + } isLoadingSetter(false) dataTypeSetter(objectData?.type) artifactSetter(objectData?.data) @@ -71,6 +68,7 @@ export const CaptureButtonViewer = ({ onInit={getThreeInstance} onCameraChange={onCameraChange} isLoading={isLoading} + camera={camera} isMinimal /> ) @@ -115,8 +113,9 @@ function SocialCardLiveViewer({ userName={project.user.userName} projectTitle={project.title} image64={partSnapShot64} - LiveProjectViewer={() => children} - /> + > + {children} + diff --git a/app/web/src/components/IdeViewer/Asset.tsx b/app/web/src/components/IdeViewer/Asset.tsx new file mode 100644 index 0000000..e6b08b1 --- /dev/null +++ b/app/web/src/components/IdeViewer/Asset.tsx @@ -0,0 +1,101 @@ +import * as THREE from 'three' +import { useState } from 'react' +import { useThree } from '@react-three/fiber' +import { useTexture } from '@react-three/drei' +import { useEdgeSplit } from 'src/helpers/hooks/useEdgeSplit' +import texture from './dullFrontLitMetal.png' +import type { ArtifactTypes } from 'src/helpers/cadPackages/common' + +const thresholdAngle = 12 + +export function Asset({ + geometry: incomingGeo, + dataType, + controlsRef, +}: { + geometry: any // eslint-disable-line @typescript-eslint/no-explicit-any + dataType: 'INIT' | ArtifactTypes + controlsRef: React.MutableRefObject // eslint-disable-line @typescript-eslint/no-explicit-any +}) { + const threeInstance = useThree() + const [initZoom, setInitZoom] = useState(true) + const mesh = useEdgeSplit((thresholdAngle * Math.PI) / 180, true, incomingGeo) + const edges = React.useMemo( + () => + incomingGeo.length || dataType !== 'geometry' + ? null + : new THREE.EdgesGeometry(incomingGeo, thresholdAngle), + [incomingGeo, dataType] + ) + React.useEffect(() => { + const getBoundingSphere = () => { + if (dataType === 'geometry') { + return incomingGeo.boundingSphere + } + const group = new THREE.Group() + incomingGeo.forEach((mesh) => group.add(mesh)) + const bbox = new THREE.Box3().setFromObject(group) + return bbox.getBoundingSphere(new THREE.Sphere()) + } + const bSphere = getBoundingSphere() + + const zoomToFit = () => { + const { center, radius } = bSphere + const { camera } = threeInstance + const offset = 3 + controlsRef.current.reset() + controlsRef.current.target.copy(center) + + camera.position.copy( + center + .clone() + .add( + new THREE.Vector3( + offset * radius, + -offset * radius, + offset * radius + ) + ) + ) + camera.updateProjectionMatrix() + } + if (initZoom) { + if (!bSphere) return + zoomToFit() + setInitZoom(false) + } + }, [incomingGeo, dataType, controlsRef, initZoom, threeInstance]) + const PrimitiveArray = React.useMemo( + () => + dataType === 'primitive-array' && + incomingGeo?.map((mesh) => mesh.clone()), + [dataType, incomingGeo] + ) + const colorMap = useTexture(texture) + + if (!incomingGeo) return null + if (PrimitiveArray) + return PrimitiveArray.map((mesh, index) => ( + + )) + + return ( + + + + + + + + + ) +} diff --git a/app/web/src/components/IdeViewer/IdeViewer.tsx b/app/web/src/components/IdeViewer/IdeViewer.tsx index 2fd2350..51ea4a4 100644 --- a/app/web/src/components/IdeViewer/IdeViewer.tsx +++ b/app/web/src/components/IdeViewer/IdeViewer.tsx @@ -1,337 +1,6 @@ import { useIdeContext } from 'src/helpers/hooks/useIdeContext' -import * as THREE from 'three' -import { useRef, useState, useEffect, Suspense } from 'react' -import { Canvas, useThree } from '@react-three/fiber' -import { - PerspectiveCamera, - GizmoHelper, - GizmoViewport, - OrbitControls, - useTexture, -} from '@react-three/drei' -import { useEdgeSplit } from 'src/helpers/hooks/useEdgeSplit' -import { Vector3 } from 'three' import { requestRender } from 'src/helpers/hooks/useIdeState' -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' - -const thresholdAngle = 12 - -function Asset({ - geometry: incomingGeo, - dataType, - controlsRef, -}: { - geometry: any - dataType: 'INIT' | ArtifactTypes - controlsRef: React.MutableRefObject -}) { - const threeInstance = useThree() - const [initZoom, setInitZoom] = useState(true) - const mesh = useEdgeSplit((thresholdAngle * Math.PI) / 180, true, incomingGeo) - const edges = React.useMemo( - () => - incomingGeo.length || dataType !== 'geometry' - ? null - : new THREE.EdgesGeometry(incomingGeo, thresholdAngle), - [incomingGeo, dataType] - ) - React.useEffect(() => { - const getBoundingSphere = () => { - if (dataType === 'geometry') { - return incomingGeo.boundingSphere - } - const group = new THREE.Group() - incomingGeo.forEach((mesh) => group.add(mesh)) - const bbox = new THREE.Box3().setFromObject(group) - return bbox.getBoundingSphere(new THREE.Sphere()) - } - const bSphere = getBoundingSphere() - - const zoomToFit = () => { - const { center, radius } = bSphere - const { camera } = threeInstance - const offset = 3 - controlsRef.current.reset() - controlsRef.current.target.copy(center) - - camera.position.copy( - center - .clone() - .add( - new THREE.Vector3( - offset * radius, - -offset * radius, - offset * radius - ) - ) - ) - camera.updateProjectionMatrix() - } - if (initZoom) { - if (!bSphere) return - zoomToFit() - setInitZoom(false) - } - }, [incomingGeo, dataType]) - const PrimitiveArray = React.useMemo( - () => - dataType === 'primitive-array' && - incomingGeo?.map((mesh) => mesh.clone()), - [dataType, incomingGeo] - ) - const colorMap = useTexture(texture) - - if (!incomingGeo) return null - if (PrimitiveArray) - return PrimitiveArray.map((mesh, index) => ( - - )) - - return ( - - - - - - - - - ) -} - -let debounceTimeoutId -function Controls({ onCameraChange, onDragStart, onInit, controlsRef }) { - const threeInstance = useThree() - const { camera, gl } = threeInstance - useEffect(() => { - onInit(threeInstance) - // init camera position - camera.position.x = 200 - camera.position.y = 140 - camera.position.z = 20 - camera.far = 10000 - camera.fov = 22.5 // matches default openscad fov - camera.updateProjectionMatrix() - - camera.rotation._order = 'ZYX' - const getRotations = (): number[] => { - const { x, y, z } = camera?.rotation || {} - return [x, y, z].map((rot) => (rot * 180) / Math.PI) - } - const getPositions = () => { - // Difficult to make this clean since I'm not sure why it works - // The OpenSCAD camera seems hard to work with but maybe it's just me - - // this gives us a vector the same length as the camera.position - const cameraViewVector = new Vector3(0, 0, 1) - .applyQuaternion(camera.quaternion) // make unit vector of the camera - .multiplyScalar(camera.position.length()) // make it the same length as the position vector - - // make a vector from the position vector to the cameraView vector - const head2Head = new Vector3().subVectors( - camera.position, - cameraViewVector - ) - const { x, y, z } = head2Head.add(camera.position) - return { - position: { x, y, z }, - dist: camera.position.length(), - } - } - - if (controlsRef.current) { - const dragCallback = () => { - clearTimeout(debounceTimeoutId) - debounceTimeoutId = setTimeout(() => { - const [x, y, z] = getRotations() - const { position, dist } = getPositions() - - onCameraChange({ - position, - rotation: { x, y, z }, - dist, - }) - }, 400) - } - const dragStart = () => { - onDragStart() - clearTimeout(debounceTimeoutId) - } - controlsRef?.current?.addEventListener('end', dragCallback) - controlsRef?.current?.addEventListener('start', dragStart) - const oldCurrent = controlsRef.current - dragCallback() - return () => { - oldCurrent.removeEventListener('end', dragCallback) - oldCurrent.removeEventListener('start', dragStart) - } - } - }, [camera, controlsRef]) - - return ( - - ) -} - -function Box(props) { - // This reference will give us direct access to the mesh - const mesh = useRef() - - return ( - - - - - ) -} -function Sphere(props) { - const mesh = useRef() - return ( - - - - - ) -} - -export function PureIdeViewer({ - dataType, - artifact, - onInit, - onCameraChange, - isLoading, - isMinimal = false, - scadRatio = 1, -}: { - dataType: 'INIT' | ArtifactTypes - artifact: any - isLoading: boolean - onInit: Function - onCameraChange: Function - isMinimal?: boolean - scadRatio?: number -}) { - const [isDragging, setIsDragging] = useState(false) - const [image, setImage] = useState() - const controlsRef = useRef() - - useEffect(() => { - setImage(dataType === 'png' && artifact) - setIsDragging(false) - }, [dataType, artifact]) - - // the following are tailwind colors in hex, can't use these classes to color three.js meshes. - const pink400 = '#F472B6' - const indigo300 = '#A5B4FC' - const indigo900 = '#312E81' - const jscadLightIntensity = dataType === 'primitive-array' ? 0.5 : 1.1 - return ( -
- {image && ( -
- code-cad preview -
- )} -
setIsDragging(true)} - > - - setIsDragging(true)} - onInit={onInit} - onCameraChange={onCameraChange} - controlsRef={controlsRef} - /> - - - - - - - - {!isMinimal && ( - - - - )} - {dataType === 'png' && ( - <> - - - - - - )} - {dataType !== 'png' && artifact && ( - - - - )} - -
- - {!isMinimal && } -
- ) -} +import { PureIdeViewer } from './PureIdeViewer' const IdeViewer = ({ handleOwnCamera = false, @@ -347,7 +16,6 @@ const IdeViewer = ({ } const onCameraChange = (camera) => { if (handleOwnCamera) { - console.log('yo') return } thunkDispatch({ @@ -362,6 +30,7 @@ const IdeViewer = ({ state, dispatch, camera, + viewAll: state?.objectData?.type === 'INIT', }) } }) @@ -374,6 +43,7 @@ const IdeViewer = ({ onInit={onInit} onCameraChange={onCameraChange} isLoading={state.isLoading} + camera={state?.camera} /> ) } diff --git a/app/web/src/components/IdeViewer/PureIdeViewer.tsx b/app/web/src/components/IdeViewer/PureIdeViewer.tsx new file mode 100644 index 0000000..39b79d3 --- /dev/null +++ b/app/web/src/components/IdeViewer/PureIdeViewer.tsx @@ -0,0 +1,287 @@ +import * as THREE from 'three' +import { useRef, useState, useEffect, Suspense } from 'react' +import { Canvas, useThree } from '@react-three/fiber' +import { + PerspectiveCamera, + GizmoHelper, + GizmoViewport, + OrbitControls, +} from '@react-three/drei' +import { Vector3 } from 'three' +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' +import { Asset } from './Asset' + +function Controls({ + onCameraChange, + onDragStart, + onInit, + controlsRef, + camera: scadCamera, +}: { + onCameraChange: Function + onDragStart: () => void + onInit: Function + controlsRef: React.MutableRefObject + camera: State['camera'] +}) { + const debounceTimeoutId = useRef(0) + const isFirstCameraChange = useRef(true) + const threeInstance = useThree() + const { camera, gl } = threeInstance + useEffect(() => { + // setup three to openscad camera sync + + onInit(threeInstance) + // init camera position + 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() + + camera.rotation._order = 'ZYX' + const getRotations = (): number[] => { + const { x, y, z } = camera?.rotation || {} + return [x, y, z].map((rot) => (rot * 180) / Math.PI) + } + const getPositions = () => { + // Difficult to make this clean since I'm not sure why it works + // The OpenSCAD camera seems hard to work with but maybe it's just me + + // this gives us a vector the same length as the camera.position + const cameraViewVector = new Vector3(0, 0, 1) + .applyQuaternion(camera.quaternion) // make unit vector of the camera + .multiplyScalar(camera.position.length()) // make it the same length as the position vector + + // make a vector from the position vector to the cameraView vector + const head2Head = new Vector3().subVectors( + camera.position, + cameraViewVector + ) + const { x, y, z } = head2Head.add(camera.position) + return { + position: { x: x / 2, y: y / 2, z: z / 2 }, + dist: camera.position.length() / 2, + } + } + + if (controlsRef.current) { + const dragCallback = () => { + clearTimeout(debounceTimeoutId.current) + debounceTimeoutId.current = setTimeout(() => { + const [x, y, z] = getRotations() + const { position, dist } = getPositions() + + onCameraChange( + { + position, + rotation: { x, y, z }, + dist, + }, + isFirstCameraChange.current + ) + isFirstCameraChange.current = false + }, 400) as unknown as number + } + const dragStart = () => { + onDragStart() + clearTimeout(debounceTimeoutId.current) + } + controlsRef?.current?.addEventListener('end', dragCallback) + controlsRef?.current?.addEventListener('start', dragStart) + const oldCurrent = controlsRef.current + dragCallback() + return () => { + oldCurrent.removeEventListener('end', dragCallback) + oldCurrent.removeEventListener('start', dragStart) + } + } + }, [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 ( + + ) +} + +function Box(props) { + // This reference will give us direct access to the mesh + const mesh = useRef() + + return ( + + + + + ) +} +function Sphere(props) { + const mesh = useRef() + return ( + + + + + ) +} + +export function PureIdeViewer({ + dataType, + artifact, + onInit, + onCameraChange, + isLoading, + isMinimal = false, + scadRatio = 1, + camera, +}: { + dataType: 'INIT' | ArtifactTypes + artifact: any + isLoading: boolean + onInit: Function + onCameraChange: Function + isMinimal?: boolean + scadRatio?: number + camera?: State['camera'] +}) { + const [isDragging, setIsDragging] = useState(false) + const [image, setImage] = useState() + const controlsRef = useRef() + + useEffect(() => { + setImage(dataType === 'png' && artifact) + setIsDragging(false) + }, [dataType, artifact]) + + // the following are tailwind colors in hex, can't use these classes to color three.js meshes. + const pink400 = '#F472B6' + const indigo300 = '#A5B4FC' + const indigo900 = '#312E81' + const jscadLightIntensity = dataType === 'primitive-array' ? 0.5 : 1.1 + return ( +
+ {image && ( +
+ code-cad preview +
+ )} +
setIsDragging(true)} + > + + setIsDragging(true)} + onInit={onInit} + onCameraChange={onCameraChange} + controlsRef={controlsRef} + camera={camera} + /> + + + + + + + + {!isMinimal && ( + + + + )} + {dataType === 'png' && ( + <> + + + + + + )} + {dataType !== 'png' && artifact && ( + + + + )} + +
+ + {!isMinimal && } +
+ ) +} diff --git a/app/web/src/components/SocialCardCell/SocialCardCell.tsx b/app/web/src/components/SocialCardCell/SocialCardCell.tsx index 6341bc8..379a19a 100644 --- a/app/web/src/components/SocialCardCell/SocialCardCell.tsx +++ b/app/web/src/components/SocialCardCell/SocialCardCell.tsx @@ -35,10 +35,15 @@ export const Failure = ({ error }: CellFailureProps) => (
Error: {error.message}
) +interface SocialCardProps extends CellSuccessProps { + children: React.ReactNode +} + export const Success = ({ userProject, - variables: { image64, LiveProjectViewer }, -}: CellSuccessProps) => { + variables: { image64 }, + children, +}: SocialCardProps) => { const image = userProject?.Project?.mainImage const gravatar = userProject?.image const truncatedDescription = @@ -102,9 +107,9 @@ export const Success = ({ image64 && 'opacity-0' }`} > - {LiveProjectViewer && ( + {children && (
- + {children}
)} 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