377 lines
10 KiB
TypeScript
377 lines
10 KiB
TypeScript
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();
|
||
}
|
||
}
|