import * as THREE from "three"; import type { BoardLayout3D } from "./BoardLayout3D"; export interface PathData { points: Array<{ x: number; y: number }>; // GridPoints (x=col, y=row) direction: "up" | "down" | "left" | "right"; color: string; } export type PathState = "normal" | "highlight" | "blocked" | "animating"; /** * Three.js mesh-per-cell 路径渲染器。 * 路径覆盖的每个 cell 放置一个 3D 模型,全部朝路径方向旋转。 */ export class PathRenderer3D { private scene: THREE.Scene; private boardLayout: BoardLayout3D; private pathMeshes: Map = new Map(); // pathIndex → mesh[] private baseMaterial!: THREE.MeshStandardMaterial; private baseGeometry!: THREE.BufferGeometry; // 方向 → Y 轴旋转角度(模型默认朝 +Z) private static DIRECTION_ANGLES: Record = { up: Math.PI, down: 0, left: -Math.PI / 2, right: Math.PI / 2, }; constructor(scene: THREE.Scene, boardLayout: BoardLayout3D) { this.scene = scene; this.boardLayout = boardLayout; } /** * 设置共享几何体(所有车复用同一个 geometry) * @param geometry 车模型的几何体(已归一化到单位大小) */ setSharedGeometry(geometry: THREE.BufferGeometry): void { this.baseGeometry = geometry; } /** * 如果没有加载外部模型,使用内置的简单几何体(BoxGeometry 作为占位) */ useDefaultGeometry(): void { // 使用圆角盒子近似汽车形状 this.baseGeometry = new THREE.BoxGeometry(0.6, 0.3, 0.9); } /** * 渲染所有路径 * @param paths 路径数据数组 * @param colorToHex 颜色字符串 → hex 数字 */ renderPaths(paths: PathData[], colorToHex: (color: string) => number): void { this.clearAll(); const cellSize = this.boardLayout.getLayout().cellSize; const meshScale = cellSize * 0.75; for (let pathIdx = 0; pathIdx < paths.length; pathIdx++) { const path = paths[pathIdx]; const meshes: THREE.Mesh[] = []; const angle = PathRenderer3D.DIRECTION_ANGLES[path.direction] ?? 0; const color = colorToHex(path.color); for (let cellIdx = 0; cellIdx < path.points.length; cellIdx++) { const pt = path.points[cellIdx]; const worldPos = this.boardLayout.getCellWorldPos(pt.x, pt.y); // 每个 cell 一个独立材质(用于状态变色) const mat = new THREE.MeshStandardMaterial({ color, roughness: 0.5, metalness: 0.1, }); const mesh = new THREE.Mesh(this.baseGeometry, mat); mesh.position.copy(worldPos); mesh.position.y = meshScale * 0.2; // 略微抬起 mesh.rotation.y = angle; mesh.scale.setScalar(meshScale); mesh.castShadow = true; mesh.receiveShadow = true; // 存储索引信息,用于 Raycaster hitTest mesh.userData = { pathIndex: pathIdx, cellIndex: cellIdx }; this.scene.add(mesh); meshes.push(mesh); } this.pathMeshes.set(pathIdx, meshes); } } /** * 设置路径状态(高亮/阻挡/正常) */ setPathState(pathIndex: number, state: PathState): void { const meshes = this.pathMeshes.get(pathIndex); if (!meshes) return; for (const mesh of meshes) { const mat = mesh.material as THREE.MeshStandardMaterial; switch (state) { case "highlight": mat.emissive.setHex(0x333333); break; case "blocked": mat.emissive.setHex(0x660000); break; case "normal": default: mat.emissive.setHex(0x000000); break; } } } /** * 获取某条路径的所有 Mesh(用于动画) */ getPathMeshes(pathIndex: number): THREE.Mesh[] { return this.pathMeshes.get(pathIndex) || []; } /** * 获取所有可选中的 Mesh 列表(用于 Raycaster) */ getAllSelectableMeshes(): THREE.Mesh[] { const all: THREE.Mesh[] = []; this.pathMeshes.forEach((meshes) => all.push(...meshes)); return all; } /** * 移除某条路径的所有 Mesh */ removePath(pathIndex: number): void { const meshes = this.pathMeshes.get(pathIndex); if (!meshes) return; for (const mesh of meshes) { this.scene.remove(mesh); mesh.geometry.dispose(); (mesh.material as THREE.Material).dispose(); } this.pathMeshes.delete(pathIndex); } /** * 清除所有路径渲染 */ clearAll(): void { this.pathMeshes.forEach((meshes, idx) => { for (const mesh of meshes) { this.scene.remove(mesh); (mesh.material as THREE.Material).dispose(); } }); this.pathMeshes.clear(); } /** * 销毁(释放共享资源) */ destroy(): void { this.clearAll(); if (this.baseGeometry) { this.baseGeometry.dispose(); } } }