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' import { State } from 'src/helpers/hooks/useIdeState' const thresholdAngle = 12 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 ( ) } let debounceTimeoutId 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 = 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) 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]) 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 && }
) } const IdeViewer = ({ handleOwnCamera = false, }: { handleOwnCamera?: boolean }) => { const { state, thunkDispatch } = useIdeContext() const dataType = state.objectData?.type const artifact = state.objectData?.data const onInit = (threeInstance) => { thunkDispatch({ type: 'setThreeInstance', payload: threeInstance }) } const onCameraChange = (camera) => { if (handleOwnCamera) { return } thunkDispatch({ type: 'updateCamera', payload: { camera }, }) thunkDispatch((dispatch, getState) => { const state = getState() if (['png', 'INIT'].includes(state?.objectData?.type)) { dispatch({ type: 'setLoading' }) requestRender({ state, dispatch, camera, viewAll: state?.objectData?.type === 'INIT', }) } }) } return ( ) } export default IdeViewer