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 @@
- +
@@ -870,11 +870,7 @@ console.log(kernel); - // 监听放置区域点击事件 - kernel.on('dropzone:click', function (dropzone_data) { - window.AppLogic.getEvent(dropzone_data, sku); - }); - + // 存储当前选中的材质名和网格 var currentMaterialName = ''; var currentPickedMesh = null; diff --git a/examples/demo-module.html b/examples/demo-module.html index 9fc44af..b046526 100644 --- a/examples/demo-module.html +++ b/examples/demo-module.html @@ -221,33 +221,40 @@ cursor: pointer; } - /* 进度条样式 */ + /* 加载遮罩样式 */ #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; } @@ -258,8 +265,8 @@
@@ -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; // 限制垂直角范围,实现上帝视角