feat: establish facade-first runtime boundary

This commit is contained in:
2026-08-02 02:39:37 -04:00
parent 3978833c5c
commit df0053cfe5
16 changed files with 1004 additions and 34 deletions

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