import * as THREE from "three"; import { BoardLayout3D, type BoardLayout3DResult } from "./BoardLayout3D"; /** * 3D 棋盘渲染器配置 */ export interface BoardRendererConfig { /** 格子材质(默认:深蓝半透明) */ cellMaterial?: THREE.Material; /** 格子相对于 cellSize 的填充比例(默认 0.92,即 8% 间隙) */ cellPadding?: number; /** 格子地面 Y 坐标(默认 0.005,略高于地面防止 z-fighting) */ cellY?: number; /** 是否创建网格线(默认 false) */ showGridLines?: boolean; } const DEFAULT_CONFIG: Required = { cellMaterial: null!, // 运行时创建默认材质 cellPadding: 0.92, cellY: 0.005, showGridLines: false, }; /** * Three.js 3D 棋盘渲染器 * * 建立在 BoardLayout3D 纯数学层之上,负责实际的 3D 渲染: * - 创建网格地面(PlaneGeometry cells) * - 在 boardGroup 中管理所有棋盘物体(统一平移/抖动) * - 放置/移除 3D 物体到指定格子 * - 基于 Set 的批量可见性管理 * - Raycaster 命中检测(遍历 userData.pathIndex) * - 棋盘抖动动画 * * 坐标系:棋盘在 XZ 平面,以原点为中心(来自 BoardLayout3D)。 */ export class BoardRenderer3D { private scene: THREE.Scene; private layout: BoardLayout3D; private config: Required; /** 棋盘根 Group(所有棋盘元素的容器,用于统一 shake) */ private boardGroup: THREE.Group; /** 格子底板 Mesh 列表 */ private cellMeshes: THREE.Mesh[] = []; /** 按 cellIndex 存储放置的物体 */ private placedObjects: Map = new Map(); /** 按 pathIndex 分组的物体索引(用于批量可见性) */ private pathToIndices: Map = new Map(); constructor(scene: THREE.Scene, layout: BoardLayout3D, config?: BoardRendererConfig) { this.scene = scene; this.layout = layout; this.config = { ...DEFAULT_CONFIG, ...config }; // 创建默认格子材质 if (!this.config.cellMaterial) { this.config.cellMaterial = new THREE.MeshStandardMaterial({ color: 0x2d2d4e, roughness: 0.9, metalness: 0, transparent: true, opacity: 0.6, }); } this.boardGroup = new THREE.Group(); this.scene.add(this.boardGroup); } /** * 创建网格底板 */ createGrid(): void { const layoutResult = this.layout.getLayout(); const { cellSize, rows, cols } = layoutResult; const padding = this.config.cellPadding; const cellGeoSize = cellSize * padding; const cellGeo = new THREE.PlaneGeometry(cellGeoSize, cellGeoSize); for (let r = 0; r < rows; r++) { for (let c = 0; c < cols; c++) { const cell = new THREE.Mesh(cellGeo.clone(), (this.config.cellMaterial as THREE.Material).clone()); cell.rotation.x = -Math.PI / 2; const pos = this.layout.getCellWorldPos(c, r); cell.position.set(pos.x, this.config.cellY, pos.z); cell.receiveShadow = true; this.boardGroup.add(cell); this.cellMeshes.push(cell); } } } /** * 在指定格子放置一个 3D 物体 * @param col 列 * @param row 行 * @param object 要放置的物体 * @param tag 附加到 userData 的标签(如 { pathIndex: 0, cellIndex: 5 }) */ placeObject(col: number, row: number, object: THREE.Object3D, tag?: Record): void { const pos = this.layout.getCellWorldPos(col, row); object.position.set(pos.x, 0, pos.z); if (tag) { Object.assign(object.userData, tag); } const index = this.layout.cellToIndex(col, row); this.placedObjects.set(index, object); this.boardGroup.add(object); // 维护 pathIndex → indices 映射 const pathIndex = tag?.pathIndex; if (typeof pathIndex === "number") { const indices = this.pathToIndices.get(pathIndex) || []; indices.push(index); this.pathToIndices.set(pathIndex, indices); } } /** * 移除指定格子的物体 */ removeObject(col: number, row: number): void { const index = this.layout.cellToIndex(col, row); const obj = this.placedObjects.get(index); if (obj) { this.boardGroup.remove(obj); this.placedObjects.delete(index); } } /** * 获取指定格子的物体 */ getObject(col: number, row: number): THREE.Object3D | undefined { const index = this.layout.cellToIndex(col, row); return this.placedObjects.get(index); } /** * 获取某个 pathIndex 对应的所有物体 */ getObjectsByPath(pathIndex: number): THREE.Object3D[] { const indices = this.pathToIndices.get(pathIndex) || []; const objects: THREE.Object3D[] = []; for (const idx of indices) { const obj = this.placedObjects.get(idx); if (obj) objects.push(obj); } return objects; } /** * 批量更新可见性 * @param hiddenIndices 需要隐藏的 cellIndex 集合 */ updateVisibility(hiddenIndices: Set): void { for (const [index, obj] of this.placedObjects) { obj.visible = !hiddenIndices.has(index); } } /** * 按 pathIndex 批量更新可见性 * @param hiddenPaths 需要隐藏的 pathIndex 集合 */ updateVisibilityByPath(hiddenPaths: Set): void { for (const [pathIndex, indices] of this.pathToIndices) { const hidden = hiddenPaths.has(pathIndex); for (const idx of indices) { const obj = this.placedObjects.get(idx); if (obj) obj.visible = !hidden; } } } /** * Raycaster 命中检测 * @param raycaster 射线 * @param excludeIndices 排除的 cellIndex 集合 * @returns 命中的 { index, pathIndex, object } 或 null */ hitTest( raycaster: THREE.Raycaster, excludeIndices?: Set, ): { index: number; pathIndex: number; object: THREE.Object3D } | null { // 收集所有可被检测的 Mesh const meshes: THREE.Mesh[] = []; for (const [index, obj] of this.placedObjects) { if (excludeIndices?.has(index)) continue; if (!obj.visible) continue; obj.traverse((child) => { if (child instanceof THREE.Mesh) { meshes.push(child); } }); } const intersects = raycaster.intersectObjects(meshes, false); if (intersects.length === 0) return null; // 沿 parent chain 查找含 pathIndex 的 userData let current: THREE.Object3D | null = intersects[0].object; while (current && current !== this.boardGroup) { if (current.userData && typeof current.userData.pathIndex === "number") { // 找到对应的 cellIndex for (const [idx, obj] of this.placedObjects) { if (obj === current) { return { index: idx, pathIndex: current.userData.pathIndex, object: current }; } } // 如果没有精确匹配到 placedObjects 的顶层,仍然返回 return { index: -1, pathIndex: current.userData.pathIndex, object: current }; } current = current.parent; } return null; } /** * 棋盘抖动动画 * @param dx X 方向强度倍数 * @param dz Z 方向强度倍数 * @param duration 持续时间 ms(默认 300) * @param intensity 基础强度(默认 0.08) */ shake(dx: number = 1, dz: number = 1, duration: number = 300, intensity: number = 0.08): void { const originalX = this.boardGroup.position.x; const originalZ = this.boardGroup.position.z; const startTime = performance.now(); const animate = () => { const elapsed = performance.now() - startTime; const progress = Math.min(elapsed / duration, 1); if (progress < 1) { const decay = 1 - progress; const offsetX = Math.sin(progress * Math.PI * 6) * intensity * decay * dx; const offsetZ = Math.sin(progress * Math.PI * 6) * intensity * decay * dz; this.boardGroup.position.x = originalX + offsetX; this.boardGroup.position.z = originalZ + offsetZ; requestAnimationFrame(animate); } else { this.boardGroup.position.x = originalX; this.boardGroup.position.z = originalZ; } }; requestAnimationFrame(animate); } /** * 获取棋盘根 Group */ getBoardGroup(): THREE.Group { return this.boardGroup; } /** * 获取底层 BoardLayout3D 实例 */ getLayout(): BoardLayout3D { return this.layout; } /** * 清空所有棋盘内容(格子 + 物体),保留 boardGroup */ clear(): void { for (const cell of this.cellMeshes) { this.boardGroup.remove(cell); cell.geometry.dispose(); (cell.material as THREE.Material).dispose(); } this.cellMeshes = []; for (const obj of this.placedObjects.values()) { this.boardGroup.remove(obj); } this.placedObjects.clear(); this.pathToIndices.clear(); } /** * 完全销毁(包括 boardGroup 自身) */ dispose(): void { this.clear(); this.scene.remove(this.boardGroup); } }