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:
Pierre Jacquier
2023-03-08 05:45:25 -05:00
committed by GitHub
parent 40815479bf
commit f8b891cb47
17 changed files with 1974 additions and 740 deletions

2360
.pnp.cjs generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { CadDiff } from '../components/CadDiff' import { CadDiff } from '../components/diff/CadDiff'
import { Loading } from '../components/Loading' import { Loading } from '../components/Loading'
import { Commit, DiffEntry, FileDiff, Message, MessageIds, Pull } from './types' import { Commit, DiffEntry, FileDiff, Message, MessageIds, Pull } from './types'
import { import {

View File

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

View 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>
)
}

View 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>
</>
)
}

View 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}
/>
)
}

View 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>
)
}

View 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>
)
}

View File

@ -1,7 +1,7 @@
import { Box, ThemeProvider } from '@primer/react' import { Box, ThemeProvider } from '@primer/react'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { KittycadUser, MessageIds, User } from '../chrome/types' import { KittycadUser, MessageIds, User } from '../../chrome/types'
import { Loading } from './Loading' import { Loading } from '../Loading'
import { TokenForm } from './TokenForm' import { TokenForm } from './TokenForm'
import { UserCard } from './UserCard' import { UserCard } from './UserCard'

View File

@ -1,6 +1,6 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import { Settings } from './components/Settings' import { Settings } from './components/settings/Settings'
import './index.css' import './index.css'
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement) const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)

View File

@ -7366,7 +7366,7 @@ __metadata:
"fsevents@patch:fsevents@^2.3.2#~builtin<compat/fsevents>, fsevents@patch:fsevents@~2.3.2#~builtin<compat/fsevents>": "fsevents@patch:fsevents@^2.3.2#~builtin<compat/fsevents>, fsevents@patch:fsevents@~2.3.2#~builtin<compat/fsevents>":
version: 2.3.2 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: dependencies:
node-gyp: latest node-gyp: latest
conditions: os=darwin 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>": "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 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: dependencies:
is-core-module: ^2.9.0 is-core-module: ^2.9.0
path-parse: ^1.0.7 path-parse: ^1.0.7
@ -12212,7 +12212,7 @@ __metadata:
"resolve@patch:resolve@^2.0.0-next.4#~builtin<compat/resolve>": "resolve@patch:resolve@^2.0.0-next.4#~builtin<compat/resolve>":
version: 2.0.0-next.4 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: dependencies:
is-core-module: ^2.9.0 is-core-module: ^2.9.0
path-parse: ^1.0.7 path-parse: ^1.0.7
@ -13662,11 +13662,11 @@ __metadata:
"typescript@patch:typescript@^4.4.2#~builtin<compat/typescript>": "typescript@patch:typescript@^4.4.2#~builtin<compat/typescript>":
version: 4.9.5 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: bin:
tsc: bin/tsc tsc: bin/tsc
tsserver: bin/tsserver tsserver: bin/tsserver
checksum: 2eee5c37cad4390385db5db5a8e81470e42e8f1401b0358d7390095d6f681b410f2c4a0c496c6ff9ebd775423c7785cdace7bcdad76c7bee283df3d9718c0f20 checksum: ab417a2f398380c90a6cf5a5f74badd17866adf57f1165617d6a551f059c3ba0a3e4da0d147b3ac5681db9ac76a303c5876394b13b3de75fdd5b1eaa06181c9d
languageName: node languageName: node
linkType: hard linkType: hard