1
This commit is contained in:
@ -32,7 +32,7 @@ export class AppCamera extends Monobehiver {
|
||||
// this.object.upperBetaLimit = Tools.ToRadians(60); // 最大垂直角(接近90度,避免万向锁)
|
||||
// this.object.lowerBetaLimit = Tools.ToRadians(60); // 最小垂直角
|
||||
|
||||
this.object.position = new Vector3(-0, 100, 0);
|
||||
this.object.position = new Vector3(-0, 10, 0);
|
||||
this.setTarget(0, 2, 0);
|
||||
}
|
||||
|
||||
|
||||
540
src/babylonjs/AppDropZone.ts
Normal file
540
src/babylonjs/AppDropZone.ts
Normal file
@ -0,0 +1,540 @@
|
||||
import { Scene, Mesh, MeshBuilder, StandardMaterial, Color3, Vector3, AbstractMesh, BoundingBoxGizmo } from '@babylonjs/core';
|
||||
|
||||
export interface DropZoneConfig {
|
||||
modelName: string; // 目标模型名称
|
||||
divisions: number; // 分割块数(每条边分成几块)
|
||||
color?: string; // 颜色(十六进制)
|
||||
alpha?: number; // 透明度
|
||||
thickness?: number; // 厚度
|
||||
offset?: number; // 距离模型的偏移量
|
||||
scale?: number; // 整体缩放比例(0-1),用于生成内部放置区域
|
||||
}
|
||||
|
||||
export class AppDropZone {
|
||||
private scene: Scene;
|
||||
private dropZones: Mesh[] = [];
|
||||
private dropZoneConfigs: Map<string, any[]> = new Map(); // 存储每个模型的放置区域配置
|
||||
private boundingBoxLines: Mesh[] = []; // 存储包围盒线框
|
||||
|
||||
constructor(scene: Scene) {
|
||||
this.scene = 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有放置区域
|
||||
*/
|
||||
getAllDropZones(): Mesh[] {
|
||||
return this.dropZones;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定模型的放置区域配置
|
||||
*/
|
||||
getDropZoneConfigsForModel(modelName: string): any[] {
|
||||
return this.dropZoneConfigs.get(modelName) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示模型的包围盒
|
||||
* @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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏所有包围盒
|
||||
*/
|
||||
private hideBoundingBox(): void {
|
||||
this.boundingBoxLines.forEach(line => line.dispose());
|
||||
this.boundingBoxLines = [];
|
||||
}
|
||||
}
|
||||
@ -8,6 +8,7 @@ import { Monobehiver } from '../base/Monobehiver';
|
||||
import { Dictionary } from '../utils/Dictionary';
|
||||
import { AppConfig } from './AppConfig';
|
||||
import { EventBridge } from '../event/bridge';
|
||||
import { DragConfig } from './AppModelDrag';
|
||||
|
||||
type LoadResult = {
|
||||
success: boolean;
|
||||
@ -27,6 +28,7 @@ type ModelMetadata = {
|
||||
modelId: string;
|
||||
modelUrl: string;
|
||||
modelControlType?: ModelControlType;
|
||||
drag?: DragConfig;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -221,14 +223,15 @@ export class AppModel extends Monobehiver {
|
||||
return await this.addSingle(
|
||||
modelConfig.modelId,
|
||||
modelConfig.modelUrl,
|
||||
modelConfig.modelControlType
|
||||
modelConfig.modelControlType,
|
||||
modelConfig.drag
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加单个模型
|
||||
*/
|
||||
private async addSingle(modelName: string, modelUrl: string, modelControlType?: ModelControlType): Promise<LoadResult> {
|
||||
private async addSingle(modelName: string, modelUrl: string, modelControlType?: ModelControlType, drag?: DragConfig): Promise<LoadResult> {
|
||||
// 检查是否已存在
|
||||
const existingMeshes = this.modelDic.Get(modelName);
|
||||
if (existingMeshes?.length && !existingMeshes[0].isDisposed()) {
|
||||
@ -250,9 +253,15 @@ export class AppModel extends Monobehiver {
|
||||
this.modelMetadataDic.Set(modelName, {
|
||||
modelId: modelName,
|
||||
modelUrl: modelUrl,
|
||||
modelControlType: modelControlType
|
||||
modelControlType: modelControlType,
|
||||
drag: drag
|
||||
});
|
||||
|
||||
// 配置拖拽功能
|
||||
if (drag) {
|
||||
this.mainApp.appModelDrag?.configureDrag(modelName, drag);
|
||||
}
|
||||
|
||||
// 更新 GameManager 的字典
|
||||
this.mainApp.gameManager?.updateDictionaries();
|
||||
|
||||
@ -274,7 +283,7 @@ export class AppModel extends Monobehiver {
|
||||
EventBridge.modelLoadProgress({ loaded: 0, total, progress: 0, percentage: 0 });
|
||||
|
||||
for (let i = 0; i < models.length; i++) {
|
||||
const { modelId, modelUrl, modelControlType } = models[i];
|
||||
const { modelId, modelUrl, modelControlType, drag } = models[i];
|
||||
|
||||
const result = await this.loadSingleModel(modelUrl, (event) => {
|
||||
this.emitProgress(i, total, modelUrl, event);
|
||||
@ -288,8 +297,14 @@ export class AppModel extends Monobehiver {
|
||||
this.modelMetadataDic.Set(modelId, {
|
||||
modelId: modelId,
|
||||
modelUrl: modelUrl,
|
||||
modelControlType: modelControlType
|
||||
modelControlType: modelControlType,
|
||||
drag: drag
|
||||
});
|
||||
|
||||
// 配置拖拽功能
|
||||
if (drag) {
|
||||
this.mainApp.appModelDrag?.configureDrag(modelId, drag);
|
||||
}
|
||||
}
|
||||
|
||||
results.push(result);
|
||||
@ -410,7 +425,8 @@ export class AppModel extends Monobehiver {
|
||||
return await this.addSingle(
|
||||
modelConfig.modelId,
|
||||
modelConfig.modelUrl,
|
||||
modelConfig.modelControlType
|
||||
modelConfig.modelControlType,
|
||||
modelConfig.drag
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
257
src/babylonjs/AppModelDrag.ts
Normal file
257
src/babylonjs/AppModelDrag.ts
Normal file
@ -0,0 +1,257 @@
|
||||
import { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
|
||||
import { PointerDragBehavior } from '@babylonjs/core/Behaviors/Meshes/pointerDragBehavior';
|
||||
import { Vector3 } from '@babylonjs/core/Maths/math.vector';
|
||||
import { Scene } from '@babylonjs/core/scene';
|
||||
import { Monobehiver } from '../base/Monobehiver';
|
||||
|
||||
/**
|
||||
* 拖拽配置接口
|
||||
*/
|
||||
export interface DragConfig {
|
||||
enable: boolean;
|
||||
axis?: 'x' | 'y' | 'z' | 'xy' | 'xz' | 'yz' | 'xyz';
|
||||
step?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模型拖拽信息
|
||||
*/
|
||||
interface ModelDragInfo {
|
||||
config: DragConfig;
|
||||
behavior: PointerDragBehavior | null;
|
||||
currentAxis: 'x' | 'y' | 'z' | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模型拖拽管理器 - 负责处理模型的拖拽交互
|
||||
*/
|
||||
export class AppModelDrag extends Monobehiver {
|
||||
private modelDragMap: Map<string, ModelDragInfo>;
|
||||
private scene: Scene | null;
|
||||
|
||||
constructor(mainApp: any) {
|
||||
super(mainApp);
|
||||
this.modelDragMap = new Map();
|
||||
this.scene = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化拖拽管理器
|
||||
*/
|
||||
Awake(): void {
|
||||
this.scene = this.mainApp.appScene.object;
|
||||
if (!this.scene) {
|
||||
console.warn('Scene not initialized');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为模型配置拖拽
|
||||
* @param modelId 模型ID
|
||||
* @param config 拖拽配置
|
||||
*/
|
||||
configureDrag(modelId: string, config: DragConfig): void {
|
||||
// 获取模型的根网格
|
||||
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
|
||||
if (!meshes || !meshes.length) {
|
||||
console.warn(`Model not found: ${modelId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const rootMesh = meshes[0]; // 第一个是根节点
|
||||
|
||||
// 如果已存在,先移除旧的行为
|
||||
const existingInfo = this.modelDragMap.get(modelId);
|
||||
if (existingInfo?.behavior) {
|
||||
rootMesh.removeBehavior(existingInfo.behavior);
|
||||
}
|
||||
|
||||
// 创建拖拽信息
|
||||
const dragInfo: ModelDragInfo = {
|
||||
config: { ...config },
|
||||
behavior: null,
|
||||
currentAxis: this.getFirstAvailableAxis(config.axis || 'xyz')
|
||||
};
|
||||
|
||||
if (config.enable) {
|
||||
// 创建并配置拖拽行为
|
||||
dragInfo.behavior = this.createDragBehavior(modelId, dragInfo);
|
||||
rootMesh.addBehavior(dragInfo.behavior);
|
||||
}
|
||||
|
||||
this.modelDragMap.set(modelId, dragInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建拖拽行为
|
||||
*/
|
||||
private createDragBehavior(modelId: string, dragInfo: ModelDragInfo): PointerDragBehavior {
|
||||
const axis = dragInfo.currentAxis;
|
||||
let dragAxis: Vector3;
|
||||
|
||||
// 根据当前激活的轴创建拖拽向量
|
||||
switch (axis) {
|
||||
case 'x':
|
||||
dragAxis = new Vector3(1, 0, 0);
|
||||
break;
|
||||
case 'y':
|
||||
dragAxis = new Vector3(0, 1, 0);
|
||||
break;
|
||||
case 'z':
|
||||
dragAxis = new Vector3(0, 0, 1);
|
||||
break;
|
||||
default:
|
||||
dragAxis = new Vector3(1, 0, 0);
|
||||
}
|
||||
|
||||
// 创建拖拽行为
|
||||
const pointerDragBehavior = new PointerDragBehavior({ dragAxis: dragAxis });
|
||||
|
||||
// 使用世界坐标系而不是物体本地坐标系
|
||||
pointerDragBehavior.useObjectOrientationForDragging = false;
|
||||
|
||||
// 监听拖拽开始事件
|
||||
pointerDragBehavior.onDragStartObservable.add(() => {
|
||||
// 禁用相机控制
|
||||
this.disableCameraControl();
|
||||
});
|
||||
|
||||
// 监听拖拽结束事件
|
||||
pointerDragBehavior.onDragEndObservable.add(() => {
|
||||
// 恢复相机控制
|
||||
this.enableCameraControl();
|
||||
});
|
||||
|
||||
return pointerDragBehavior;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型的拖拽配置
|
||||
* @param modelId 模型ID
|
||||
*/
|
||||
getDragConfig(modelId: string): DragConfig | undefined {
|
||||
return this.modelDragMap.get(modelId)?.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用模型拖拽
|
||||
* @param modelId 模型ID
|
||||
* @param enable 是否启用
|
||||
*/
|
||||
setDragEnabled(modelId: string, enable: boolean): void {
|
||||
const dragInfo = this.modelDragMap.get(modelId);
|
||||
if (!dragInfo) return;
|
||||
|
||||
dragInfo.config.enable = enable;
|
||||
|
||||
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
|
||||
if (!meshes || !meshes.length) return;
|
||||
|
||||
const rootMesh = meshes[0];
|
||||
|
||||
if (enable) {
|
||||
// 启用:创建并添加行为
|
||||
if (!dragInfo.behavior) {
|
||||
dragInfo.behavior = this.createDragBehavior(modelId, dragInfo);
|
||||
rootMesh.addBehavior(dragInfo.behavior);
|
||||
}
|
||||
} else {
|
||||
// 禁用:移除行为
|
||||
if (dragInfo.behavior) {
|
||||
rootMesh.removeBehavior(dragInfo.behavior);
|
||||
dragInfo.behavior = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换激活的轴向
|
||||
* @param modelId 模型ID
|
||||
* @param axis 要激活的轴向
|
||||
*/
|
||||
switchAxis(modelId: string, axis: 'x' | 'y' | 'z'): void {
|
||||
const dragInfo = this.modelDragMap.get(modelId);
|
||||
if (!dragInfo) return;
|
||||
|
||||
// 检查该轴是否在允许的轴向中
|
||||
if (!this.isAxisAllowed(axis, dragInfo.config.axis || 'xyz')) {
|
||||
console.warn(`Axis ${axis} is not allowed for model ${modelId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新当前轴
|
||||
dragInfo.currentAxis = axis;
|
||||
|
||||
// 重新创建拖拽行为
|
||||
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
|
||||
if (!meshes || !meshes.length) return;
|
||||
|
||||
const rootMesh = meshes[0];
|
||||
|
||||
// 移除旧行为
|
||||
if (dragInfo.behavior) {
|
||||
rootMesh.removeBehavior(dragInfo.behavior);
|
||||
}
|
||||
|
||||
// 创建新行为
|
||||
if (dragInfo.config.enable) {
|
||||
dragInfo.behavior = this.createDragBehavior(modelId, dragInfo);
|
||||
rootMesh.addBehavior(dragInfo.behavior);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置中的第一个可用轴
|
||||
*/
|
||||
private getFirstAvailableAxis(axisConfig: string): 'x' | 'y' | 'z' | null {
|
||||
if (axisConfig.includes('x')) return 'x';
|
||||
if (axisConfig.includes('y')) return 'y';
|
||||
if (axisConfig.includes('z')) return 'z';
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查轴是否在允许的配置中
|
||||
*/
|
||||
private isAxisAllowed(axis: 'x' | 'y' | 'z', axisConfig: string): boolean {
|
||||
return axisConfig.includes(axis);
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用相机控制
|
||||
*/
|
||||
private disableCameraControl(): void {
|
||||
const camera = this.mainApp.appCamera?.object;
|
||||
if (camera) {
|
||||
camera.detachControl();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用相机控制
|
||||
*/
|
||||
private enableCameraControl(): void {
|
||||
const camera = this.mainApp.appCamera?.object;
|
||||
const canvas = this.mainApp.appEngin?.object?.getRenderingCanvas();
|
||||
if (camera && canvas) {
|
||||
camera.attachControl(canvas, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
dispose(): void {
|
||||
// 移除所有拖拽行为
|
||||
this.modelDragMap.forEach((dragInfo, modelId) => {
|
||||
if (dragInfo.behavior) {
|
||||
const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
|
||||
if (meshes && meshes.length) {
|
||||
meshes[0].removeBehavior(dragInfo.behavior);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.modelDragMap.clear();
|
||||
}
|
||||
}
|
||||
@ -746,42 +746,70 @@ export class GameManager extends Monobehiver {
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用材质
|
||||
* @param target 目标对象
|
||||
* @param material 材质路径
|
||||
* 应用材质属性
|
||||
* @param options 材质配置选项
|
||||
*/
|
||||
applyMaterial(target: string, attribute: string, value:string): void {
|
||||
if (attribute !== 'baseColor' || typeof value !== 'string') return;
|
||||
applyMaterial(options: {
|
||||
target: string;
|
||||
albedoColor?: string;
|
||||
albedoTexture?: string;
|
||||
normalMap?: string;
|
||||
metallicTexture?: string;
|
||||
roughness?: number;
|
||||
metallic?: number;
|
||||
}): void {
|
||||
this.updateDictionaries();
|
||||
|
||||
// 示例实现:根据目标和材质路径应用材质
|
||||
// 1. 查找目标网格
|
||||
// 1. 查找目标材质
|
||||
const targetMaterials: PBRMaterial[] = [];
|
||||
this.materialDic.Values().forEach(material => {
|
||||
if (material.name === target) {
|
||||
if (material.name === options.target) {
|
||||
targetMaterials.push(material);
|
||||
}
|
||||
});
|
||||
|
||||
if (targetMaterials.length === 0) {
|
||||
console.warn(`Target not found: ${target}`);
|
||||
console.warn(`Material not found: ${options.target}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 处理材质路径
|
||||
// 这里可以根据材质路径加载对应的材质配置
|
||||
// 例如:paint/blue 可以映射到特定的材质配置
|
||||
|
||||
// 3. 应用材质到目标网格
|
||||
const color = Color3.FromHexString(value);
|
||||
// 2. 应用材质属性到目标材质
|
||||
targetMaterials.forEach(material => {
|
||||
|
||||
// 如果是 baseColor 且值是字符串(16进制颜色),转换为 Color3
|
||||
// 应用颜色
|
||||
if (options.albedoColor) {
|
||||
const color = Color3.FromHexString(options.albedoColor);
|
||||
material.albedoColor.copyFrom(color);
|
||||
}
|
||||
|
||||
// 如果有纹理,颜色会作为纹理的乘法因子
|
||||
// 强制刷新材质
|
||||
material.markDirty();
|
||||
// 应用反照率纹理(颜色贴图)
|
||||
if (options.albedoTexture) {
|
||||
material.albedoTexture = new Texture(options.albedoTexture);
|
||||
}
|
||||
|
||||
// 应用法线贴图
|
||||
if (options.normalMap) {
|
||||
material.bumpTexture = new Texture(options.normalMap);
|
||||
}
|
||||
|
||||
// 应用金属度贴图
|
||||
if (options.metallicTexture) {
|
||||
material.metallicTexture = new Texture(options.metallicTexture);
|
||||
}
|
||||
|
||||
// 应用粗糙度值
|
||||
if (options.roughness !== undefined) {
|
||||
material.roughness = options.roughness;
|
||||
}
|
||||
|
||||
// 应用金属度值
|
||||
if (options.metallic !== undefined) {
|
||||
material.metallic = options.metallic;
|
||||
}
|
||||
|
||||
// 强制刷新材质
|
||||
material.markDirty();
|
||||
});
|
||||
|
||||
console.log(`Material applied to: ${options.target}`, options);
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,8 @@ import { AppHotspot } from './AppHotspot';
|
||||
import { AppDomTo3D } from './AppDomTo3D';
|
||||
import { AppSelectionOutline } from './AppSelectionOutline';
|
||||
import { AppPositionGizmo } from './AppPositionGizmo';
|
||||
import { AppModelDrag } from './AppModelDrag';
|
||||
import { AppDropZone } from './AppDropZone';
|
||||
|
||||
/**
|
||||
* 主应用类 - 3D场景的核心控制器
|
||||
@ -34,6 +36,8 @@ export class MainApp {
|
||||
appDomTo3D: AppDomTo3D;
|
||||
appSelectionOutline: AppSelectionOutline;
|
||||
appPositionGizmo: AppPositionGizmo;
|
||||
appModelDrag: AppModelDrag;
|
||||
appDropZone: AppDropZone;
|
||||
gameManager: GameManager;
|
||||
|
||||
|
||||
@ -49,6 +53,7 @@ export class MainApp {
|
||||
this.appDomTo3D = new AppDomTo3D(this);
|
||||
this.appSelectionOutline = new AppSelectionOutline(this);
|
||||
this.appPositionGizmo = new AppPositionGizmo(this);
|
||||
this.appModelDrag = new AppModelDrag(this);
|
||||
this.gameManager = new GameManager(this);
|
||||
|
||||
window.addEventListener("resize", () => this.appEngin.handleResize());
|
||||
@ -85,8 +90,11 @@ export class MainApp {
|
||||
this.appRay.Awake();
|
||||
this.appSelectionOutline.init();
|
||||
this.appPositionGizmo.Awake();
|
||||
this.appModelDrag.Awake();
|
||||
this.appDomTo3D.init();
|
||||
this.appModel.initManagers();
|
||||
// 在场景创建后初始化 AppDropZone
|
||||
this.appDropZone = new AppDropZone(this.appScene.object);
|
||||
this.update();
|
||||
EventBridge.sceneReady({ scene: this.appScene.object });
|
||||
}
|
||||
@ -106,6 +114,7 @@ export class MainApp {
|
||||
this.appModel?.clean();
|
||||
this.appEnv?.clean();
|
||||
this.appPositionGizmo?.dispose();
|
||||
this.appModelDrag?.dispose();
|
||||
// this.appHotspot?.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,7 +73,18 @@ export const once = (eventName: string, callback: (...args: unknown[]) => void,
|
||||
};
|
||||
|
||||
export const emit = (eventName: string, ...args: unknown[]): Emitter => {
|
||||
return eventBus.emit(eventName, ...args);
|
||||
// 触发内部事件总线
|
||||
const result = eventBus.emit(eventName, ...args);
|
||||
|
||||
// 同时触发 window 自定义事件,方便外部监听
|
||||
if (typeof window !== 'undefined') {
|
||||
const customEvent = new CustomEvent(eventName, {
|
||||
detail: args[0] // 传递第一个参数作为 detail
|
||||
});
|
||||
window.dispatchEvent(customEvent);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const removeAllListeners = (eventName?: string): Emitter => {
|
||||
|
||||
@ -66,17 +66,18 @@ export class KernelAdapter {
|
||||
* 应用材质
|
||||
* @param options 材质应用选项
|
||||
*/
|
||||
apply: (options: { target: string; attribute: string,value:string }): void => {
|
||||
this.mainApp.gameManager.applyMaterial(options.target, options.attribute,options.value);
|
||||
apply: (options: {
|
||||
target: string;
|
||||
albedoColor?: string;
|
||||
albedoTexture?: string;
|
||||
normalMap?: string;
|
||||
metallicTexture?: string;
|
||||
roughness?: number;
|
||||
metallic?: number;
|
||||
}): void => {
|
||||
this.mainApp.gameManager.applyMaterial(options);
|
||||
},
|
||||
/**
|
||||
* 更换材质颜色
|
||||
* @param materialName 材质名称
|
||||
* @param hexColor 16进制颜色值,例如 "#FF0000"
|
||||
*/
|
||||
color: (materialName: string, hexColor: string): void => {
|
||||
this.mainApp.gameManager.applyMaterial(materialName, 'baseColor', hexColor);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/** 卷帘门控<E997A8>?*/
|
||||
@ -214,6 +215,77 @@ export class KernelAdapter {
|
||||
}
|
||||
};
|
||||
|
||||
/** 放置区域管理 */
|
||||
dropZone = {
|
||||
/**
|
||||
* 根据模型包围盒生成四周的放置区域
|
||||
* @param options 配置选项
|
||||
* @example
|
||||
* kernel.dropZone.generate({
|
||||
* modelName: "框架",
|
||||
* divisions: 4,
|
||||
* color: "#21c7ff",
|
||||
* alpha: 0.3,
|
||||
* scale: 0.8 // 缩小到80%,用于内部放置区域
|
||||
* });
|
||||
*/
|
||||
generate: (options: {
|
||||
modelName: string;
|
||||
divisions: number;
|
||||
color?: string;
|
||||
alpha?: number;
|
||||
thickness?: number;
|
||||
offset?: number;
|
||||
scale?: number;
|
||||
}): any[] => {
|
||||
return this.mainApp.appDropZone.generateDropZones(options);
|
||||
},
|
||||
/**
|
||||
* 显示所有放置区域
|
||||
*/
|
||||
showAll: (): void => {
|
||||
this.mainApp.appDropZone.showAllDropZones();
|
||||
},
|
||||
/**
|
||||
* 隐藏所有放置区域
|
||||
*/
|
||||
hideAll: (): void => {
|
||||
this.mainApp.appDropZone.hideAllDropZones();
|
||||
},
|
||||
/**
|
||||
* 显示指定模型的放置区域
|
||||
*/
|
||||
show: (modelName: string): void => {
|
||||
this.mainApp.appDropZone.showDropZonesForModel(modelName);
|
||||
},
|
||||
/**
|
||||
* 隐藏指定模型的放置区域
|
||||
*/
|
||||
hide: (modelName: string): void => {
|
||||
this.mainApp.appDropZone.hideDropZonesForModel(modelName);
|
||||
},
|
||||
/**
|
||||
* 清除所有放置区域
|
||||
*/
|
||||
clearAll: (): void => {
|
||||
this.mainApp.appDropZone.clearAllDropZones();
|
||||
},
|
||||
/**
|
||||
* 清除指定模型的放置区域
|
||||
*/
|
||||
clear: (modelName: string): void => {
|
||||
this.mainApp.appDropZone.clearDropZonesForModel(modelName);
|
||||
},
|
||||
/**
|
||||
* 检查某个位置是否在放置区域内
|
||||
*/
|
||||
checkPosition: (position: [number, number, number]): any => {
|
||||
const { Vector3 } = require('@babylonjs/core');
|
||||
const pos = new Vector3(position[0], position[1], position[2]);
|
||||
return this.mainApp.appDropZone.checkInDropZone(pos);
|
||||
}
|
||||
};
|
||||
|
||||
debug = {
|
||||
/** 列出当前场景网格名称 */
|
||||
listMeshNames: (): string[] => {
|
||||
|
||||
Reference in New Issue
Block a user