Zoom to fit for openscad (#569)

* Add viewall flag to openscad cli in prep for zoom to fit for scad previews

* Fix remaining issues with social image capture
This commit was merged in pull request #569.
This commit is contained in:
Kurt Hutten
2021-11-06 09:46:55 +11:00
committed by GitHub
parent a909188f15
commit 43fc897bf9
12 changed files with 473 additions and 369 deletions

View File

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

View File

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

View File

@@ -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<State['camera'] | null>(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}
</SocialCardCell>
</div>
</div>
</div>

View File

@@ -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<any> // 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) => (
<primitive object={mesh} key={index} />
))
return (
<group dispose={null}>
<mesh ref={mesh} scale={[1, 1, 1]} geometry={incomingGeo}>
<meshPhysicalMaterial
envMapIntensity={0.1}
color="#F472B6"
map={colorMap}
clearcoat={0.1}
clearcoatRoughness={0.2}
roughness={10}
metalness={0.7}
smoothShading
/>
</mesh>
<lineSegments geometry={edges} renderOrder={100}>
<lineBasicMaterial color="#aaaaff" opacity={0.5} transparent />
</lineSegments>
</group>
)
}

View File

@@ -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<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])
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) => (
<primitive object={mesh} key={index} />
))
return (
<group dispose={null}>
<mesh ref={mesh} scale={[1, 1, 1]} geometry={incomingGeo}>
<meshPhysicalMaterial
envMapIntensity={0.1}
color="#F472B6"
map={colorMap}
clearcoat={0.1}
clearcoatRoughness={0.2}
roughness={10}
metalness={0.7}
smoothShading
/>
</mesh>
<lineSegments geometry={edges} renderOrder={100}>
<lineBasicMaterial color="#aaaaff" opacity={0.5} transparent />
</lineSegments>
</group>
)
}
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 (
<OrbitControls
makeDefault
ref={controlsRef}
args={[camera, gl.domElement]}
/>
)
}
function Box(props) {
// This reference will give us direct access to the mesh
const mesh = useRef()
return (
<mesh {...props} ref={mesh} scale={[1, 1, 1]}>
<boxBufferGeometry args={props.size} />
<meshStandardMaterial color={props.color} />
</mesh>
)
}
function Sphere(props) {
const mesh = useRef()
return (
<mesh {...props} ref={mesh} scale={[1, 1, 1]}>
<sphereBufferGeometry args={[2, 30, 30]} />
<meshStandardMaterial color={props.color} />
</mesh>
)
}
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<any>()
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 (
<div className="relative h-full bg-ch-gray-800 cursor-grab">
{image && (
<div
className={`absolute inset-0 transition-opacity duration-500 ${
isDragging ? 'opacity-25' : 'opacity-100'
}`}
style={{
transform: `translate(${
scadRatio !== 1 ? '-250px, -261px' : '0px, 0px'
})`,
}}
>
<img
alt="code-cad preview"
id="special"
src={URL.createObjectURL(image)}
className="h-full w-full"
/>
</div>
)}
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
className={`opacity-0 absolute inset-0 transition-opacity duration-500 ${
!(isDragging || dataType !== 'png')
? 'hover:opacity-50'
: 'opacity-100'
}`}
onMouseDown={() => setIsDragging(true)}
>
<Canvas linear={true} dpr={[1, 2]}>
<Controls
onDragStart={() => setIsDragging(true)}
onInit={onInit}
onCameraChange={onCameraChange}
controlsRef={controlsRef}
/>
<PerspectiveCamera makeDefault up={[0, 0, 1]}>
<pointLight
position={[0, 0, 100]}
intensity={jscadLightIntensity}
/>
</PerspectiveCamera>
<ambientLight intensity={2 * jscadLightIntensity} />
<pointLight
position={[-1000, -1000, -1000]}
color="#5555FF"
intensity={1 * jscadLightIntensity}
/>
<pointLight
position={[-1000, 0, 1000]}
color="#5555FF"
intensity={1 * jscadLightIntensity}
/>
<gridHelper
args={[200, 20, 0xff5555, 0x555555]}
material-opacity={0.2}
material-transparent
rotation-x={Math.PI / 2}
/>
{!isMinimal && (
<GizmoHelper alignment={'top-left'} margin={[80, 80]}>
<GizmoViewport
axisColors={['red', 'green', 'blue']}
labelColor="black"
/>
</GizmoHelper>
)}
{dataType === 'png' && (
<>
<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} />
</>
)}
{dataType !== 'png' && artifact && (
<Suspense fallback={null}>
<Asset
geometry={artifact}
dataType={dataType}
controlsRef={controlsRef}
/>
</Suspense>
)}
</Canvas>
</div>
<DelayedPingAnimation isLoading={isLoading} />
{!isMinimal && <Customizer />}
</div>
)
}
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}
/>
)
}

View File

@@ -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<any>
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 (
<OrbitControls
makeDefault
ref={controlsRef}
args={[camera, gl.domElement]}
/>
)
}
function Box(props) {
// This reference will give us direct access to the mesh
const mesh = useRef()
return (
<mesh {...props} ref={mesh} scale={[1, 1, 1]}>
<boxBufferGeometry args={props.size} />
<meshStandardMaterial color={props.color} />
</mesh>
)
}
function Sphere(props) {
const mesh = useRef()
return (
<mesh {...props} ref={mesh} scale={[1, 1, 1]}>
<sphereBufferGeometry args={[2, 30, 30]} />
<meshStandardMaterial color={props.color} />
</mesh>
)
}
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<any>()
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 (
<div className="relative h-full bg-ch-gray-800 cursor-grab">
{image && (
<div
className={`absolute inset-0 transition-opacity duration-500 ${
isDragging ? 'opacity-25' : 'opacity-100'
}`}
style={{
transform: `translate(${
scadRatio !== 1 ? '-250px, -261px' : '0px, 0px'
})`,
}}
>
<img
alt="code-cad preview"
id="special"
src={URL.createObjectURL(image)}
className="h-full w-full"
/>
</div>
)}
<div // eslint-disable-line jsx-a11y/no-static-element-interactions
className={`opacity-0 absolute inset-0 transition-opacity duration-500 ${
!(isDragging || dataType !== 'png')
? 'hover:opacity-50'
: 'opacity-100'
}`}
onMouseDown={() => setIsDragging(true)}
>
<Canvas linear={true} dpr={[1, 2]}>
<Controls
onDragStart={() => setIsDragging(true)}
onInit={onInit}
onCameraChange={onCameraChange}
controlsRef={controlsRef}
camera={camera}
/>
<PerspectiveCamera makeDefault up={[0, 0, 1]}>
<pointLight
position={[0, 0, 100]}
intensity={jscadLightIntensity}
/>
</PerspectiveCamera>
<ambientLight intensity={2 * jscadLightIntensity} />
<pointLight
position={[-1000, -1000, -1000]}
color="#5555FF"
intensity={1 * jscadLightIntensity}
/>
<pointLight
position={[-1000, 0, 1000]}
color="#5555FF"
intensity={1 * jscadLightIntensity}
/>
<gridHelper
args={[200, 20, 0xff5555, 0x555555]}
material-opacity={0.2}
material-transparent
rotation-x={Math.PI / 2}
/>
{!isMinimal && (
<GizmoHelper alignment={'top-left'} margin={[80, 80]}>
<GizmoViewport
axisColors={['red', 'green', 'blue']}
labelColor="black"
/>
</GizmoHelper>
)}
{dataType === 'png' && (
<>
<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} />
</>
)}
{dataType !== 'png' && artifact && (
<Suspense fallback={null}>
<Asset
geometry={artifact}
dataType={dataType}
controlsRef={controlsRef}
/>
</Suspense>
)}
</Canvas>
</div>
<DelayedPingAnimation isLoading={isLoading} />
{!isMinimal && <Customizer />}
</div>
)
}

View File

@@ -35,10 +35,15 @@ export const Failure = ({ error }: CellFailureProps) => (
<div style={{ color: 'red' }}>Error: {error.message}</div>
)
interface SocialCardProps extends CellSuccessProps<FindSocialCardQuery> {
children: React.ReactNode
}
export const Success = ({
userProject,
variables: { image64, LiveProjectViewer },
}: CellSuccessProps<FindSocialCardQuery>) => {
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 && (
<div className="w-full h-full" id="social-card-canvas">
<LiveProjectViewer />
{children}
</div>
)}
</div>

View File

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

View File

@@ -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<RenderResponse> => {
const response: Promise<RenderResponse> = new Promise(
(resolve: ResolveFn) => {

View File

@@ -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 || []),
})

View File

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

View File

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