Stealing the website's viewer (#15)
* Stealing the website's viewer * Cleaned up, diff colors * fix test * Clean up unused logic * Clean up
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { CadDiff } from '../components/CadDiff'
|
||||
import { CadDiff } from '../components/diff/CadDiff'
|
||||
import { Loading } from '../components/Loading'
|
||||
import { Commit, DiffEntry, FileDiff, Message, MessageIds, Pull } from './types'
|
||||
import {
|
||||
|
@ -1,52 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import '@react-three/fiber'
|
||||
import { OrbitControls } from '@react-three/drei'
|
||||
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
|
||||
import { BufferGeometry } from 'three'
|
||||
import { Canvas } from '@react-three/fiber'
|
||||
import { Box, ThemeProvider, useTheme } from '@primer/react'
|
||||
import { FileDiff } from '../chrome/types'
|
||||
|
||||
function ModelView({ file }: { file: string }): React.ReactElement {
|
||||
const { theme } = useTheme()
|
||||
const [geometry, setGeometry] = useState<BufferGeometry>()
|
||||
useEffect(() => {
|
||||
const loader = new STLLoader()
|
||||
const geometry = loader.parse(atob(file))
|
||||
console.log(`Model ${geometry.id} loaded`)
|
||||
setGeometry(geometry)
|
||||
}, [file])
|
||||
return (
|
||||
<Canvas>
|
||||
<ambientLight intensity={0.7} />
|
||||
<pointLight position={[10, 10, 10]} />
|
||||
<mesh geometry={geometry}>
|
||||
<meshStandardMaterial color={theme?.colors.fg.default} />
|
||||
</mesh>
|
||||
<OrbitControls />
|
||||
</Canvas>
|
||||
)
|
||||
}
|
||||
|
||||
export type CadDiffProps = FileDiff
|
||||
|
||||
export function CadDiff({ before, after }: CadDiffProps): React.ReactElement {
|
||||
return (
|
||||
<ThemeProvider colorMode="auto">
|
||||
<Box display="flex" height={300}>
|
||||
<Box flexGrow={1} backgroundColor="danger.subtle">
|
||||
{before && <ModelView file={before} />}
|
||||
</Box>
|
||||
<Box
|
||||
flexGrow={1}
|
||||
backgroundColor="success.subtle"
|
||||
borderLeftWidth={1}
|
||||
borderLeftColor="border.default"
|
||||
borderLeftStyle="solid"
|
||||
>
|
||||
{after && <ModelView file={after} />}
|
||||
</Box>
|
||||
</Box>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
70
src/components/diff/CadDiff.tsx
Normal file
70
src/components/diff/CadDiff.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import '@react-three/fiber'
|
||||
import { Box, ThemeProvider, useTheme } from '@primer/react'
|
||||
import { FileDiff } from '../../chrome/types'
|
||||
import { Viewer3D } from './Viewer3D'
|
||||
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader'
|
||||
import { BufferGeometry } from 'three'
|
||||
import { WireframeColors } from './WireframeModel'
|
||||
|
||||
type ViewerSTLProps = {
|
||||
file: string
|
||||
colors: WireframeColors
|
||||
}
|
||||
|
||||
function ViewerSTL({ file, colors }: ViewerSTLProps) {
|
||||
const [geomety, setGeometry] = useState<BufferGeometry>()
|
||||
useEffect(() => {
|
||||
const loader = new STLLoader()
|
||||
const buffer = window.atob(file)
|
||||
const geometry = loader.parse(buffer)
|
||||
console.log(`Model ${geometry.id} loaded`)
|
||||
setGeometry(geometry)
|
||||
}, [file])
|
||||
return geomety ? <Viewer3D geometry={geomety} colors={colors} /> : null
|
||||
}
|
||||
|
||||
type CadDiffThemedProps = FileDiff
|
||||
|
||||
function CadDiffInternals({
|
||||
before,
|
||||
after,
|
||||
}: CadDiffThemedProps): React.ReactElement {
|
||||
const { theme } = useTheme()
|
||||
const beforeColors: WireframeColors = {
|
||||
face: theme?.colors.fg.default,
|
||||
edge: theme?.colors.danger.muted,
|
||||
dashEdge: theme?.colors.danger.subtle,
|
||||
}
|
||||
const afterColors: WireframeColors = {
|
||||
face: theme?.colors.fg.default,
|
||||
edge: theme?.colors.success.muted,
|
||||
dashEdge: theme?.colors.success.subtle,
|
||||
}
|
||||
return (
|
||||
<Box display="flex" height={300}>
|
||||
<Box flexGrow={1} backgroundColor="danger.subtle">
|
||||
{before && <ViewerSTL file={before} colors={beforeColors} />}
|
||||
</Box>
|
||||
<Box
|
||||
flexGrow={1}
|
||||
backgroundColor="success.subtle"
|
||||
borderLeftWidth={1}
|
||||
borderLeftColor="border.default"
|
||||
borderLeftStyle="solid"
|
||||
>
|
||||
{after && <ViewerSTL file={after} colors={afterColors} />}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export type CadDiffProps = FileDiff
|
||||
|
||||
export function CadDiff({ before, after }: CadDiffProps): React.ReactElement {
|
||||
return (
|
||||
<ThemeProvider colorMode="auto">
|
||||
<CadDiffInternals before={before} after={after} />
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
78
src/components/diff/Camera.tsx
Normal file
78
src/components/diff/Camera.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
import { OrthographicCamera } from '@react-three/drei'
|
||||
import { useThree } from '@react-three/fiber'
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import { BufferGeometry } from 'three'
|
||||
|
||||
function CameraLighting({ geometry }: Props) {
|
||||
const ref1 = useRef<any>()
|
||||
const ref2 = useRef<any>()
|
||||
useEffect(() => {
|
||||
if (geometry && ref1.current) {
|
||||
geometry.computeBoundingSphere()
|
||||
const { radius } = geometry.boundingSphere || { radius: 1 }
|
||||
// move spot light away relative to the object's size
|
||||
ref1.current.position.setLength(radius * 15)
|
||||
}
|
||||
}, [geometry])
|
||||
return (
|
||||
<>
|
||||
<spotLight
|
||||
ref={ref1}
|
||||
position={[20, 20, 5]}
|
||||
angle={(8 * Math.PI) / 180}
|
||||
intensity={4}
|
||||
castShadow
|
||||
shadow-mapSize={[1024, 1024]}
|
||||
shadowCameraNear={1}
|
||||
/>
|
||||
<spotLight
|
||||
ref={ref2}
|
||||
position={[0, 0, 0]}
|
||||
angle={0.8}
|
||||
intensity={1.8}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function calculateFovFactor(fov: number, canvasHeight: number): number {
|
||||
const pixelsFromCenterToTop = canvasHeight / 2
|
||||
// Only interested in the angle from the center to the top of frame
|
||||
const deg2Rad = Math.PI / 180
|
||||
const halfFovRadians = (fov * deg2Rad) / 2
|
||||
return pixelsFromCenterToTop / Math.tan(halfFovRadians)
|
||||
}
|
||||
|
||||
type Props = {
|
||||
geometry: BufferGeometry
|
||||
}
|
||||
|
||||
export function Camera({ geometry }: Props) {
|
||||
const fov = 15
|
||||
const persRef = useRef<any>(null)
|
||||
const orthoRef = useRef<any>(null)
|
||||
const canvasHeight = useThree(state => state.size.height)
|
||||
const [isFirstRender, setIsFirstRender] = useState(true)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const fovFactor = calculateFovFactor(fov, canvasHeight)
|
||||
if (!persRef.current || !orthoRef.current) return
|
||||
if (isFirstRender) {
|
||||
setIsFirstRender(false)
|
||||
return
|
||||
}
|
||||
setTimeout(() => {
|
||||
orthoRef.current.position.copy(persRef.current.position.clone())
|
||||
orthoRef.current.zoom =
|
||||
fovFactor / orthoRef.current.position.length()
|
||||
orthoRef.current.updateProjectionMatrix()
|
||||
})
|
||||
}, [canvasHeight, orthoRef, isFirstRender])
|
||||
return (
|
||||
<>
|
||||
<OrthographicCamera ref={orthoRef} makeDefault>
|
||||
<CameraLighting geometry={geometry} />
|
||||
</OrthographicCamera>
|
||||
</>
|
||||
)
|
||||
}
|
19
src/components/diff/CameraControls.tsx
Normal file
19
src/components/diff/CameraControls.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { OrbitControls } from '@react-three/drei'
|
||||
import { useThree } from '@react-three/fiber'
|
||||
|
||||
type Props = {
|
||||
cameraRef: any
|
||||
}
|
||||
|
||||
export function CameraControls({ cameraRef }: Props) {
|
||||
const camera = useThree(s => s.camera)
|
||||
const gl = useThree(s => s.gl)
|
||||
return (
|
||||
<OrbitControls
|
||||
makeDefault
|
||||
ref={cameraRef}
|
||||
args={[camera, gl.domElement]}
|
||||
enablePan={false}
|
||||
/>
|
||||
)
|
||||
}
|
29
src/components/diff/Viewer3D.tsx
Normal file
29
src/components/diff/Viewer3D.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { useRef } from 'react'
|
||||
import '@react-three/fiber'
|
||||
import { BufferGeometry } from 'three'
|
||||
import { Canvas } from '@react-three/fiber'
|
||||
import { WireframeColors, WireframeModel } from './WireframeModel'
|
||||
import { Camera } from './Camera'
|
||||
import { CameraControls } from './CameraControls'
|
||||
|
||||
type Props = {
|
||||
geometry: BufferGeometry
|
||||
colors: WireframeColors
|
||||
}
|
||||
|
||||
export function Viewer3D({ geometry, colors }: Props) {
|
||||
const cameraRef = useRef<any>()
|
||||
return (
|
||||
<Canvas dpr={[1, 2]} shadows>
|
||||
{typeof window !== 'undefined' && geometry && (
|
||||
<WireframeModel
|
||||
geometry={geometry}
|
||||
cameraRef={cameraRef}
|
||||
colors={colors}
|
||||
/>
|
||||
)}
|
||||
<CameraControls cameraRef={cameraRef} />
|
||||
{geometry && <Camera geometry={geometry} />}
|
||||
</Canvas>
|
||||
)
|
||||
}
|
88
src/components/diff/WireframeModel.tsx
Normal file
88
src/components/diff/WireframeModel.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import { useThree } from '@react-three/fiber'
|
||||
import type { MutableRefObject } from 'react'
|
||||
import { Suspense, useEffect, useMemo, useRef } from 'react'
|
||||
import { BufferGeometry, DoubleSide } from 'three'
|
||||
import { EdgesGeometry, Vector3 } from 'three'
|
||||
import { calculateFovFactor } from './Camera'
|
||||
|
||||
export type WireframeColors = {
|
||||
face: string
|
||||
edge: string
|
||||
dashEdge: string
|
||||
}
|
||||
|
||||
type Props = {
|
||||
cameraRef: MutableRefObject<any>
|
||||
geometry: BufferGeometry
|
||||
colors: WireframeColors
|
||||
}
|
||||
|
||||
export function WireframeModel({ geometry, cameraRef, colors }: Props) {
|
||||
const groupRef = useRef<any>()
|
||||
const camera = useThree(state => state.camera)
|
||||
const canvasHeight = useThree(state => state.size.height)
|
||||
|
||||
// Camera view
|
||||
useEffect(() => {
|
||||
if (geometry && cameraRef.current) {
|
||||
geometry.computeBoundingSphere()
|
||||
geometry.center()
|
||||
|
||||
// move the camera away so the object fits in the view
|
||||
const { radius } = geometry.boundingSphere || { radius: 1 }
|
||||
if (!camera.position.length()) {
|
||||
const arbitraryNonZeroStartPosition = new Vector3(0.5, 0.5, 1)
|
||||
camera.position.copy(arbitraryNonZeroStartPosition)
|
||||
}
|
||||
const initialZoomOffset = 7.5
|
||||
camera.position.setLength(radius * initialZoomOffset)
|
||||
|
||||
// set zoom for orthographic Camera
|
||||
const fov = 15 // TODO fov shouldn't be hardcoded
|
||||
const fovFactor = calculateFovFactor(fov, canvasHeight)
|
||||
camera.zoom = fovFactor / camera.position.length()
|
||||
camera.updateProjectionMatrix()
|
||||
}
|
||||
}, [geometry, camera, cameraRef, canvasHeight])
|
||||
|
||||
// Edges for wireframe
|
||||
const edgeThresholdAngle = 10
|
||||
const edges = useMemo(
|
||||
() => new EdgesGeometry(geometry.center(), edgeThresholdAngle),
|
||||
[geometry]
|
||||
)
|
||||
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<group ref={groupRef}>
|
||||
<mesh
|
||||
castShadow={true}
|
||||
receiveShadow={true}
|
||||
geometry={geometry}
|
||||
>
|
||||
<meshBasicMaterial
|
||||
color={colors.face}
|
||||
side={DoubleSide}
|
||||
depthTest={true}
|
||||
/>
|
||||
</mesh>
|
||||
<lineSegments
|
||||
geometry={edges}
|
||||
renderOrder={100}
|
||||
onUpdate={line => line.computeLineDistances()}
|
||||
>
|
||||
<lineDashedMaterial
|
||||
color={colors.dashEdge}
|
||||
dashSize={5}
|
||||
gapSize={4}
|
||||
scale={40}
|
||||
depthTest={false}
|
||||
/>
|
||||
</lineSegments>
|
||||
<lineSegments geometry={edges} renderOrder={100}>
|
||||
<lineBasicMaterial color={colors.edge} depthTest={true} />
|
||||
</lineSegments>
|
||||
</group>
|
||||
</Suspense>
|
||||
)
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { Box, ThemeProvider } from '@primer/react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { KittycadUser, MessageIds, User } from '../chrome/types'
|
||||
import { Loading } from './Loading'
|
||||
import { KittycadUser, MessageIds, User } from '../../chrome/types'
|
||||
import { Loading } from '../Loading'
|
||||
import { TokenForm } from './TokenForm'
|
||||
import { UserCard } from './UserCard'
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import { Settings } from './components/Settings'
|
||||
import { Settings } from './components/settings/Settings'
|
||||
import './index.css'
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
|
||||
|
10
yarn.lock
10
yarn.lock
@ -7366,7 +7366,7 @@ __metadata:
|
||||
|
||||
"fsevents@patch:fsevents@^2.3.2#~builtin<compat/fsevents>, fsevents@patch:fsevents@~2.3.2#~builtin<compat/fsevents>":
|
||||
version: 2.3.2
|
||||
resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin<compat/fsevents>::version=2.3.2&hash=18f3a7"
|
||||
resolution: "fsevents@patch:fsevents@npm%3A2.3.2#~builtin<compat/fsevents>::version=2.3.2&hash=df0bf1"
|
||||
dependencies:
|
||||
node-gyp: latest
|
||||
conditions: os=darwin
|
||||
@ -12199,7 +12199,7 @@ __metadata:
|
||||
|
||||
"resolve@patch:resolve@^1.1.7#~builtin<compat/resolve>, resolve@patch:resolve@^1.14.2#~builtin<compat/resolve>, resolve@patch:resolve@^1.19.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.20.0#~builtin<compat/resolve>, resolve@patch:resolve@^1.22.1#~builtin<compat/resolve>":
|
||||
version: 1.22.1
|
||||
resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin<compat/resolve>::version=1.22.1&hash=07638b"
|
||||
resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin<compat/resolve>::version=1.22.1&hash=c3c19d"
|
||||
dependencies:
|
||||
is-core-module: ^2.9.0
|
||||
path-parse: ^1.0.7
|
||||
@ -12212,7 +12212,7 @@ __metadata:
|
||||
|
||||
"resolve@patch:resolve@^2.0.0-next.4#~builtin<compat/resolve>":
|
||||
version: 2.0.0-next.4
|
||||
resolution: "resolve@patch:resolve@npm%3A2.0.0-next.4#~builtin<compat/resolve>::version=2.0.0-next.4&hash=07638b"
|
||||
resolution: "resolve@patch:resolve@npm%3A2.0.0-next.4#~builtin<compat/resolve>::version=2.0.0-next.4&hash=c3c19d"
|
||||
dependencies:
|
||||
is-core-module: ^2.9.0
|
||||
path-parse: ^1.0.7
|
||||
@ -13662,11 +13662,11 @@ __metadata:
|
||||
|
||||
"typescript@patch:typescript@^4.4.2#~builtin<compat/typescript>":
|
||||
version: 4.9.5
|
||||
resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin<compat/typescript>::version=4.9.5&hash=a1c5e5"
|
||||
resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin<compat/typescript>::version=4.9.5&hash=23ec76"
|
||||
bin:
|
||||
tsc: bin/tsc
|
||||
tsserver: bin/tsserver
|
||||
checksum: 2eee5c37cad4390385db5db5a8e81470e42e8f1401b0358d7390095d6f681b410f2c4a0c496c6ff9ebd775423c7785cdace7bcdad76c7bee283df3d9718c0f20
|
||||
checksum: ab417a2f398380c90a6cf5a5f74badd17866adf57f1165617d6a551f059c3ba0a3e4da0d147b3ac5681db9ac76a303c5876394b13b3de75fdd5b1eaa06181c9d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
Reference in New Issue
Block a user