import * as THREE from 'three'; import type { GeometryCache } from './geometryCache'; import { meshToDto, type MeshDto } from '../backend/dto/mesh'; import { model } from '../model/model'; import { Geometry } from '../backend/geometry/geometry'; import type { Id } from '../types'; export class SceneSync { private scene: THREE.Scene; private meshByFace: Record = {}; // faceId → THREE.Mesh private cache: GeometryCache; private _selectedFaceIds: Id[] = []; private readonly baseMaterial = new THREE.MeshPhongMaterial({ color: 0x4a7fc8, shininess: 40, specular: 0x223344, wireframe: false, }); constructor(scene: THREE.Scene, cache: GeometryCache) { this.scene = scene; this.cache = cache; } public get selectedFaceIds() { return this._selectedFaceIds; } public get meshes(): THREE.Mesh[] { return Object.values(this.meshByFace); } public get items(): Record { return this.meshByFace; } addWholeModel() { for (const id of Object.keys(model.solids)) { const meshes = Geometry .tessellateSolid(id) .map(meshToDto); for (const mesh of meshes) this.addSolid(mesh); } } // Called when FE scene graph syncs from BE addSolid(dto: MeshDto) { const faceId = dto.faceId; const transform = new Float32Array([ 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 ]); if (this.meshByFace[faceId]) return; const geo = this.cache.getOrCreate(faceId, 0, dto); const mesh = new THREE.Mesh(geo, this.baseMaterial.clone()); mesh.userData.faceId = faceId; mesh.userData.surfaceId = dto.surfaceId; mesh.userData.solidId = dto.solidId; // Apply transform (col-major mat4 from BE) const m = new THREE.Matrix4(); m.fromArray(transform); // THREE expects col-major mesh.applyMatrix4(m); this.scene.add(mesh); this.meshByFace[faceId] = mesh; } updateTransform(faceId: Id, colMajorMat4: number[]) { const mesh = this.meshByFace[faceId]; if (!mesh) return; const m = new THREE.Matrix4().fromArray(colMajorMat4); mesh.matrix.copy(m); mesh.matrix.decompose(mesh.position, mesh.quaternion, mesh.scale); } setSelected(faceIds: Id[]) { this._selectedFaceIds = faceIds; for (const [sid, mesh] of Object.entries(this.meshByFace)) { const mat = mesh.material as THREE.MeshPhongMaterial; if (faceIds.includes(sid)) { mat.color.setHex(0xf0a040); mat.emissive.setHex(0x221100); } else { mat.color.setHex(0x4a7fc8); mat.emissive.setHex(0x000000); } } } public disposeMesh(faceId: Id) { const mesh = this.meshByFace[faceId]; if (!mesh) return; this.scene.remove(mesh); mesh.geometry.dispose(); if (Array.isArray(mesh.material)) { for (const mat of mesh.material) mat.dispose(); } else mesh.material.dispose(); this.cache.unset(faceId, 0); delete (this.meshByFace[faceId]); } public dispose() { for (const faceId of Object.keys(this.meshByFace)) this.disposeMesh(faceId); this.baseMaterial.dispose(); } }