From 48456acd3d52a6492fb7d200c093feaaff277bf8 Mon Sep 17 00:00:00 2001
From: zguiy <1415466602@qq.com>
Date: Fri, 29 May 2026 14:24:59 +0800
Subject: [PATCH] 1
---
examples/app-global.js | 84 ++---
examples/demo-global.html | 8 +-
examples/demo-module.html | 71 +++--
examples/index.global.js | 614 ++++++++++++++++++++++++++++++++++++-
index.html | 74 +++--
index.js | 17 +-
src/babylonjs/AppCamera.ts | 2 +-
7 files changed, 755 insertions(+), 115 deletions(-)
diff --git a/examples/app-global.js b/examples/app-global.js
index dce56a5..71232b6 100644
--- a/examples/app-global.js
+++ b/examples/app-global.js
@@ -181,44 +181,48 @@ const getEvent = async (dropzone_data, sku) => {
//点击放置区域执行事件 一般是换配件
const executeEvent = async (dropzone_data, result, sku) => {
- const kernel = getKernel();
+ const kernel = getKernel();
+
+ const { wallName, index, transform } = dropzone_data;
+ const { position, rotation } = transform;
+
+ let modelId = null; // 在外部声明,用于在两个循环之间传递
+ let modelName = null;
+ let pergolaSku = null; // 用于存储棚子的 SKU
+
+ // 第一次循环:处理 change_model
+ for (const event of result.data.events) {
+ if (event.event_type === 'change_model') {
+ const { name, file_url, model_control_type, category } = event.target_data;
- const { wallName, index, transform } = dropzone_data;
- const { position, rotation } = transform;
-
- let modelId = null; // 在外部声明,用于在两个循环之间传递
- let modelName = null;
- let pergolaSku = null; // 用于存储棚子的 SKU
-
- // 第一次循环:处理 change_model
- for (const event of result.data.events) {
- if (event.event_type === 'change_model') {
- const { name, file_url, model_control_type, category } = event.target_data;
-
- // 生成唯一的模型ID
- modelId = Date.now();
- modelName = name;
- kernel.dropZone.recordModelPlacement(wallName, index, name + '_' + modelId);
-
- await kernel.model.add({
- modelName: name,
- modelId: modelId,
- modelUrl: file_url,
- modelControlType: model_control_type,
- drag: {
- enable: true,
- axis: rotation.y === 0 || rotation.y === 180 ? 'x' : 'z',
- step: 0.1,
- },
- transform: {
- position: position,
- rotation: rotation,
- }
- });
-
- console.log(`百叶模型已放置为 ${name + '_' + modelId}`);
- }
- }
+ // 生成唯一的模型ID
+ modelId = Date.now();
+ modelName = name;
+ kernel.dropZone.recordModelPlacement(wallName, index, name + '_' + modelId);
+
+ await kernel.model.add({
+ modelName: name,
+ modelId: modelId,
+ modelUrl: file_url,
+ modelControlType: model_control_type,
+ drag: {
+ enable: true,
+ axis: rotation.y === 0 || rotation.y === 180 ? 'x' : 'z',
+ step: 0.1,
+ snapToZone: true, // 拖拽吸附到最近的分割区域
+ returnWhenOutOfBounds: true, // 拖拽到区域外时返回原位置
+ handleOccupiedZone: true, // 处理已占用区域(false=允许重叠)
+ occupiedZoneAction: 'replace' // 当开关3=true时的行为:'return'=返回原位置,'replace'=替换
+ },
+ transform: {
+ position: position,
+ rotation: rotation,
+ }
+ });
+
+ console.log(`百叶模型已放置为 ${name + '_' + modelId}`);
+ }
+ }
// 第二次循环:处理 change_color(此时模型已加载完成)
for (const event of result.data.events) {
@@ -265,7 +269,7 @@ const isModelExists = (modelId) => {
}
//一般是换棚子/换颜色/设置放置区域
- const executeEvent2 = async (result, sku) => {
+const executeEvent2 = async (result, sku) => {
const kernel = getKernel();
// 检查是否有模型更换事件
@@ -418,8 +422,8 @@ const getProductConfig = async (sku) => {
}
// API 配置
-const API_BASE_URL = 'https://ztserver.zguiy.com';
-
+//const API_BASE_URL = 'https://ztserver.zguiy.com';
+const API_BASE_URL = 'http://localhost:26517';
const getApiUrl = (path) => {
return `${API_BASE_URL}${path}`;
};
diff --git a/examples/demo-global.html b/examples/demo-global.html
index 71dcf1f..c6e9afa 100644
--- a/examples/demo-global.html
+++ b/examples/demo-global.html
@@ -453,7 +453,7 @@
@@ -847,9 +854,25 @@
// 监听放置区域点击事件
kernel.on('dropzone:click', async (dropzone_data) => {
+ // 显示进度条
+ const progressContainer = document.getElementById('progress-container');
+ if (progressContainer) {
+ progressContainer.style.display = 'flex';
+ }
+
getEvent(dropzone_data, sku)
});
+ // 监听模型加载完成事件
+ kernel.on('model:loaded', (data) => {
+ console.log('模型加载完成', data);
+ // 隐藏进度条
+ const progressContainer = document.getElementById('progress-container');
+ if (progressContainer) {
+ progressContainer.style.display = 'none';
+ }
+ });
+
diff --git a/examples/index.global.js b/examples/index.global.js
index 571afc6..addec36 100644
--- a/examples/index.global.js
+++ b/examples/index.global.js
@@ -79877,10 +79877,8 @@ discard;}}
}
/** 初始化灯光并开启阴影 */
Awake() {
- new DirectionalLight(
- "mainLight",
- new Vector3(0, -0.5, -1)
- );
+ const light = new DirectionalLight("sun", new Vector3(-1, -2, -1), this.mainApp.appScene.object);
+ light.intensity = 1.2;
}
}
@@ -354748,6 +354746,22 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
}
if (drag) {
this.mainApp.appModelDrag?.configureDrag(modelName + "_" + modelId, drag);
+ if (this.mainApp.appDropZone) {
+ let modelWallName = null;
+ const fullModelId = modelName + "_" + modelId;
+ this.mainApp.appDropZone["zoneModelMap"]?.forEach((id, zoneKey) => {
+ if (id === fullModelId) {
+ const match = zoneKey.match(/^(.+)\[(\d+)\]$/);
+ if (match) {
+ modelWallName = match[1];
+ }
+ }
+ });
+ if (modelWallName && this.mainApp.appDropZone.isWallFull(modelWallName)) {
+ console.log(`[拖拽控制] 墙面 ${modelWallName} 已满,禁用模型 ${fullModelId} 的拖拽`);
+ this.mainApp.appModelDrag?.setDragEnabled(fullModelId, false);
+ }
+ }
}
this.mainApp.gameManager?.updateDictionaries();
EventBridge.modelLoaded({ urls: [modelUrl] });
@@ -354907,6 +354921,9 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
this.modelDic.Remove(modelName);
this.modelMetadataDic.Remove(modelName);
this.mainApp.gameManager?.updateDictionaries();
+ if (this.mainApp.appDropZone && typeof this.mainApp.appDropZone.notifyModelRemoved === "function") {
+ this.mainApp.appDropZone.notifyModelRemoved(modelName);
+ }
return true;
}
/**
@@ -355067,9 +355084,10 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
* 将模型放置到指定的放置区域
* @param modelId 模型ID
* @param zoneInfo 放置区域信息
- * @param offsetDistance 距离墙面的偏移距离(默认0.1,正数向外)
+ * @param offsetDistance 距离墙面的偏移距离(默认0,正数向外)
*/
placeToZone(modelId, zoneInfo, offsetDistance = 0) {
+ console.log(zoneInfo);
const meshes = this.modelDic.Get(modelId);
if (!meshes?.length) {
console.warn(`Model not found: ${modelId}`);
@@ -355119,6 +355137,12 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
highlightLayer = null;
originalMaterial = null;
highlightedMesh = null;
+ pointerDownTime = 0;
+ pointerDownPickInfo = null;
+ longPressTimer = null;
+ longPressThreshold = 500;
+ // 长按阈值(毫秒)
+ isLongPress = false;
constructor(mainApp) {
super(mainApp);
}
@@ -355143,29 +355167,85 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
}
if (type === PointerEventTypes.POINTERDOWN) {
this.oldPoint.set(pointerEvent.clientX, 0, pointerEvent.clientY);
+ this.pointerDownTime = Date.now();
+ this.pointerDownPickInfo = pickInfo;
+ this.isLongPress = false;
+ if (this.longPressTimer) {
+ clearTimeout(this.longPressTimer);
+ }
+ this.longPressTimer = setTimeout(() => {
+ this.isLongPress = true;
+ this.handleLongPress(pointerEvent, this.pointerDownPickInfo);
+ }, this.longPressThreshold);
} else if (type === PointerEventTypes.POINTERUP) {
+ if (this.longPressTimer) {
+ clearTimeout(this.longPressTimer);
+ this.longPressTimer = null;
+ }
this.newPoint.set(pointerEvent.clientX, 0, pointerEvent.clientY);
const distance = Vector3.Distance(this.oldPoint, this.newPoint);
- if (distance < 5) {
+ if (this.isLongPress) {
+ console.log("[长按] 松手,隐藏分割区域");
+ this.mainApp.appDropZone.hide();
+ }
+ if (distance < 5 && !this.isLongPress) {
this.handleSingleClick(pointerEvent, pickInfo);
}
+ this.isLongPress = false;
+ } else if (type === PointerEventTypes.POINTERMOVE) {
+ this.newPoint.set(pointerEvent.clientX, 0, pointerEvent.clientY);
+ const distance = Vector3.Distance(this.oldPoint, this.newPoint);
+ if (distance > 5 && this.longPressTimer) {
+ clearTimeout(this.longPressTimer);
+ this.longPressTimer = null;
+ }
}
});
}
+ // 处理长按
+ handleLongPress(evt, pickInfo) {
+ if (pickInfo && pickInfo.hit && pickInfo.pickedMesh) {
+ if (pickInfo.pickedMesh.metadata?.type === "hotspot") {
+ return;
+ }
+ if (pickInfo.pickedMesh.name.startsWith("placement_")) {
+ return;
+ }
+ const modelName = this.mainApp.appModel.findModelNameByMesh(pickInfo.pickedMesh);
+ if (modelName) {
+ const wallName = this.findModelWallName(modelName);
+ if (wallName) {
+ console.log(`[长按] 模型 ${modelName} 位于墙面 ${wallName},显示该墙面的放置区域`);
+ this.mainApp.appDropZone.showWall(wallName);
+ }
+ }
+ }
+ }
+ // 查找模型所在的墙面
+ findModelWallName(modelId) {
+ const zoneModelMap = this.mainApp.appDropZone["zoneModelMap"];
+ if (!zoneModelMap) return null;
+ for (const [zoneKey, id] of zoneModelMap.entries()) {
+ if (id === modelId) {
+ const match = zoneKey.match(/^(.+)\[(\d+)\]$/);
+ if (match) {
+ return match[1];
+ }
+ }
+ }
+ return null;
+ }
// 处理单击
handleSingleClick(evt, pickInfo) {
if (pickInfo && pickInfo.hit && pickInfo.pickedMesh && pickInfo.pickedPoint) {
if (pickInfo.pickedMesh.metadata?.type === "hotspot") {
return;
}
- if (pickInfo.pickedMesh.name === "gridGround" || pickInfo.pickedMesh.name === "ground" || pickInfo.pickedMesh.name === "sphere_yundong_mesh") {
- return;
- }
if (pickInfo.pickedMesh.name.startsWith("placement_")) {
const zones = this.mainApp.appDropZone.getPlacementZones();
const clickedZone = zones.find((zone) => zone.mesh === pickInfo.pickedMesh);
if (clickedZone) {
- const offsetDistance = 0.05;
+ const offsetDistance = 0;
const targetPosition = clickedZone.center.add(clickedZone.normal.scale(offsetDistance));
const targetDirection = clickedZone.normal.scale(-1);
const angle = Math.atan2(targetDirection.x, targetDirection.z);
@@ -355186,7 +355266,6 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
rotation: {
x: 0,
y: angle * 180 / Math.PI,
- // 转换为角度
z: 0
},
scale: {
@@ -355200,6 +355279,7 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
}
}
this.mainApp.appDomTo3D.hideAll();
+ this.mainApp.appDropZone.hide();
const materialName = pickInfo.pickedMesh.material?.name || "";
const holdingShift = Boolean(evt.shiftKey);
const modelMeshes = this.mainApp.appModel.getModelMeshesByMesh(pickInfo.pickedMesh);
@@ -355222,7 +355302,6 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
modelControlType: modelMetadata?.modelControlType
});
} else {
- console.log(1111);
this.mainApp.appSelectionOutline.clear();
this.mainApp.appPositionGizmo.detach();
this.mainApp.appDomTo3D.hideAll();
@@ -356535,11 +356614,44 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
}
const pointerDragBehavior = new PointerDragBehavior({ dragAxis });
pointerDragBehavior.useObjectOrientationForDragging = false;
+ let dragStartPosition = null;
+ let hasShownZones = false;
pointerDragBehavior.onDragStartObservable.add(() => {
+ const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
+ if (meshes && meshes.length > 0) {
+ dragStartPosition = meshes[0].position.clone();
+ }
this.disableCameraControl();
+ hasShownZones = false;
+ });
+ pointerDragBehavior.onDragObservable.add((event) => {
+ const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
+ if (meshes && meshes.length > 0 && dragStartPosition) {
+ const distance = Vector3.Distance(dragStartPosition, meshes[0].position);
+ if (distance > 0.01 && !hasShownZones && dragInfo.config.snapToZone) {
+ this.showZonesForModel(modelId);
+ hasShownZones = true;
+ }
+ }
});
pointerDragBehavior.onDragEndObservable.add(() => {
+ const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
+ let hasMoved = false;
+ if (meshes && meshes.length > 0 && dragStartPosition) {
+ const distance = Vector3.Distance(dragStartPosition, meshes[0].position);
+ hasMoved = distance > 0.01;
+ }
this.enableCameraControl();
+ if (hasMoved) {
+ if (dragInfo.config.snapToZone && hasShownZones) {
+ this.hideZonesForModel(modelId);
+ this.snapModelToZone(modelId);
+ } else {
+ this.updateModelZoneMapping(modelId);
+ }
+ }
+ dragStartPosition = null;
+ hasShownZones = false;
});
return pointerDragBehavior;
}
@@ -356632,6 +356744,259 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
camera.attachControl(canvas, true);
}
}
+ /**
+ * 显示模型所在墙面的分割区域
+ * @param modelId 模型ID
+ */
+ showZonesForModel(modelId) {
+ const appDropZone = this.mainApp.appDropZone;
+ if (!appDropZone) return;
+ let wallName = null;
+ appDropZone["zoneModelMap"]?.forEach((id, zoneKey) => {
+ if (id === modelId) {
+ const match = zoneKey.match(/^(.+)\[(\d+)\]$/);
+ if (match) {
+ wallName = match[1];
+ }
+ }
+ });
+ if (wallName) {
+ console.log(`[拖拽吸附] 显示墙面 ${wallName} 的分割区域`);
+ appDropZone.showWall(wallName);
+ }
+ }
+ /**
+ * 隐藏分割区域
+ * @param modelId 模型ID
+ */
+ hideZonesForModel(modelId) {
+ const appDropZone = this.mainApp.appDropZone;
+ if (!appDropZone) return;
+ console.log(`[拖拽吸附] 隐藏分割区域`);
+ appDropZone.hide();
+ }
+ /**
+ * 将模型吸附到最近的分割区域
+ * @param modelId 模型ID
+ */
+ snapModelToZone(modelId) {
+ const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
+ if (!meshes || !meshes.length) return;
+ const rootMesh = meshes[0];
+ const appDropZone = this.mainApp.appDropZone;
+ if (!appDropZone) return;
+ let wallName = null;
+ let originalZoneIndex = -1;
+ appDropZone["zoneModelMap"]?.forEach((id, zoneKey) => {
+ if (id === modelId) {
+ const match = zoneKey.match(/^(.+)\[(\d+)\]$/);
+ if (match) {
+ wallName = match[1];
+ originalZoneIndex = parseInt(match[2]);
+ }
+ }
+ });
+ if (!wallName) return;
+ const wallZones = appDropZone.getZonesByWall(wallName);
+ if (!wallZones.length) return;
+ let closestZoneIndex = -1;
+ let minDistance = Number.POSITIVE_INFINITY;
+ wallZones.forEach((zone, index) => {
+ const distance = rootMesh.position.subtract(zone.center).length();
+ if (distance < minDistance) {
+ minDistance = distance;
+ closestZoneIndex = index;
+ }
+ });
+ if (closestZoneIndex === -1) return;
+ const dragInfo = this.modelDragMap.get(modelId);
+ const returnWhenOutOfBounds = dragInfo?.config.returnWhenOutOfBounds ?? false;
+ const handleOccupiedZone = dragInfo?.config.handleOccupiedZone ?? false;
+ const occupiedZoneAction = dragInfo?.config.occupiedZoneAction ?? "return";
+ let isOutOfBounds = false;
+ let minX = Number.POSITIVE_INFINITY;
+ let maxX = Number.NEGATIVE_INFINITY;
+ let minZ = Number.POSITIVE_INFINITY;
+ let maxZ = Number.NEGATIVE_INFINITY;
+ wallZones.forEach((zone) => {
+ const halfWidth = zone.width / 2;
+ if (Math.abs(zone.normal.x) > 0.5) {
+ minZ = Math.min(minZ, zone.center.z - halfWidth);
+ maxZ = Math.max(maxZ, zone.center.z + halfWidth);
+ } else if (Math.abs(zone.normal.z) > 0.5) {
+ minX = Math.min(minX, zone.center.x - halfWidth);
+ maxX = Math.max(maxX, zone.center.x + halfWidth);
+ }
+ });
+ const currentPos = rootMesh.position;
+ if (minX !== Number.POSITIVE_INFINITY && maxX !== Number.NEGATIVE_INFINITY) {
+ if (currentPos.x < minX || currentPos.x > maxX) {
+ isOutOfBounds = true;
+ }
+ }
+ if (minZ !== Number.POSITIVE_INFINITY && maxZ !== Number.NEGATIVE_INFINITY) {
+ if (currentPos.z < minZ || currentPos.z > maxZ) {
+ isOutOfBounds = true;
+ }
+ }
+ if (isOutOfBounds) {
+ console.log(`[拖拽吸附] 模型 ${modelId} 超出边界`);
+ if (returnWhenOutOfBounds) {
+ console.log(`[拖拽吸附] 启用边界返回,回到原区域 ${originalZoneIndex}`);
+ if (originalZoneIndex !== -1) {
+ const originalZone = wallZones[originalZoneIndex];
+ if (originalZone) {
+ const offsetDistance2 = -0.05;
+ const returnPosition = originalZone.center.add(originalZone.normal.scale(offsetDistance2));
+ rootMesh.position.copyFrom(returnPosition);
+ const targetDirection2 = originalZone.normal.scale(-1);
+ const angle2 = Math.atan2(targetDirection2.x, targetDirection2.z);
+ rootMesh.rotation.y = angle2;
+ console.log(`[拖拽吸附] 模型 ${modelId} 已返回原区域 ${originalZoneIndex}`);
+ return;
+ }
+ }
+ } else {
+ console.log(`[拖拽吸附] 未启用边界返回,保持当前位置,不做吸附`);
+ this.updateModelZoneMapping(modelId);
+ return;
+ }
+ }
+ const targetZone = wallZones[closestZoneIndex];
+ const targetZoneKey = `${wallName}[${closestZoneIndex}]`;
+ const occupyingModelId = appDropZone["zoneModelMap"]?.get(targetZoneKey);
+ if (occupyingModelId && occupyingModelId !== modelId) {
+ console.log(`[拖拽吸附] 目标区域 ${closestZoneIndex} 已被模型 ${occupyingModelId} 占用`);
+ if (handleOccupiedZone) {
+ if (occupiedZoneAction === "return") {
+ console.log(`[拖拽吸附] 配置为返回原位置,回到区域 ${originalZoneIndex}`);
+ if (originalZoneIndex !== -1) {
+ const originalZone = wallZones[originalZoneIndex];
+ if (originalZone) {
+ const offsetDistance2 = -0.05;
+ const returnPosition = originalZone.center.add(originalZone.normal.scale(offsetDistance2));
+ rootMesh.position.copyFrom(returnPosition);
+ const targetDirection2 = originalZone.normal.scale(-1);
+ const angle2 = Math.atan2(targetDirection2.x, targetDirection2.z);
+ rootMesh.rotation.y = angle2;
+ console.log(`[拖拽吸附] 模型 ${modelId} 返回原区域 ${originalZoneIndex}`);
+ return;
+ }
+ }
+ } else if (occupiedZoneAction === "replace") {
+ console.log(`[拖拽吸附] 配置为替换模型,将替换模型 ${occupyingModelId}`);
+ }
+ } else {
+ console.log(`[拖拽吸附] 未启用占用区域处理,允许重叠`);
+ }
+ }
+ const offsetDistance = 0;
+ const snapPosition = targetZone.center.add(targetZone.normal.scale(offsetDistance));
+ rootMesh.position.copyFrom(snapPosition);
+ const targetDirection = targetZone.normal.scale(-1);
+ const angle = Math.atan2(targetDirection.x, targetDirection.z);
+ rootMesh.rotation.y = angle;
+ console.log(`[拖拽吸附] 模型 ${modelId} 吸附到区域 ${closestZoneIndex}`);
+ this.updateModelZoneMapping(modelId);
+ }
+ /**
+ * 更新模型所属的分割区域映射
+ * @param modelId 模型ID
+ */
+ updateModelZoneMapping(modelId) {
+ const meshes = this.mainApp.appModel?.modelDic?.Get(modelId);
+ if (!meshes || !meshes.length) return;
+ const rootMesh = meshes[0];
+ const modelPosition = rootMesh.position;
+ console.log(`[边界检测] 模型 ${modelId} 拖拽结束,当前位置:`, modelPosition);
+ const appDropZone = this.mainApp.appDropZone;
+ if (!appDropZone) return;
+ let originalWallName = null;
+ appDropZone["zoneModelMap"]?.forEach((id, zoneKey) => {
+ if (id === modelId) {
+ const match = zoneKey.match(/^(.+)\[(\d+)\]$/);
+ if (match) {
+ originalWallName = match[1];
+ }
+ }
+ });
+ if (!originalWallName) {
+ console.log(`[边界检测] 模型 ${modelId} 未找到原始墙面,跳过检测`);
+ return;
+ }
+ console.log(`[边界检测] 模型 ${modelId} 原始墙面: ${originalWallName}`);
+ const wallZones = appDropZone.getZonesByWall(originalWallName);
+ if (!wallZones.length) return;
+ console.log(`[边界检测] 墙面 ${originalWallName} 有 ${wallZones.length} 个分割区域`);
+ let closestZoneIndex = -1;
+ let minDistance = Number.POSITIVE_INFINITY;
+ wallZones.forEach((zone, index) => {
+ const distance = modelPosition.subtract(zone.center).length();
+ console.log(`[边界检测] 区域 ${index} 中心:`, zone.center, `距离: ${distance.toFixed(3)}`);
+ if (distance < minDistance) {
+ minDistance = distance;
+ closestZoneIndex = index;
+ }
+ });
+ if (closestZoneIndex === -1) {
+ console.log(`[边界检测] 未找到最近的区域`);
+ return;
+ }
+ console.log(`[边界检测] 模型 ${modelId} 最接近区域 ${closestZoneIndex},距离: ${minDistance.toFixed(3)}`);
+ let currentZoneIndex = -1;
+ appDropZone["zoneModelMap"]?.forEach((id, zoneKey) => {
+ if (id === modelId) {
+ const match = zoneKey.match(/^.+\[(\d+)\]$/);
+ if (match) {
+ currentZoneIndex = parseInt(match[1]);
+ }
+ }
+ });
+ if (currentZoneIndex !== closestZoneIndex) {
+ console.log(`[边界检测] 模型 ${modelId} 从区域 ${currentZoneIndex} 移动到区域 ${closestZoneIndex}`);
+ if (currentZoneIndex !== -1) {
+ const oldKey = `${originalWallName}[${currentZoneIndex}]`;
+ appDropZone["zoneModelMap"]?.delete(oldKey);
+ console.log(`[边界检测] 删除旧映射: ${oldKey}`);
+ }
+ const newKey = `${originalWallName}[${closestZoneIndex}]`;
+ const existingModelId = appDropZone["zoneModelMap"]?.get(newKey);
+ const dragInfo = this.modelDragMap.get(modelId);
+ const handleOccupiedZone = dragInfo?.config.handleOccupiedZone ?? false;
+ const occupiedZoneAction = dragInfo?.config.occupiedZoneAction ?? "return";
+ if (existingModelId && existingModelId !== modelId) {
+ console.log(`[边界检测] 目标区域 ${closestZoneIndex} 已有模型 ${existingModelId}`);
+ if (handleOccupiedZone && occupiedZoneAction === "replace") {
+ console.log(`[边界检测] 配置为替换模式,交换位置`);
+ if (currentZoneIndex !== -1) {
+ const swapKey = `${originalWallName}[${currentZoneIndex}]`;
+ appDropZone["zoneModelMap"]?.set(swapKey, existingModelId);
+ console.log(`[边界检测] 模型 ${existingModelId} 移动到区域 ${currentZoneIndex}`);
+ const existingMeshes = this.mainApp.appModel?.modelDic?.Get(existingModelId);
+ if (existingMeshes && existingMeshes.length) {
+ const existingRootMesh = existingMeshes[0];
+ const swapZone = wallZones[currentZoneIndex];
+ if (swapZone) {
+ const offsetDistance = -0.05;
+ const swapPosition = swapZone.center.add(swapZone.normal.scale(offsetDistance));
+ existingRootMesh.position.copyFrom(swapPosition);
+ const targetDirection = swapZone.normal.scale(-1);
+ const angle = Math.atan2(targetDirection.x, targetDirection.z);
+ existingRootMesh.rotation.y = angle;
+ console.log(`[边界检测] 已将模型 ${existingModelId} 物理移动到区域 ${currentZoneIndex}`);
+ }
+ }
+ }
+ } else {
+ console.log(`[边界检测] 未启用替换模式或未启用占用区域处理,允许重叠`);
+ }
+ }
+ appDropZone["zoneModelMap"]?.set(newKey, modelId);
+ console.log(`[边界检测] 添加新映射: ${newKey} -> ${modelId}`);
+ } else {
+ console.log(`[边界检测] 模型 ${modelId} 仍在区域 ${currentZoneIndex},无需更新映射`);
+ }
+ }
/**
* 清理资源
*/
@@ -356884,6 +357249,23 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
line.isVisible = true;
});
}
+ /**
+ * 只显示指定墙面的放置区域
+ * @param wallName 墙面名称
+ */
+ showWall(wallName) {
+ this.hide();
+ this.placementZones.forEach((zone) => {
+ if (zone.wallName === wallName) {
+ zone.mesh.isVisible = true;
+ }
+ });
+ this.borderLines.forEach((line) => {
+ if (line.name.includes(`_${wallName}_`)) {
+ line.isVisible = true;
+ }
+ });
+ }
/**
* 隐藏所有放置区域
*/
@@ -356920,6 +357302,7 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
scene;
placementWall;
appModel = null;
+ mainApp = null;
// 内部映射:放置区域 -> 模型ID
zoneModelMap = /* @__PURE__ */ new Map();
// 墙面 -> 当前分割数
@@ -356940,6 +357323,12 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
setModelManager(appModel) {
this.appModel = appModel;
}
+ /**
+ * 设置 MainApp 引用(内部使用)
+ */
+ setMainApp(mainApp) {
+ this.mainApp = mainApp;
+ }
/**
* 设置放置区域数据
* @param config 配置参数
@@ -357018,6 +357407,9 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
divisions: newDivisions
};
}).filter((wall) => wall !== null);
+ this.dropZoneConfig.walls.forEach((wall) => {
+ this.wallDivisionsMap.set(wall.name, wall.divisions);
+ });
this.clearZones();
const zones = this.generateDropZones();
this.show();
@@ -357071,6 +357463,176 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
}
this.zoneModelMap.set(zoneKey, modelId);
console.log(`已记录模型 ${modelId} 到区域 ${zoneKey}`);
+ this.checkAndAutoArrange(wallName);
+ }
+ /**
+ * 通知模型被删除(外部调用,用于更新映射和重新启用拖拽)
+ * @param modelId 被删除的模型ID
+ */
+ notifyModelRemoved(modelId) {
+ console.log(`[模型删除通知] 模型 ${modelId} 被删除`);
+ let removedWallName = null;
+ let removedZoneKey = null;
+ this.zoneModelMap.forEach((id, zoneKey) => {
+ if (id === modelId) {
+ removedZoneKey = zoneKey;
+ const match = zoneKey.match(/^(.+)\[(\d+)\]$/);
+ if (match) {
+ removedWallName = match[1];
+ }
+ }
+ });
+ if (removedZoneKey) {
+ this.zoneModelMap.delete(removedZoneKey);
+ console.log(`[模型删除通知] 已从映射中删除: ${removedZoneKey}`);
+ }
+ if (removedWallName) {
+ this.checkAndReenableDrag(removedWallName);
+ }
+ }
+ /**
+ * 检查墙面是否不满,如果不满则重新启用该墙面所有模型的拖拽
+ * @param wallName 墙面名称
+ */
+ checkAndReenableDrag(wallName) {
+ const currentDivisions = this.wallDivisionsMap.get(wallName);
+ if (!currentDivisions) return;
+ let placedCount = 0;
+ const placedModelIds = [];
+ this.zoneModelMap.forEach((modelId, zoneKey) => {
+ if (zoneKey.startsWith(`${wallName}[`)) {
+ placedCount++;
+ placedModelIds.push(modelId);
+ }
+ });
+ console.log(`[拖拽检查] 墙面 ${wallName} 当前模型数: ${placedCount}/${currentDivisions}`);
+ if (placedCount < currentDivisions) {
+ console.log(`[拖拽检查] 墙面 ${wallName} 未满,重新启用拖拽`);
+ placedModelIds.forEach((modelId) => {
+ if (this.mainApp && this.mainApp.appModelDrag && typeof this.mainApp.appModelDrag.setDragEnabled === "function") {
+ this.mainApp.appModelDrag.setDragEnabled(modelId, true);
+ console.log(`[拖拽检查] ✓ 已启用模型 ${modelId} 的拖拽功能`);
+ }
+ });
+ }
+ }
+ /**
+ * 检查墙面是否已满
+ * @param wallName 墙面名称
+ * @returns 是否已满
+ */
+ isWallFull(wallName) {
+ const currentDivisions = this.wallDivisionsMap.get(wallName);
+ if (!currentDivisions) return false;
+ let placedCount = 0;
+ this.zoneModelMap.forEach((modelId, zoneKey) => {
+ if (zoneKey.startsWith(`${wallName}[`)) {
+ placedCount++;
+ }
+ });
+ return placedCount >= currentDivisions;
+ }
+ /**
+ * 检查墙面是否已满,如果满了则自动排列模型
+ * @param wallName 墙面名称
+ */
+ checkAndAutoArrange(wallName) {
+ const currentDivisions = this.wallDivisionsMap.get(wallName);
+ console.log(`[自动排列检查] 墙面: ${wallName}, 分割数: ${currentDivisions}`);
+ if (!currentDivisions) {
+ console.log(`[自动排列检查] 墙面 ${wallName} 没有分割数配置,跳过`);
+ return;
+ }
+ let placedCount = 0;
+ const placedModels = [];
+ this.zoneModelMap.forEach((modelId, zoneKey) => {
+ if (zoneKey.startsWith(`${wallName}[`)) {
+ placedCount++;
+ placedModels.push(`${zoneKey} -> ${modelId}`);
+ }
+ });
+ console.log(`[自动排列检查] 墙面 ${wallName} 已放置模型数: ${placedCount}/${currentDivisions}`);
+ console.log(`[自动排列检查] 已放置的模型:`, placedModels);
+ if (placedCount === currentDivisions) {
+ console.log(`[自动排列] 墙面 ${wallName} 已满(${placedCount}/${currentDivisions}),开始执行自动排列`);
+ this.autoArrangeWall(wallName);
+ } else {
+ console.log(`[自动排列检查] 墙面 ${wallName} 未满,不执行自动排列`);
+ }
+ }
+ /**
+ * 自动排列墙面上的所有模型
+ * @param wallName 墙面名称
+ */
+ autoArrangeWall(wallName) {
+ console.log(`[自动排列] 开始排列墙面: ${wallName}`);
+ const wallZones = this.getZonesByWall(wallName);
+ console.log(`[自动排列] 墙面 ${wallName} 的放置区域数量: ${wallZones.length}`);
+ if (!wallZones.length) {
+ console.log(`[自动排列] 墙面 ${wallName} 没有放置区域,退出`);
+ return;
+ }
+ const placedModels = [];
+ this.zoneModelMap.forEach((modelId, zoneKey) => {
+ const match = zoneKey.match(new RegExp(`^${wallName}\\[(\\d+)\\]$`));
+ if (match) {
+ const currentIndex = parseInt(match[1]);
+ placedModels.push({
+ modelId,
+ currentIndex
+ });
+ console.log(`[自动排列] 找到模型: ${modelId}, 当前索引: ${currentIndex}`);
+ }
+ });
+ console.log(`[自动排列] 收集到 ${placedModels.length} 个模型`);
+ placedModels.sort((a, b) => a.currentIndex - b.currentIndex);
+ console.log(`[自动排列] 排序后的模型顺序:`, placedModels.map((m) => `${m.modelId}(索引${m.currentIndex})`));
+ placedModels.forEach((model, newIndex) => {
+ console.log(`[自动排列] 处理模型 ${model.modelId}: 当前索引=${model.currentIndex}, 目标索引=${newIndex}`);
+ const targetZone = wallZones[newIndex];
+ if (!targetZone) {
+ console.warn(`[自动排列] ✗ 找不到索引 ${newIndex} 的放置区域`);
+ return;
+ }
+ if (this.appModel) {
+ const offsetDistance = 0;
+ const targetPosition = targetZone.center.add(targetZone.normal.scale(offsetDistance));
+ const targetDirection = targetZone.normal.scale(-1);
+ const angle = Math.atan2(targetDirection.x, targetDirection.z);
+ console.log(`[自动排列] 目标区域 ${newIndex} 的位置:`, {
+ center: targetZone.center,
+ normal: targetZone.normal,
+ targetPosition,
+ rotation: angle * 180 / Math.PI
+ });
+ const meshes = this.appModel.getCachedMeshes(model.modelId);
+ if (meshes && meshes.length > 0) {
+ const rootMesh = meshes[0];
+ rootMesh.position.copyFrom(targetPosition);
+ rootMesh.rotation.y = angle;
+ console.log(`[自动排列] ✓ 模型 ${model.modelId} 已移动到索引 ${newIndex} 的位置`);
+ } else {
+ console.warn(`[自动排列] ✗ 找不到模型 ${model.modelId} 的网格`);
+ }
+ if (model.currentIndex !== newIndex) {
+ const oldKey = `${wallName}[${model.currentIndex}]`;
+ const newKey = `${wallName}[${newIndex}]`;
+ this.zoneModelMap.delete(oldKey);
+ this.zoneModelMap.set(newKey, model.modelId);
+ console.log(`[自动排列] 更新映射: ${oldKey} -> ${newKey}`);
+ }
+ }
+ });
+ console.log(`[自动排列] 开始禁用拖拽功能`);
+ placedModels.forEach((model) => {
+ if (this.mainApp && this.mainApp.appModelDrag && typeof this.mainApp.appModelDrag.setDragEnabled === "function") {
+ this.mainApp.appModelDrag.setDragEnabled(model.modelId, false);
+ console.log(`[自动排列] ✓ 已禁用模型 ${model.modelId} 的拖拽功能`);
+ } else {
+ console.warn(`[自动排列] ✗ 无法禁用模型 ${model.modelId} 的拖拽功能:appModelDrag 未初始化`);
+ }
+ });
+ console.log(`[自动排列] 墙面 ${wallName} 自动排列完成`);
}
/**
* 获取所有放置区域
@@ -357101,12 +357663,37 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
*/
show() {
this.placementWall.show();
+ this.setModelsPickable(false);
+ }
+ /**
+ * 只显示指定墙面的放置区域
+ * @param wallName 墙面名称
+ */
+ showWall(wallName) {
+ this.placementWall.showWall(wallName);
+ this.setModelsPickable(false);
}
/**
* 隐藏所有放置区域
*/
hide() {
this.placementWall.hide();
+ this.setModelsPickable(true);
+ }
+ /**
+ * 设置所有已放置模型的可拾取状态
+ * @param pickable 是否可拾取
+ */
+ setModelsPickable(pickable) {
+ if (!this.appModel) return;
+ this.zoneModelMap.forEach((modelId) => {
+ const meshes = this.appModel.getCachedMeshes(modelId);
+ if (meshes && meshes.length > 0) {
+ meshes.forEach((mesh) => {
+ mesh.isPickable = pickable;
+ });
+ }
+ });
}
/**
* 清除所有放置区域(只清除网格,不清除模型)
@@ -357501,6 +358088,7 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
if (this.config.receiveShadows) {
this.ground.receiveShadows = true;
}
+ this.ground.isPickable = false;
}
/** 创建材质 */
createMaterial() {
@@ -357539,6 +358127,7 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
const gridPosition = this.config.position ? this.config.position.clone() : new Vector3(0, 0, 0);
gridPosition.y += 0.01;
this.gridGround.position = gridPosition;
+ this.gridGround.isPickable = false;
this.gridMaterial = new GridMaterial("gridMaterial", this.mainApp.appScene.object);
this.gridMaterial.mainColor = this.config.gridColor || new Color3(0.3, 0.3, 0.3);
this.gridMaterial.lineColor = this.config.gridColor || new Color3(0.3, 0.3, 0.3);
@@ -357663,6 +358252,7 @@ clipPos=viewProjection*worldPos;previousClipPos=previousViewProjection*previousW
this.appModel.initManagers();
this.appDropZone = new AppDropZone(this.appScene.object);
this.appDropZone.setModelManager(this.appModel);
+ this.appDropZone.setMainApp(this);
this.update();
EventBridge.sceneReady({ scene: this.appScene.object });
}
diff --git a/index.html b/index.html
index 165cbb9..56fd27f 100644
--- a/index.html
+++ b/index.html
@@ -275,33 +275,40 @@
letter-spacing: 2px;
}
- /* 进度条样式 */
+ /* 加载遮罩样式 */
#progress-container {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- width: 80%;
- max-width: 500px;
- background: rgba(255, 255, 255, 0.2);
- border-radius: 10px;
- padding: 10px;
- z-index: 1000;
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100vw;
+ height: 100vh;
+ background: rgba(0, 0, 0, 0.7);
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ z-index: 9999;
}
- #progress-bar {
- width: 0%;
- height: 10px;
- background: linear-gradient(90deg, #4CAF50, #45a049);
- border-radius: 5px;
- transition: width 0.1s ease;
+ .spinner {
+ width: 50px;
+ height: 50px;
+ border: 4px solid rgba(255, 255, 255, 0.3);
+ border-top-color: #4CAF50;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
}
- #progress-text {
- color: white;
- text-align: center;
- margin-top: 5px;
- font-size: 14px;
+ @keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+ }
+
+ .loading-text {
+ color: #fff;
+ font-size: 16px;
+ margin-top: 20px;
}
@@ -312,8 +319,8 @@
@@ -892,6 +899,7 @@
// 移除按钮事件
document.getElementById('remove-model-btn').addEventListener('click', () => {
+
const pickedMesh = window.getCurrentPickedMesh();
if (pickedMesh) {
const meshName = pickedMesh.name;
@@ -916,9 +924,25 @@
// 监听放置区域点击事件
kernel.on('dropzone:click', async (dropzone_data) => {
+ // 显示进度条
+ const progressContainer = document.getElementById('progress-container');
+ if (progressContainer) {
+ progressContainer.style.display = 'flex';
+ }
+
getEvent(dropzone_data, sku)
});
+ // 监听模型加载完成事件
+ kernel.on('model:loaded', (data) => {
+ console.log('模型加载完成', data);
+ // 隐藏进度条
+ const progressContainer = document.getElementById('progress-container');
+ if (progressContainer) {
+ progressContainer.style.display = 'none';
+ }
+ });
+
// 存储当前选中的材质名和网格
let currentMaterialName = '';
@@ -994,6 +1018,8 @@
// 暴露 kernel 到全局,方便调试
+ kernel.on('model:loaded', (event) => {
+ });
kernel.on('hotspot:click', (event) => {
console.log('热点被点击:', event);
diff --git a/index.js b/index.js
index 2564b73..3f76935 100644
--- a/index.js
+++ b/index.js
@@ -44,7 +44,8 @@ export const init = async (customConfig = {}) => {
container: document.querySelector('#renderDom'),
modelUrlList: [],
env: { envPath: 'https://cdn.files.zguiy.com/zt/environment.env', intensity: 1.2, rotationY: 0.3, background: false },
- gizmo: {
+ gcamera:{},
+ izmo: {
position: false,
rotation: false,
scale: false
@@ -62,7 +63,7 @@ export const init = async (customConfig = {}) => {
const config = { ...defaultConfig, ...customConfig };
kernel.init(config);
}
-
+//
//初始化加载模型
export const getAutoLoadModelList = async () => {
const kernel = getKernel();
@@ -201,6 +202,7 @@ export const executeEvent = async (dropzone_data, result, sku) => {
// 第一次循环:处理 change_model
for (const event of result.data.events) {
if (event.event_type === 'change_model') {
+
const { name, file_url, model_control_type, category } = event.target_data;
// 生成唯一的模型ID
@@ -208,8 +210,7 @@ export const executeEvent = async (dropzone_data, result, sku) => {
modelName = name;
kernel.dropZone.recordModelPlacement(wallName, index, name + '_' + modelId);
- // 记录模型ID到SKU的映射
- setSkuMapping(modelId, sku);
+
await kernel.model.add({
modelName: name,
@@ -220,10 +221,10 @@ export const executeEvent = async (dropzone_data, result, sku) => {
enable: true,
axis: rotation.y === 0 || rotation.y === 180 ? 'x' : 'z',
step: 0.1,
- // snapToZone: true, // 开关1:拖拽吸附到最近的分割区域
- // returnWhenOutOfBounds: false, // 开关2:拖拽到区域外时返回原位置
- // handleOccupiedZone: true, // 开关3:处理已占用区域(false=允许重叠)
- // occupiedZoneAction: 'return' // 当开关3=true时的行为:'return'=返回原位置,'replace'=替换
+ snapToZone: true, // 拖拽吸附到最近的分割区域
+ returnWhenOutOfBounds: true, // 拖拽到区域外时返回原位置
+ handleOccupiedZone: true, // 处理已占用区域(false=允许重叠)
+ occupiedZoneAction: 'replace' // 当开关3=true时的行为:'return'=返回原位置,'replace'=替换
},
transform: {
position: position,
diff --git a/src/babylonjs/AppCamera.ts b/src/babylonjs/AppCamera.ts
index 0286172..b28dc18 100644
--- a/src/babylonjs/AppCamera.ts
+++ b/src/babylonjs/AppCamera.ts
@@ -25,7 +25,7 @@ export class AppCamera extends Monobehiver {
this.object = new ArcRotateCamera('Camera', Tools.ToRadians(70), Tools.ToRadians(85), 5, new Vector3(0, 2, 0), scene);
this.object.attachControl(canvas, true);
this.object.minZ = 0.01; // 近裁剪面
- this.object.wheelPrecision =200; // 滚轮缩放精度
+ this.object.wheelPrecision =200; // 滚轮缩放精度
this.object.panningSensibility = 0;
// 限制垂直角范围,实现上帝视角