feat: establish facade-first runtime boundary
This commit is contained in:
75
src/facade/threeViewport.ts
Normal file
75
src/facade/threeViewport.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import * as THREE from 'three'
|
||||
import type { BitBybitViewportAdapter } from './types'
|
||||
|
||||
/** Internal renderer boundary. React receives only the adapter contract. */
|
||||
export class ThreeViewportAdapter implements BitBybitViewportAdapter {
|
||||
private host: HTMLElement | null = null
|
||||
private renderer: THREE.WebGLRenderer | null = null
|
||||
private scene: THREE.Scene | null = null
|
||||
private camera: THREE.PerspectiveCamera | null = null
|
||||
private selection: THREE.Mesh | null = null
|
||||
private frame = 0
|
||||
|
||||
mount(host: HTMLElement) {
|
||||
this.host = host
|
||||
this.scene = new THREE.Scene()
|
||||
this.scene.background = new THREE.Color(0x0e1417)
|
||||
this.camera = new THREE.PerspectiveCamera(38, 1, 0.1, 1000)
|
||||
this.camera.position.set(3.8, 3.1, 5.2)
|
||||
this.camera.lookAt(0, 0, 0)
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false, powerPreference: 'high-performance' })
|
||||
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))
|
||||
this.renderer.setSize(host.clientWidth || 1, host.clientHeight || 1, false)
|
||||
host.appendChild(this.renderer.domElement)
|
||||
const grid = new THREE.GridHelper(8, 16, 0x31555a, 0x1e3439)
|
||||
grid.rotation.x = 0
|
||||
this.scene.add(grid)
|
||||
const geometry = new THREE.BoxGeometry(2.7, 1.6, 1.4)
|
||||
const material = new THREE.MeshStandardMaterial({ color: 0x579a9c, roughness: 0.62, metalness: 0.1 })
|
||||
this.selection = new THREE.Mesh(geometry, material)
|
||||
this.selection.position.y = 0.8
|
||||
this.scene.add(this.selection)
|
||||
this.scene.add(new THREE.HemisphereLight(0xb8eeee, 0x142125, 2.1))
|
||||
const key = new THREE.DirectionalLight(0xffffff, 2.4)
|
||||
key.position.set(4, 6, 4)
|
||||
this.scene.add(key)
|
||||
const render = () => {
|
||||
if (!this.renderer || !this.scene || !this.camera) return
|
||||
this.selection && (this.selection.rotation.y += 0.002)
|
||||
this.renderer.render(this.scene, this.camera)
|
||||
this.frame = requestAnimationFrame(render)
|
||||
}
|
||||
this.resize(host.clientWidth || 1, host.clientHeight || 1)
|
||||
render()
|
||||
}
|
||||
|
||||
resize(width: number, height: number, devicePixelRatio = Math.min(window.devicePixelRatio || 1, 2)) {
|
||||
if (!this.renderer || !this.camera) return
|
||||
this.camera.aspect = Math.max(width, 1) / Math.max(height, 1)
|
||||
this.camera.updateProjectionMatrix()
|
||||
this.renderer.setPixelRatio(Math.min(devicePixelRatio, 2))
|
||||
this.renderer.setSize(Math.max(width, 1), Math.max(height, 1), false)
|
||||
}
|
||||
|
||||
setSelection(objectId: string) {
|
||||
if (!this.selection) return
|
||||
const material = this.selection.material as THREE.MeshStandardMaterial
|
||||
material.color.set(objectId ? 0x5ed6d6 : 0x579a9c)
|
||||
material.emissive.set(objectId ? 0x123c3c : 0x000000)
|
||||
}
|
||||
|
||||
dispose() {
|
||||
cancelAnimationFrame(this.frame)
|
||||
this.selection?.geometry.dispose()
|
||||
if (this.selection?.material instanceof THREE.Material) this.selection.material.dispose()
|
||||
this.renderer?.dispose()
|
||||
this.renderer?.domElement.remove()
|
||||
this.host = null
|
||||
this.renderer = null
|
||||
this.scene = null
|
||||
this.camera = null
|
||||
this.selection = null
|
||||
}
|
||||
|
||||
getBackend() { return 'webgl2' as const }
|
||||
}
|
||||
Reference in New Issue
Block a user