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 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 {
|
||||||
|
@ -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 { 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'
|
||||||
|
|
@ -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)
|
||||||
|
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>":
|
"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
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user