This commit is contained in:
2026-05-13 11:28:49 +08:00
parent 223fa5dd4e
commit 21255a701d
12 changed files with 1765 additions and 537 deletions

View File

@ -1,540 +1,84 @@
import { Scene, Mesh, MeshBuilder, StandardMaterial, Color3, Vector3, AbstractMesh, BoundingBoxGizmo } from '@babylonjs/core';
import { Scene, Vector3 } from '@babylonjs/core';
import { AppPlacementWall, WallConfig, PlacementZoneInfo } from './AppPlacementWall';
/**
* 放置区域配置
*/
export interface DropZoneConfig {
modelName: string; // 目标模型名称
divisions: number; // 分割块数(每条边分成几块)
walls: WallConfig[]; // 墙面配置数组
color?: string; // 颜色(十六进制)
alpha?: number; // 透明度
thickness?: number; // 厚度
offset?: number; // 距离模型的偏移量
scale?: number; // 整体缩放比例0-1用于生成内部放置区域
showBorder?: boolean; // 是否显示边框
borderColor?: string; // 边框颜色
}
/**
* 放置区域管理类(使用新的墙面参数化方案)
*/
export class AppDropZone {
private scene: Scene;
private dropZones: Mesh[] = [];
private dropZoneConfigs: Map<string, any[]> = new Map(); // 存储每个模型的放置区域配置
private boundingBoxLines: Mesh[] = []; // 存储包围盒线框
private placementWall: AppPlacementWall;
constructor(scene: Scene) {
this.scene = scene;
this.placementWall = new AppPlacementWall(scene);
}
/**
* 根据模型包围盒生成四周的放置区域
* 生成放置区域
* @param config 配置参数
*/
generateDropZones(config: DropZoneConfig): Mesh[] {
const {
modelName,
divisions,
color = '#21c7ff',
alpha = 0.3,
thickness = 2,
offset = 5,
scale = 1.0
} = config;
// 查找目标模型(支持 modelId 或 mesh name
let targetMeshes: AbstractMesh[] | undefined;
// 先尝试通过 modelId 查找(从 AppModel 的 modelDic
const mainApp = (this.scene as any).mainApp;
if (mainApp?.appModel) {
targetMeshes = mainApp.appModel.getCachedMeshes(modelName);
}
// 如果没找到,尝试通过 mesh name 查找
if (!targetMeshes || targetMeshes.length === 0) {
const mesh = this.scene.getMeshByName(modelName);
if (mesh) {
targetMeshes = [mesh];
}
}
if (!targetMeshes || targetMeshes.length === 0) {
console.warn(`模型 ${modelName} 不存在`);
return [];
}
// 计算所有网格的总包围盒(使用世界坐标)
let minX = Infinity, minY = Infinity, minZ = Infinity;
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
targetMeshes.forEach(mesh => {
// 强制更新世界矩阵
mesh.computeWorldMatrix(true);
const boundingInfo = mesh.getBoundingInfo();
// 获取世界空间的包围盒
const worldMin = boundingInfo.boundingBox.minimumWorld;
const worldMax = boundingInfo.boundingBox.maximumWorld;
minX = Math.min(minX, worldMin.x);
minY = Math.min(minY, worldMin.y);
minZ = Math.min(minZ, worldMin.z);
maxX = Math.max(maxX, worldMax.x);
maxY = Math.max(maxY, worldMax.y);
maxZ = Math.max(maxZ, worldMax.z);
});
console.log('包围盒坐标:', { minX, minY, minZ, maxX, maxY, maxZ });
console.log('包围盒尺寸:', {
width: maxX - minX,
height: maxY - minY,
depth: maxZ - minZ
});
const width = maxX - minX;
const height = maxY - minY;
const depth = maxZ - minZ;
// 应用缩放比例
const centerX = (minX + maxX) / 2;
const centerY = (minY + maxY) / 2;
const centerZ = (minZ + maxZ) / 2;
const scaledWidth = width * scale;
const scaledHeight = height * scale;
const scaledDepth = depth * scale;
const scaledMinX = centerX - scaledWidth / 2;
const scaledMaxX = centerX + scaledWidth / 2;
const scaledMinY = centerY - scaledHeight / 2;
const scaledMaxY = centerY + scaledHeight / 2;
const scaledMinZ = centerZ - scaledDepth / 2;
const scaledMaxZ = centerZ + scaledDepth / 2;
// 计算每块的尺寸
const blockWidth = scaledWidth / divisions;
const blockDepth = scaledDepth / divisions;
const zones: Mesh[] = [];
const zoneConfigs: any[] = [];
// 创建材质
const material = this.createDropZoneMaterial(color, alpha);
// 前面Z轴负方向
for (let i = 0; i < divisions; i++) {
const x = scaledMinX + blockWidth * i + blockWidth / 2;
const z = scaledMinZ - offset;
const position = new Vector3(x, scaledMinY, z);
const zone = this.createDropZonePlane(
`dropZone_${modelName}_front_${i}`,
blockWidth,
scaledHeight,
position,
0,
material,
thickness
);
zones.push(zone);
zoneConfigs.push({
position: position.clone(),
width: blockWidth,
height: scaledHeight,
rotation: 0,
side: 'front',
index: i
});
}
// 后面Z轴正方向
for (let i = 0; i < divisions; i++) {
const x = scaledMinX + blockWidth * i + blockWidth / 2;
const z = scaledMaxZ + offset;
const position = new Vector3(x, scaledMinY, z);
const zone = this.createDropZonePlane(
`dropZone_${modelName}_back_${i}`,
blockWidth,
scaledHeight,
position,
0,
material,
thickness
);
zones.push(zone);
zoneConfigs.push({
position: position.clone(),
width: blockWidth,
height: scaledHeight,
rotation: 0,
side: 'back',
index: i
});
}
// 左侧X轴负方向
for (let i = 0; i < divisions; i++) {
const x = scaledMinX - offset;
const z = scaledMinZ + blockDepth * i + blockDepth / 2;
const position = new Vector3(x, scaledMinY, z);
const zone = this.createDropZonePlane(
`dropZone_${modelName}_left_${i}`,
blockDepth,
scaledHeight,
position,
Math.PI / 2,
material,
thickness
);
zones.push(zone);
zoneConfigs.push({
position: position.clone(),
width: blockDepth,
height: scaledHeight,
rotation: Math.PI / 2,
side: 'left',
index: i
});
}
// 右侧X轴正方向
for (let i = 0; i < divisions; i++) {
const x = scaledMaxX + offset;
const z = scaledMinZ + blockDepth * i + blockDepth / 2;
const position = new Vector3(x, scaledMinY, z);
const zone = this.createDropZonePlane(
`dropZone_${modelName}_right_${i}`,
blockDepth,
scaledHeight,
position,
Math.PI / 2,
material,
thickness
);
zones.push(zone);
zoneConfigs.push({
position: position.clone(),
width: blockDepth,
height: scaledHeight,
rotation: Math.PI / 2,
side: 'right',
index: i
});
}
// 保存配置
this.dropZoneConfigs.set(modelName, zoneConfigs);
this.dropZones.push(...zones);
// 显示包围盒
this.showBoundingBox(modelName, '#ff0000');
// 默认隐藏
zones.forEach(zone => zone.setEnabled(false));
return zones;
}
/**
* 创建单个放置区域平面
*/
private createDropZonePlane(
name: string,
width: number,
height: number,
position: Vector3,
rotationY: number,
material: StandardMaterial,
thickness: number
): Mesh {
// 创建主平面
const plane = MeshBuilder.CreatePlane(name, {
width: width,
height: height
}, this.scene);
plane.position = position;
plane.rotation.y = rotationY;
plane.material = material;
plane.isPickable = true; // 可以被拾取,用于检测拖拽
plane.metadata = { isDropZone: true }; // 标记为放置区域
// 创建边框线
this.createBorder(plane, width, height, thickness);
return plane;
}
/**
* 创建边框线
*/
private createBorder(parent: Mesh, width: number, height: number, thickness: number): void {
const halfWidth = width / 2;
const halfHeight = height / 2;
const points = [
new Vector3(-halfWidth, -halfHeight, -0.01),
new Vector3(halfWidth, -halfHeight, -0.01),
new Vector3(halfWidth, halfHeight, -0.01),
new Vector3(-halfWidth, halfHeight, -0.01),
new Vector3(-halfWidth, -halfHeight, -0.01)
];
const border = MeshBuilder.CreateLines(`${parent.name}_border`, {
points: points
}, this.scene);
border.color = new Color3(1, 1, 1); // 白色边框
border.parent = parent;
}
/**
* 创建放置区域材质
*/
private createDropZoneMaterial(hexColor: string, alpha: number): StandardMaterial {
const material = new StandardMaterial('dropZoneMat_' + Date.now(), this.scene);
const rgb = this.hexToRgb(hexColor);
material.diffuseColor = new Color3(rgb.r, rgb.g, rgb.b);
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.13, g: 0.78, b: 1 };
}
/**
* 显示所有放置区域
*/
showAllDropZones(): void {
this.dropZones.forEach(zone => zone.setEnabled(true));
// 显示包围盒
this.boundingBoxLines.forEach(line => line.setEnabled(true));
}
/**
* 隐藏所有放置区域
*/
hideAllDropZones(): void {
this.dropZones.forEach(zone => zone.setEnabled(false));
// 隐藏包围盒
this.boundingBoxLines.forEach(line => line.setEnabled(false));
}
/**
* 显示指定模型的放置区域
*/
showDropZonesForModel(modelName: string): void {
this.dropZones
.filter(zone => zone.name.includes(`dropZone_${modelName}_`))
.forEach(zone => zone.setEnabled(true));
}
/**
* 隐藏指定模型的放置区域
*/
hideDropZonesForModel(modelName: string): void {
this.dropZones
.filter(zone => zone.name.includes(`dropZone_${modelName}_`))
.forEach(zone => zone.setEnabled(false));
}
/**
* 检查某个位置是否在放置区域内
* @param position 要检查的位置
* @returns 如果在放置区域内,返回该区域的配置信息,否则返回 null
*/
checkInDropZone(position: Vector3): { zone: Mesh; config: any } | null {
for (const zone of this.dropZones) {
if (!zone.isEnabled()) continue;
// 简单的距离检测
const distance = Vector3.Distance(
new Vector3(position.x, 0, position.z),
new Vector3(zone.position.x, 0, zone.position.z)
);
// 获取区域的宽度(从 scaling 或原始尺寸计算)
const zoneBounds = zone.getBoundingInfo();
const zoneSize = zoneBounds.boundingBox.extendSize;
const maxDistance = Math.max(zoneSize.x, zoneSize.z);
if (distance < maxDistance) {
// 找到对应的配置
const modelName = zone.name.split('_')[1];
const configs = this.dropZoneConfigs.get(modelName);
const configIndex = parseInt(zone.name.split('_').pop() || '0');
const config = configs ? configs.find(c => c.index === configIndex) : null;
return { zone, config };
}
}
return null;
}
/**
* 高亮某个放置区域(鼠标悬停效果)
*/
highlightDropZone(zone: Mesh): void {
const material = zone.material as StandardMaterial;
if (material) {
material.alpha = 0.6; // 增加透明度
material.emissiveColor = new Color3(0.2, 0.2, 0.2); // 添加发光效果
}
}
/**
* 取消高亮
*/
unhighlightDropZone(zone: Mesh): void {
const material = zone.material as StandardMaterial;
if (material) {
material.alpha = 0.3; // 恢复透明度
material.emissiveColor = new Color3(0, 0, 0); // 移除发光效果
}
}
/**
* 清除所有放置区域
*/
clearAllDropZones(): void {
this.dropZones.forEach(zone => {
zone.dispose();
});
this.dropZones = [];
this.dropZoneConfigs.clear();
}
/**
* 清除指定模型的放置区域
*/
clearDropZonesForModel(modelName: string): void {
const zonesToRemove = this.dropZones.filter(zone =>
zone.name.includes(`dropZone_${modelName}_`)
);
zonesToRemove.forEach(zone => {
zone.dispose();
const index = this.dropZones.indexOf(zone);
if (index > -1) {
this.dropZones.splice(index, 1);
}
});
this.dropZoneConfigs.delete(modelName);
generateDropZones(config: DropZoneConfig): PlacementZoneInfo[] {
return this.placementWall.generatePlacementAreas(config);
}
/**
* 获取所有放置区域
*/
getAllDropZones(): Mesh[] {
return this.dropZones;
getPlacementZones(): PlacementZoneInfo[] {
return this.placementWall.getPlacementZones();
}
/**
* 获取指定模型的放置区域配置
* 根据墙面名称获取放置区域
*/
getDropZoneConfigsForModel(modelName: string): any[] {
return this.dropZoneConfigs.get(modelName) || [];
getZonesByWall(wallName: string): PlacementZoneInfo[] {
return this.placementWall.getZonesByWall(wallName);
}
/**
* 显示模型的包围盒
* @param modelName 模型名称
* @param color 包围盒颜色
* 根据索引获取特定放置区域
*/
private showBoundingBox(modelName: string, color: string = '#ff0000'): void {
// 查找目标模型
let targetMeshes: AbstractMesh[] | undefined;
const mainApp = (this.scene as any).mainApp;
if (mainApp?.appModel) {
targetMeshes = mainApp.appModel.getCachedMeshes(modelName);
}
if (!targetMeshes || targetMeshes.length === 0) {
const mesh = this.scene.getMeshByName(modelName);
if (mesh) {
targetMeshes = [mesh];
}
}
if (!targetMeshes || targetMeshes.length === 0) {
console.warn(`模型 ${modelName} 不存在`);
return;
}
// 计算总包围盒(使用世界坐标)
let minX = Infinity, minY = Infinity, minZ = Infinity;
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
targetMeshes.forEach(mesh => {
// 强制更新世界矩阵
mesh.computeWorldMatrix(true);
const boundingInfo = mesh.getBoundingInfo();
// 获取世界空间的包围盒
const worldMin = boundingInfo.boundingBox.minimumWorld;
const worldMax = boundingInfo.boundingBox.maximumWorld;
minX = Math.min(minX, worldMin.x);
minY = Math.min(minY, worldMin.y);
minZ = Math.min(minZ, worldMin.z);
maxX = Math.max(maxX, worldMax.x);
maxY = Math.max(maxY, worldMax.y);
maxZ = Math.max(maxZ, worldMax.z);
});
// 创建包围盒的8个顶点
const corners = [
new Vector3(minX, minY, minZ),
new Vector3(maxX, minY, minZ),
new Vector3(maxX, minY, maxZ),
new Vector3(minX, minY, maxZ),
new Vector3(minX, maxY, minZ),
new Vector3(maxX, maxY, minZ),
new Vector3(maxX, maxY, maxZ),
new Vector3(minX, maxY, maxZ)
];
// 创建12条边
const edges = [
// 底面4条边
[corners[0], corners[1]],
[corners[1], corners[2]],
[corners[2], corners[3]],
[corners[3], corners[0]],
// 顶面4条边
[corners[4], corners[5]],
[corners[5], corners[6]],
[corners[6], corners[7]],
[corners[7], corners[4]],
// 4条竖边
[corners[0], corners[4]],
[corners[1], corners[5]],
[corners[2], corners[6]],
[corners[3], corners[7]]
];
const rgb = this.hexToRgb(color);
const lineColor = new Color3(rgb.r, rgb.g, rgb.b);
edges.forEach((edge, index) => {
const line = MeshBuilder.CreateLines(`boundingBox_${modelName}_${index}`, {
points: edge
}, this.scene);
line.color = lineColor;
this.boundingBoxLines.push(line);
});
getZone(wallName: string, index: number): PlacementZoneInfo | undefined {
return this.placementWall.getZone(wallName, index);
}
/**
* 隐藏所有包围盒
* 显示所有放置区域
*/
private hideBoundingBox(): void {
this.boundingBoxLines.forEach(line => line.dispose());
this.boundingBoxLines = [];
show(): void {
this.placementWall.show();
}
/**
* 隐藏所有放置区域
*/
hide(): void {
this.placementWall.hide();
}
/**
* 清除所有放置区域
*/
clearAll(): void {
this.placementWall.clearAll();
}
/**
* 销毁
*/
dispose(): void {
this.placementWall.dispose();
}
}