More centering fixes (#200)

* More centering fixes

* Fix types

* Break out functions and add tests

* Clean up and lint

* Add missing import

* Add added/removed boundingsphere defaults

* Update darwin e2e snapshots

* Update linux e2e snapshots

* Update names

* Better error message

* Update e2e darwin

* Update e2e linux

* Consistency tweak

* Consistency tweak
This commit is contained in:
Pierre Jacquier
2023-06-08 17:51:48 -04:00
committed by GitHub
parent de54658f75
commit 1f62e6f34c
14 changed files with 322 additions and 163 deletions

View File

@ -1,54 +1,106 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import '@react-three/fiber' import '@react-three/fiber'
import { Box, useTheme, Text, TabNav, StyledOcticon } from '@primer/react' import { Box, Text, useTheme, TabNav, StyledOcticon } from '@primer/react'
import { FileDiff } from '../../chrome/types' import { FileDiff } from '../../chrome/types'
import { Viewer3D } from './Viewer3D' import { Viewer3D } from './Viewer3D'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader' import { BufferGeometry, Sphere } from 'three'
import { BufferAttribute, BufferGeometry, Mesh } from 'three'
import { WireframeColors, WireframeModel } from './WireframeModel' import { WireframeColors, WireframeModel } from './WireframeModel'
import { Buffer } from 'buffer'
import { useRef } from 'react' import { useRef } from 'react'
import { UnifiedModel } from './UnifiedModel' import { CombinedModel } from './CombinedModel'
import { BeakerIcon } from '@primer/octicons-react' import { BeakerIcon } from '@primer/octicons-react'
import { LegendBox, LegendLabel } from './Legend' import { LegendBox, LegendLabel } from './Legend'
import { getCommonSphere, loadGeometry } from '../../utils/three'
function loadGeometry(file: string, checkUV = false): BufferGeometry { function Viewer3D2Up({
const loader = new OBJLoader() beforeGeometry,
const buffer = Buffer.from(file, 'base64').toString() afterGeometry,
const group = loader.parse(buffer) boundingSphere,
console.log(`Model ${group.id} loaded`) }: {
const geometry = (group.children[0] as Mesh)?.geometry beforeGeometry?: BufferGeometry
if (checkUV && !geometry.attributes.uv) { afterGeometry?: BufferGeometry
// UV is needed for @react-three/csg boundingSphere?: Sphere
// see: github.com/KittyCAD/diff-viewer-extension/issues/73 }) {
geometry.setAttribute( const cameraRef = useRef<any>()
'uv', const { theme } = useTheme()
new BufferAttribute(new Float32Array([]), 1) const beforeColors: WireframeColors = {
) face: theme?.colors.fg.default,
edge: theme?.colors.danger.muted,
dashEdge: theme?.colors.danger.subtle,
} }
geometry.computeBoundingSphere() // will be used for auto-centering const afterColors: WireframeColors = {
return geometry face: theme?.colors.fg.default,
edge: theme?.colors.success.muted,
dashEdge: theme?.colors.success.subtle,
}
return (
<>
{beforeGeometry && (
<Box flexGrow={1} minWidth={0} backgroundColor="danger.subtle">
<Viewer3D
cameraRef={cameraRef}
geometry={beforeGeometry}
boundingSphere={boundingSphere}
>
<WireframeModel
geometry={beforeGeometry}
boundingSphere={boundingSphere}
cameraRef={cameraRef}
colors={beforeColors}
/>
</Viewer3D>
</Box>
)}
{afterGeometry && (
<Box
flexGrow={1}
minWidth={0}
backgroundColor="success.subtle"
borderLeftWidth={1}
borderLeftColor="border.default"
borderLeftStyle="solid"
>
<Viewer3D
cameraRef={cameraRef}
geometry={afterGeometry}
boundingSphere={boundingSphere}
>
<WireframeModel
geometry={afterGeometry}
boundingSphere={boundingSphere}
cameraRef={cameraRef}
colors={afterColors}
/>
</Viewer3D>
</Box>
)}
</>
)
} }
function Loader3DUnified({ before, after }: { before: string; after: string }) { function Viewer3DCombined({
beforeGeometry,
afterGeometry,
boundingSphere,
}: {
beforeGeometry: BufferGeometry
afterGeometry: BufferGeometry
boundingSphere: Sphere
}) {
const cameraRef = useRef<any>()
const [showUnchanged, setShowUnchanged] = useState(true) const [showUnchanged, setShowUnchanged] = useState(true)
const [showAdditions, setShowAdditions] = useState(true) const [showAdditions, setShowAdditions] = useState(true)
const [showDeletions, setShowDeletions] = useState(true) const [showDeletions, setShowDeletions] = useState(true)
const cameraRef = useRef<any>() return (
const [beforeGeometry, setBeforeGeometry] = useState<BufferGeometry>()
const [afterGeometry, setAfterGeometry] = useState<BufferGeometry>()
useEffect(() => {
setBeforeGeometry(loadGeometry(before, true))
}, [before])
useEffect(() => {
setAfterGeometry(loadGeometry(after, true))
}, [after])
return beforeGeometry && afterGeometry ? (
<> <>
<Viewer3D cameraRef={cameraRef} geometry={beforeGeometry}> <Viewer3D
<UnifiedModel cameraRef={cameraRef}
geometry={beforeGeometry}
boundingSphere={boundingSphere}
>
<CombinedModel
beforeGeometry={beforeGeometry} beforeGeometry={beforeGeometry}
afterGeometry={afterGeometry} afterGeometry={afterGeometry}
boundingSphere={boundingSphere}
cameraRef={cameraRef} cameraRef={cameraRef}
showUnchanged={showUnchanged} showUnchanged={showUnchanged}
showAdditions={showAdditions} showAdditions={showAdditions}
@ -76,87 +128,67 @@ function Loader3DUnified({ before, after }: { before: string; after: string }) {
/> />
</LegendBox> </LegendBox>
</> </>
) : (
<Box p={3}>
<Text>Sorry, the rich diff can't be displayed for this file.</Text>
</Box>
)
}
function Loader3D({ file, colors }: { file: string; colors: WireframeColors }) {
const cameraRef = useRef<any>()
const [geometry, setGeometry] = useState<BufferGeometry>()
useEffect(() => {
setGeometry(loadGeometry(file))
}, [file])
return geometry ? (
<Viewer3D cameraRef={cameraRef} geometry={geometry}>
<WireframeModel
geometry={geometry}
cameraRef={cameraRef}
colors={colors}
/>
</Viewer3D>
) : (
<Box p={3}>
<Text>Sorry, the rich diff can't be displayed for this file.</Text>
</Box>
) )
} }
export function CadDiff({ before, after }: FileDiff): React.ReactElement { export function CadDiff({ before, after }: FileDiff): React.ReactElement {
const canShowUnified = before && after let [showCombined, setShowCombined] = useState(false)
let [showUnified, setShowUnified] = useState(false) const [beforeGeometry, setBeforeGeometry] = useState<BufferGeometry>()
const { theme } = useTheme() const [afterGeometry, setAfterGeometry] = useState<BufferGeometry>()
const beforeColors: WireframeColors = { const [boundingSphere, setBoundingSphere] = useState<Sphere>()
face: theme?.colors.fg.default, useEffect(() => {
edge: theme?.colors.danger.muted, let beforeGeometry: BufferGeometry | undefined = undefined
dashEdge: theme?.colors.danger.subtle, let afterGeometry: BufferGeometry | undefined = undefined
} if (before) {
const afterColors: WireframeColors = { beforeGeometry = loadGeometry(before)
face: theme?.colors.fg.default, setBeforeGeometry(beforeGeometry)
edge: theme?.colors.success.muted, }
dashEdge: theme?.colors.success.subtle, if (after) {
} afterGeometry = loadGeometry(after)
setAfterGeometry(afterGeometry)
}
if (beforeGeometry && afterGeometry) {
const boundingSphere = getCommonSphere(
beforeGeometry,
afterGeometry
)
setBoundingSphere(boundingSphere)
} else if (beforeGeometry && beforeGeometry.boundingSphere) {
setBoundingSphere(beforeGeometry.boundingSphere)
} else if (afterGeometry && afterGeometry.boundingSphere) {
setBoundingSphere(afterGeometry.boundingSphere)
}
}, [before, after])
return ( return (
<> <>
<Box {(beforeGeometry || afterGeometry) && (
display="flex" <Box
height={300} display="flex"
overflow="hidden" height={300}
minWidth={0} overflow="hidden"
position="relative" minWidth={0}
> position="relative"
{canShowUnified && showUnified && ( >
<Loader3DUnified before={before} after={after} /> {beforeGeometry &&
)} afterGeometry &&
{!showUnified && ( boundingSphere &&
<> showCombined && (
{before && ( <Viewer3DCombined
<Box beforeGeometry={beforeGeometry}
flexGrow={1} afterGeometry={afterGeometry}
minWidth={0} boundingSphere={boundingSphere}
backgroundColor="danger.subtle" />
>
<Loader3D file={before} colors={beforeColors} />
</Box>
)} )}
{after && ( {!showCombined && (
<Box <Viewer3D2Up
flexGrow={1} beforeGeometry={beforeGeometry}
minWidth={0} afterGeometry={afterGeometry}
backgroundColor="success.subtle" boundingSphere={boundingSphere}
borderLeftWidth={1} />
borderLeftColor="border.default" )}
borderLeftStyle="solid" </Box>
> )}
<Loader3D file={after} colors={afterColors} /> {beforeGeometry && afterGeometry && boundingSphere && (
</Box>
)}
</>
)}
</Box>
{canShowUnified && (
<Box <Box
pt={2} pt={2}
backgroundColor="canvas.default" backgroundColor="canvas.default"
@ -177,18 +209,18 @@ export function CadDiff({ before, after }: FileDiff): React.ReactElement {
}} }}
> >
<TabNav.Link <TabNav.Link
selected={!showUnified} selected={!showCombined}
onClick={() => setShowUnified(false)} onClick={() => setShowCombined(false)}
sx={{ cursor: 'pointer' }} sx={{ cursor: 'pointer' }}
> >
Side-by-side 2-up
</TabNav.Link> </TabNav.Link>
<TabNav.Link <TabNav.Link
selected={showUnified} selected={showCombined}
onClick={() => setShowUnified(true)} onClick={() => setShowCombined(true)}
sx={{ cursor: 'pointer' }} sx={{ cursor: 'pointer' }}
> >
Unified Combined
<StyledOcticon <StyledOcticon
icon={BeakerIcon} icon={BeakerIcon}
color="fg.muted" color="fg.muted"
@ -199,6 +231,13 @@ export function CadDiff({ before, after }: FileDiff): React.ReactElement {
</TabNav> </TabNav>
</Box> </Box>
)} )}
{!beforeGeometry && !afterGeometry && (
<Box p={3}>
<Text>
Sorry, the rich diff can't be displayed for this file.
</Text>
</Box>
)}
</> </>
) )
} }

View File

@ -1,19 +1,18 @@
import { OrthographicCamera } from '@react-three/drei' import { OrthographicCamera } from '@react-three/drei'
import { useThree } from '@react-three/fiber' import { useThree } from '@react-three/fiber'
import { useEffect, useLayoutEffect, useRef, useState } from 'react' import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { BufferGeometry } from 'three' import { Sphere } from 'three'
function CameraLighting({ geometry }: Props) { function CameraLighting({ boundingSphere }: { boundingSphere?: Sphere }) {
const ref1 = useRef<any>() const ref1 = useRef<any>()
const ref2 = useRef<any>() const ref2 = useRef<any>()
useEffect(() => { useEffect(() => {
if (geometry && ref1.current) { if (ref1.current) {
geometry.computeBoundingSphere() const { radius } = boundingSphere || { radius: 1 }
const { radius } = geometry.boundingSphere || { radius: 1 }
// move spot light away relative to the object's size // move spot light away relative to the object's size
ref1.current.position.setLength(radius * 15) ref1.current.position.setLength(radius * 15)
} }
}, [geometry]) }, [boundingSphere])
return ( return (
<> <>
<spotLight <spotLight
@ -23,7 +22,7 @@ function CameraLighting({ geometry }: Props) {
intensity={4} intensity={4}
castShadow castShadow
shadow-mapSize={[1024, 1024]} shadow-mapSize={[1024, 1024]}
shadowCameraNear={1} shadow-cameraNear={1}
/> />
<spotLight <spotLight
ref={ref2} ref={ref2}
@ -43,11 +42,7 @@ export function calculateFovFactor(fov: number, canvasHeight: number): number {
return pixelsFromCenterToTop / Math.tan(halfFovRadians) return pixelsFromCenterToTop / Math.tan(halfFovRadians)
} }
type Props = { export function Camera({ boundingSphere }: { boundingSphere?: Sphere }) {
geometry: BufferGeometry
}
export function Camera({ geometry }: Props) {
const fov = 15 const fov = 15
const persRef = useRef<any>(null) const persRef = useRef<any>(null)
const orthoRef = useRef<any>(null) const orthoRef = useRef<any>(null)
@ -71,7 +66,7 @@ export function Camera({ geometry }: Props) {
return ( return (
<> <>
<OrthographicCamera ref={orthoRef} makeDefault> <OrthographicCamera ref={orthoRef} makeDefault>
<CameraLighting geometry={geometry} /> <CameraLighting boundingSphere={boundingSphere} />
</OrthographicCamera> </OrthographicCamera>
</> </>
) )

View File

@ -1,16 +1,19 @@
import { OrbitControls } from '@react-three/drei' import { OrbitControls } from '@react-three/drei'
import { useThree } from '@react-three/fiber' import { useThree } from '@react-three/fiber'
import { Vector3 } from 'three'
type Props = { type Props = {
cameraRef: any cameraRef: any
target?: Vector3
} }
export function CameraControls({ cameraRef }: Props) { export function CameraControls({ cameraRef, target }: Props) {
const camera = useThree(s => s.camera) const camera = useThree(s => s.camera)
const gl = useThree(s => s.gl) const gl = useThree(s => s.gl)
return ( return (
<OrbitControls <OrbitControls
makeDefault makeDefault
target={target}
ref={cameraRef} ref={cameraRef}
args={[camera, gl.domElement]} args={[camera, gl.domElement]}
enablePan={false} enablePan={false}

View File

@ -1,58 +1,35 @@
import type { MutableRefObject } from 'react' import type { MutableRefObject } from 'react'
import { useTheme } from '@primer/react' import { useTheme } from '@primer/react'
import { import { BufferGeometry, Sphere } from 'three'
Box3,
BufferGeometry,
Group,
Mesh,
MeshBasicMaterial,
Sphere,
Vector3,
} from 'three'
import { Geometry, Base, Subtraction, Intersection } from '@react-three/csg' import { Geometry, Base, Subtraction, Intersection } from '@react-three/csg'
import { BaseModel } from './BaseModel' import { BaseModel } from './BaseModel'
type UnifiedModelProps = { type CombinedModelProps = {
beforeGeometry: BufferGeometry beforeGeometry: BufferGeometry
afterGeometry: BufferGeometry afterGeometry: BufferGeometry
boundingSphere: Sphere
cameraRef: MutableRefObject<any> cameraRef: MutableRefObject<any>
showUnchanged: boolean showUnchanged: boolean
showAdditions: boolean showAdditions: boolean
showDeletions: boolean showDeletions: boolean
} }
function getCommonSphere( export function CombinedModel({
beforeGeometry: BufferGeometry,
afterGeometry: BufferGeometry
) {
const group = new Group()
const dummyMaterial = new MeshBasicMaterial()
group.add(new Mesh(beforeGeometry, dummyMaterial))
group.add(new Mesh(afterGeometry, dummyMaterial))
const boundingBox = new Box3().setFromObject(group)
const center = new Vector3()
boundingBox.getCenter(center)
return boundingBox.getBoundingSphere(new Sphere(center))
}
export function UnifiedModel({
beforeGeometry, beforeGeometry,
afterGeometry, afterGeometry,
boundingSphere,
cameraRef, cameraRef,
showUnchanged, showUnchanged,
showAdditions, showAdditions,
showDeletions, showDeletions,
}: UnifiedModelProps) { }: CombinedModelProps) {
const { theme } = useTheme() const { theme } = useTheme()
const commonColor = theme?.colors.fg.muted const commonColor = theme?.colors.fg.muted
const additionsColor = theme?.colors.success.muted const additionsColor = theme?.colors.success.muted
const deletionsColor = theme?.colors.danger.muted const deletionsColor = theme?.colors.danger.muted
return ( return (
<BaseModel <BaseModel boundingSphere={boundingSphere} cameraRef={cameraRef}>
boundingSphere={getCommonSphere(beforeGeometry, afterGeometry)}
cameraRef={cameraRef}
>
{/* Unchanged */} {/* Unchanged */}
<mesh> <mesh>
<meshPhongMaterial <meshPhongMaterial

View File

@ -4,22 +4,28 @@ import { BufferGeometry } from 'three'
import { Canvas } from '@react-three/fiber' import { Canvas } from '@react-three/fiber'
import { Camera } from './Camera' import { Camera } from './Camera'
import { CameraControls } from './CameraControls' import { CameraControls } from './CameraControls'
import { Sphere } from 'three'
type Viewer3DProps = { type Viewer3DProps = {
cameraRef: MutableRefObject<any> cameraRef: MutableRefObject<any>
geometry: BufferGeometry geometry: BufferGeometry
boundingSphere?: Sphere
} }
export function Viewer3D({ export function Viewer3D({
cameraRef, cameraRef,
geometry, geometry,
boundingSphere,
children, children,
}: PropsWithChildren<Viewer3DProps>) { }: PropsWithChildren<Viewer3DProps>) {
return ( return (
<Canvas dpr={[1, 2]} shadows> <Canvas dpr={[1, 2]} shadows>
{children} {children}
<CameraControls cameraRef={cameraRef} /> <CameraControls
{geometry && <Camera geometry={geometry} />} cameraRef={cameraRef}
target={boundingSphere?.center}
/>
{geometry && <Camera boundingSphere={boundingSphere} />}
</Canvas> </Canvas>
) )
} }

View File

@ -1,7 +1,7 @@
import type { MutableRefObject } from 'react' import type { MutableRefObject } from 'react'
import { useMemo, useRef } from 'react' import { useMemo, useRef } from 'react'
import { BufferGeometry, DoubleSide } from 'three' import { BufferGeometry, DoubleSide } from 'three'
import { EdgesGeometry } from 'three' import { EdgesGeometry, Sphere } from 'three'
import { BaseModel } from './BaseModel' import { BaseModel } from './BaseModel'
export type WireframeColors = { export type WireframeColors = {
@ -14,18 +14,27 @@ type Props = {
cameraRef: MutableRefObject<any> cameraRef: MutableRefObject<any>
geometry: BufferGeometry geometry: BufferGeometry
colors: WireframeColors colors: WireframeColors
boundingSphere?: Sphere
} }
export function WireframeModel({ geometry, cameraRef, colors }: Props) { export function WireframeModel({
geometry,
boundingSphere,
cameraRef,
colors,
}: Props) {
const groupRef = useRef<any>() const groupRef = useRef<any>()
const edgeThresholdAngle = 10 const edgeThresholdAngle = 10
const edges = useMemo( const edges = useMemo(
() => new EdgesGeometry(geometry.center(), edgeThresholdAngle), () => new EdgesGeometry(geometry, edgeThresholdAngle),
[geometry] [geometry]
) )
return ( return (
<BaseModel boundingSphere={geometry.boundingSphere} cameraRef={cameraRef}> <BaseModel
boundingSphere={boundingSphere || geometry.boundingSphere}
cameraRef={cameraRef}
>
<group ref={groupRef}> <group ref={groupRef}>
<mesh <mesh
castShadow={true} castShadow={true}

84
src/utils/three.test.ts Normal file
View File

@ -0,0 +1,84 @@
import { BoxGeometry } from 'three'
import { getCommonSphere, loadGeometry } from './three'
describe('Function loadGeometry', () => {
it('loads a three geometry from a 10x10mm box', () => {
const box_10 = Buffer.from(
`
v 0.000000 0.000000 0.000000
v 10.000000 0.000000 0.000000
v 0.000000 0.000000 10.000000
v 10.000000 0.000000 10.000000
v 10.000000 10.000000 0.000000
v 10.000000 10.000000 10.000000
v 0.000000 10.000000 0.000000
v 0.000000 10.000000 10.000000
vn 0.000000 -1.000000 0.000000
vn 0.000000 -1.000000 0.000000
vn 1.000000 0.000000 0.000000
vn 1.000000 -0.000000 0.000000
vn 0.000000 1.000000 -0.000000
vn 0.000000 1.000000 0.000000
vn -1.000000 0.000000 0.000000
vn -1.000000 -0.000000 0.000000
vn 0.000000 0.000000 -1.000000
vn 0.000000 0.000000 -1.000000
vn 0.000000 0.000000 1.000000
vn 0.000000 0.000000 1.000000
f 1//1 2//1 3//1
f 3//2 2//2 4//2
f 2//3 5//3 4//3
f 4//4 5//4 6//4
f 5//5 7//5 6//5
f 6//6 7//6 8//6
f 7//7 1//7 8//7
f 8//8 1//8 3//8
f 1//9 5//9 2//9
f 7//10 5//10 1//10
f 6//11 3//11 4//11
f 6//12 8//12 3//12
`
).toString('base64')
const geometry = loadGeometry(box_10)
expect(geometry?.attributes.position.count).toEqual(6 * 2 * 3)
expect(geometry?.attributes.normal.count).toEqual(6 * 2 * 3)
expect(geometry?.attributes.uv).toBeDefined()
expect(geometry?.boundingSphere?.center.x).toEqual(5)
expect(geometry?.boundingSphere?.center.y).toEqual(5)
expect(geometry?.boundingSphere?.center.z).toEqual(5)
const diagonal = 10 * Math.sqrt(3)
expect(geometry?.boundingSphere?.radius).toBeCloseTo(diagonal / 2)
})
it('fails with an empty or invalid input', () => {
expect(loadGeometry('invalid')).toBeUndefined()
})
})
describe('Function getCommonSphere', () => {
// 1mm cube with "top-right" corner at origin
const box_1_geom = new BoxGeometry(1, 1, 1)
box_1_geom.translate(-1 / 2, -1 / 2, -1 / 2)
// 2mm cube with "bottom left" corner at origin
const box_2_geom = new BoxGeometry(2, 2, 2)
box_2_geom.translate(2 / 2, 2 / 2, 2 / 2)
it('gets the common bounding sphere between two geometries', () => {
const sphere = getCommonSphere(box_1_geom, box_2_geom)
expect(sphere.center.x).toEqual((2 - 1) / 2)
expect(sphere.center.y).toEqual((2 - 1) / 2)
expect(sphere.center.z).toEqual((2 - 1) / 2)
const bothDiagonals = (1 + 2) * Math.sqrt(3)
expect(sphere.radius).toBeCloseTo(bothDiagonals / 2)
})
it('gets the same bounding sphere from two identical geometries', () => {
box_1_geom.computeBoundingSphere()
const sphere_1 = getCommonSphere(box_1_geom, box_1_geom)
expect(sphere_1.equals(box_1_geom.boundingSphere!)).toBeTruthy()
box_2_geom.computeBoundingSphere()
const sphere_2 = getCommonSphere(box_2_geom, box_2_geom)
expect(sphere_2.equals(box_2_geom.boundingSphere!)).toBeTruthy()
})
})

46
src/utils/three.ts Normal file
View File

@ -0,0 +1,46 @@
import {
Box3,
BufferAttribute,
BufferGeometry,
Group,
Mesh,
MeshBasicMaterial,
Sphere,
Vector3,
} from 'three'
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader'
import { Buffer } from 'buffer'
export function loadGeometry(file: string): BufferGeometry | undefined {
const loader = new OBJLoader()
const buffer = Buffer.from(file, 'base64').toString()
const group = loader.parse(buffer)
console.log(`Model ${group.id} loaded`)
const geometry = (group.children[0] as Mesh)?.geometry
if (geometry) {
if (!geometry.attributes.uv) {
// UV is needed for @react-three/csg
// see: github.com/KittyCAD/diff-viewer-extension/issues/73
geometry.setAttribute(
'uv',
new BufferAttribute(new Float32Array([]), 1)
)
}
geometry.computeBoundingSphere() // will be used for auto-centering
}
return geometry
}
export function getCommonSphere(
beforeGeometry: BufferGeometry,
afterGeometry: BufferGeometry
) {
const group = new Group()
const dummyMaterial = new MeshBasicMaterial()
group.add(new Mesh(beforeGeometry, dummyMaterial))
group.add(new Mesh(afterGeometry, dummyMaterial))
const boundingBox = new Box3().setFromObject(group)
const center = new Vector3()
boundingBox.getCenter(center)
return boundingBox.getBoundingSphere(new Sphere(center))
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 77 KiB