Files
zhengte.babylonjs-sdk/src/babylonjs/AppPlacementWall.ts
2026-05-17 13:59:16 +08:00

377 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Scene, Mesh, MeshBuilder, StandardMaterial, Color3, Vector3, ActionManager, ExecuteCodeAction } from '@babylonjs/core';
/**
* 墙面配置 - 定义一个垂直面的起始和结束坐标
*/
export interface WallConfig {
name: string; // 墙面名称(如 "front", "back", "left", "right"
startPoint: Vector3; // 起始点坐标(左下角)
endPoint: Vector3; // 结束点坐标(右下角)
height: number; // 墙面高度
divisions: number; // 分割数(将这个面分成几块)
offset?: number; // 偏移量正数向外负数向内默认0
}
/**
* 放置区域配置
*/
export interface PlacementAreaConfig {
walls: WallConfig[]; // 墙面数组可以定义1-N个面
color?: string; // 颜色(十六进制)
alpha?: number; // 透明度
thickness?: number; // 厚度
showBorder?: boolean; // 是否显示边框
borderColor?: string; // 边框颜色
}
/**
* 单个放置区域的信息
*/
export interface PlacementZoneInfo {
mesh: Mesh; // 放置区域的网格
wallName: string; // 所属墙面名称
index: number; // 在该墙面上的索引
center: Vector3; // 中心点坐标
width: number; // 宽度
height: number; // 高度
normal: Vector3; // 法线方向
}
export class AppPlacementWall {
private scene: Scene;
private placementZones: PlacementZoneInfo[] = [];
private borderLines: Mesh[] = [];
private onZoneClickCallback?: (zoneInfo: PlacementZoneInfo) => void;
constructor(scene: Scene) {
this.scene = scene;
}
/**
* 根据墙面配置生成放置区域
*/
generatePlacementAreas(config: PlacementAreaConfig): PlacementZoneInfo[] {
const {
walls,
color = '#21c7ff',
alpha = 0.3,
thickness = 2,
showBorder = true,
borderColor = '#ffffff'
} = config;
// 清除之前的放置区域
this.clearAll();
const material = this.createMaterial(color, alpha);
walls.forEach(wall => {
const zones = this.generateWallZones(wall, material, thickness);
this.placementZones.push(...zones);
if (showBorder) {
this.createWallBorder(wall, borderColor);
}
});
return this.placementZones;
}
/**
* 为单个墙面生成放置区域
*/
private generateWallZones(
wall: WallConfig,
material: StandardMaterial,
thickness: number
): PlacementZoneInfo[] {
const { name, startPoint, endPoint, height, divisions, offset = 0 } = wall;
// 计算墙面的方向向量和长度
const direction = endPoint.subtract(startPoint);
const wallLength = direction.length();
const normalizedDir = direction.normalize();
// 计算每块的宽度
const blockWidth = wallLength / divisions;
// 计算墙面的法线方向(垂直于墙面,指向外侧)
const normal = new Vector3(-normalizedDir.z, 0, normalizedDir.x);
// 应用偏移量(沿着法线方向)
const offsetVector = normal.scale(offset);
const zones: PlacementZoneInfo[] = [];
for (let i = 0; i < divisions; i++) {
// 计算当前块的中心位置
const offset_i = blockWidth * (i + 0.5);
const centerX = startPoint.x + normalizedDir.x * offset_i;
const centerY = startPoint.y + height / 2;
const centerZ = startPoint.z + normalizedDir.z * offset_i;
const center = new Vector3(centerX, centerY, centerZ).add(offsetVector);
// 创建放置区域平面
const plane = MeshBuilder.CreatePlane(
`placement_${name}_${i}`,
{ width: blockWidth, height: height },
this.scene
);
plane.position = center;
plane.material = material;
// 让平面面向法线方向(垂直站立并朝外)
// 使用 lookAt 让平面面向外侧
const targetPoint = center.add(normal);
plane.lookAt(targetPoint);
// 设置厚度通过缩放Z轴
plane.scaling.z = thickness;
// 启用拾取
plane.isPickable = true;
// 添加点击事件
plane.actionManager = new ActionManager(this.scene);
plane.actionManager.registerAction(
new ExecuteCodeAction(
ActionManager.OnPickTrigger,
() => {
const zoneInfo: PlacementZoneInfo = {
mesh: plane,
wallName: name,
index: i,
center: center.clone(),
width: blockWidth,
height: height,
normal: normal.clone()
};
if (this.onZoneClickCallback) {
this.onZoneClickCallback(zoneInfo);
}
}
)
);
// 为每个块创建边框
this.createBlockBorder(name, i, center, blockWidth, height, normalizedDir, normal);
zones.push({
mesh: plane,
wallName: name,
index: i,
center: center.clone(),
width: blockWidth,
height: height,
normal: normal.clone()
});
}
return zones;
}
/**
* 为单个块创建边框
*/
private createBlockBorder(
wallName: string,
index: number,
center: Vector3,
width: number,
height: number,
direction: Vector3,
normal: Vector3
): void {
const halfWidth = width / 2;
const halfHeight = height / 2;
// 计算四个角点
const bottomLeft = new Vector3(
center.x - direction.x * halfWidth,
center.y - halfHeight,
center.z - direction.z * halfWidth
);
const bottomRight = new Vector3(
center.x + direction.x * halfWidth,
center.y - halfHeight,
center.z + direction.z * halfWidth
);
const topLeft = new Vector3(
center.x - direction.x * halfWidth,
center.y + halfHeight,
center.z - direction.z * halfWidth
);
const topRight = new Vector3(
center.x + direction.x * halfWidth,
center.y + halfHeight,
center.z + direction.z * halfWidth
);
// 创建四条边
const edges = [
[bottomLeft, bottomRight], // 底边
[bottomRight, topRight], // 右边
[topRight, topLeft], // 顶边
[topLeft, bottomLeft] // 左边
];
edges.forEach((edge, edgeIndex) => {
const line = MeshBuilder.CreateLines(
`block_border_${wallName}_${index}_${edgeIndex}`,
{ points: edge },
this.scene
);
line.color = new Color3(1, 1, 1); // 白色
line.isPickable = false; // 禁用边框的点击
this.borderLines.push(line);
});
}
/**
* 创建墙面边框
*/
private createWallBorder(wall: WallConfig, color: string): void {
const { name, startPoint, endPoint, height, offset = 0 } = wall;
// 计算法线方向
const direction = endPoint.subtract(startPoint);
const normalizedDir = direction.normalize();
const normal = new Vector3(-normalizedDir.z, 0, normalizedDir.x);
// 应用偏移量
const offsetVector = normal.scale(offset);
// 计算四个角点(应用偏移)
const bottomLeft = startPoint.clone().add(offsetVector);
const bottomRight = endPoint.clone().add(offsetVector);
const topLeft = new Vector3(startPoint.x, startPoint.y + height, startPoint.z).add(offsetVector);
const topRight = new Vector3(endPoint.x, endPoint.y + height, endPoint.z).add(offsetVector);
// 创建四条边
const edges = [
[bottomLeft, bottomRight], // 底边
[bottomRight, topRight], // 右边
[topRight, topLeft], // 顶边
[topLeft, bottomLeft] // 左边
];
const rgb = this.hexToRgb(color);
const lineColor = new Color3(rgb.r, rgb.g, rgb.b);
edges.forEach((edge, index) => {
const line = MeshBuilder.CreateLines(
`border_${name}_${index}`,
{ points: edge },
this.scene
);
line.color = lineColor;
line.isPickable = false; // 禁用边框的点击
this.borderLines.push(line);
});
}
/**
* 创建材质
*/
private createMaterial(color: string, alpha: number): StandardMaterial {
const material = new StandardMaterial('placementMaterial', this.scene);
const rgb = this.hexToRgb(color);
material.emissiveColor = new Color3(rgb.r, rgb.g, rgb.b);
material.disableLighting = true;
material.alpha = alpha;
material.backFaceCulling = false;
return material;
}
/**
* 十六进制颜色转RGB
*/
private hexToRgb(hex: string): { r: number; g: number; b: number } {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16) / 255,
g: parseInt(result[2], 16) / 255,
b: parseInt(result[3], 16) / 255
}
: { r: 0, g: 0, b: 0 };
}
/**
* 获取所有放置区域
*/
getPlacementZones(): PlacementZoneInfo[] {
return this.placementZones;
}
/**
* 根据墙面名称获取放置区域
*/
getZonesByWall(wallName: string): PlacementZoneInfo[] {
return this.placementZones.filter(zone => zone.wallName === wallName);
}
/**
* 根据索引获取特定放置区域
*/
getZone(wallName: string, index: number): PlacementZoneInfo | undefined {
return this.placementZones.find(
zone => zone.wallName === wallName && zone.index === index
);
}
/**
* 设置点击回调
*/
setOnZoneClick(callback: (zoneInfo: PlacementZoneInfo) => void): void {
this.onZoneClickCallback = callback;
}
/**
* 显示所有放置区域
*/
show(): void {
this.placementZones.forEach(zone => {
zone.mesh.isVisible = true;
});
this.borderLines.forEach(line => {
line.isVisible = true;
});
}
/**
* 隐藏所有放置区域
*/
hide(): void {
this.placementZones.forEach(zone => {
zone.mesh.isVisible = false;
});
this.borderLines.forEach(line => {
line.isVisible = false;
});
}
/**
* 清除所有放置区域
*/
clearAll(): void {
this.placementZones.forEach(zone => {
zone.mesh.dispose();
});
this.placementZones = [];
this.borderLines.forEach(line => {
line.dispose();
});
this.borderLines = [];
}
/**
* 销毁
*/
dispose(): void {
this.clearAll();
}
}